diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 21b1193b07..939fab68b8 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -51,8 +51,13 @@ IGNORED_MODULES_IN_AYON = set() # - this is used to log the missing addon MOVED_ADDON_MILESTONE_VERSIONS = { "applications": VersionInfo(0, 2, 0), + "clockify": VersionInfo(0, 2, 0), + "traypublisher": VersionInfo(0, 2, 0), + "tvpaint": VersionInfo(0, 2, 0), + "nuke": VersionInfo(0, 2, 0), } + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing AYON addons. diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 0817afec71..6c30b267bc 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -1,7 +1,7 @@ from ayon_applications import PreLaunchHook -from ayon_core.pipeline.colorspace import get_imageio_config -from ayon_core.pipeline.template_data import get_template_data_with_names +from ayon_core.pipeline.colorspace import get_imageio_config_preset +from ayon_core.pipeline.template_data import get_template_data class OCIOEnvHook(PreLaunchHook): @@ -26,32 +26,38 @@ class OCIOEnvHook(PreLaunchHook): def execute(self): """Hook entry method.""" - template_data = get_template_data_with_names( - project_name=self.data["project_name"], - folder_path=self.data["folder_path"], - task_name=self.data["task_name"], + folder_entity = self.data["folder_entity"] + + template_data = get_template_data( + self.data["project_entity"], + folder_entity=folder_entity, + task_entity=self.data["task_entity"], host_name=self.host_name, - settings=self.data["project_settings"] + settings=self.data["project_settings"], ) - config_data = get_imageio_config( - project_name=self.data["project_name"], - host_name=self.host_name, - project_settings=self.data["project_settings"], - anatomy_data=template_data, + config_data = get_imageio_config_preset( + self.data["project_name"], + self.data["folder_path"], + self.data["task_name"], + self.host_name, anatomy=self.data["anatomy"], + project_settings=self.data["project_settings"], + template_data=template_data, env=self.launch_context.env, + folder_id=folder_entity["id"], ) - if config_data: - ocio_path = config_data["path"] - - if self.host_name in ["nuke", "hiero"]: - ocio_path = ocio_path.replace("\\", "/") - - self.log.info( - f"Setting OCIO environment to config path: {ocio_path}") - - self.launch_context.env["OCIO"] = ocio_path - else: + if not config_data: self.log.debug("OCIO not set or enabled") + return + + ocio_path = config_data["path"] + + if self.host_name in ["nuke", "hiero"]: + ocio_path = ocio_path.replace("\\", "/") + + self.log.info( + f"Setting OCIO environment to config path: {ocio_path}") + + self.launch_context.env["OCIO"] = ocio_path diff --git a/client/ayon_core/hosts/aftereffects/api/launch_logic.py b/client/ayon_core/hosts/aftereffects/api/launch_logic.py index 5a23f2cb35..da6887668a 100644 --- a/client/ayon_core/hosts/aftereffects/api/launch_logic.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_logic.py @@ -60,7 +60,7 @@ def main(*subprocess_args): ) ) - elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): + elif os.environ.get("AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH", True): save = False if os.getenv("WORKFILES_SAVE_AS"): save = True diff --git a/client/ayon_core/hosts/blender/api/lib.py b/client/ayon_core/hosts/blender/api/lib.py index 32137f0fcd..ea37f17d05 100644 --- a/client/ayon_core/hosts/blender/api/lib.py +++ b/client/ayon_core/hosts/blender/api/lib.py @@ -365,3 +365,62 @@ def maintained_time(): yield finally: bpy.context.scene.frame_current = current_time + + +def get_all_parents(obj): + """Get all recursive parents of object. + + Arguments: + obj (bpy.types.Object): Object to get all parents for. + + Returns: + List[bpy.types.Object]: All parents of object + + """ + result = [] + while True: + obj = obj.parent + if not obj: + break + result.append(obj) + return result + + +def get_highest_root(objects): + """Get the highest object (the least parents) among the objects. + + If multiple objects have the same amount of parents (or no parents) the + first object found in the input iterable will be returned. + + Note that this will *not* return objects outside of the input list, as + such it will not return the root of node from a child node. It is purely + intended to find the highest object among a list of objects. To instead + get the root from one object use, e.g. `get_all_parents(obj)[-1]` + + Arguments: + objects (List[bpy.types.Object]): Objects to find the highest root in. + + Returns: + Optional[bpy.types.Object]: First highest root found or None if no + `bpy.types.Object` found in input list. + + """ + included_objects = {obj.name_full for obj in objects} + num_parents_to_obj = {} + for obj in objects: + if isinstance(obj, bpy.types.Object): + parents = get_all_parents(obj) + # included parents + parents = [parent for parent in parents if + parent.name_full in included_objects] + if not parents: + # A node without parents must be a highest root + return obj + + num_parents_to_obj.setdefault(len(parents), obj) + + if not num_parents_to_obj: + return + + minimum_parent = min(num_parents_to_obj) + return num_parents_to_obj[minimum_parent] diff --git a/client/ayon_core/hosts/blender/api/plugin.py b/client/ayon_core/hosts/blender/api/plugin.py index 4a13d16805..e00e127f70 100644 --- a/client/ayon_core/hosts/blender/api/plugin.py +++ b/client/ayon_core/hosts/blender/api/plugin.py @@ -26,7 +26,8 @@ from .ops import ( ) from .lib import imprint -VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] +VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx", + ".usd", ".usdc", ".usda"] def prepare_scene_name( diff --git a/client/ayon_core/hosts/blender/plugins/create/create_usd.py b/client/ayon_core/hosts/blender/plugins/create/create_usd.py new file mode 100644 index 0000000000..2c2d0c46c6 --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/create/create_usd.py @@ -0,0 +1,30 @@ +"""Create a USD Export.""" + +from ayon_core.hosts.blender.api import plugin, lib + + +class CreateUSD(plugin.BaseCreator): + """Create USD Export""" + + identifier = "io.openpype.creators.blender.usd" + name = "usdMain" + label = "USD" + product_type = "usd" + icon = "gears" + + def create( + self, product_name: str, instance_data: dict, pre_create_data: dict + ): + # Run parent create method + collection = super().create( + product_name, instance_data, pre_create_data + ) + + if pre_create_data.get("use_selection"): + objects = lib.get_selection() + for obj in objects: + collection.objects.link(obj) + if obj.type == 'EMPTY': + objects.extend(obj.children) + + return collection diff --git a/client/ayon_core/hosts/blender/plugins/load/load_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_cache.py similarity index 94% rename from client/ayon_core/hosts/blender/plugins/load/load_abc.py rename to client/ayon_core/hosts/blender/plugins/load/load_cache.py index c074b5ed13..30c847f89d 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_cache.py @@ -26,10 +26,10 @@ class CacheModelLoader(plugin.AssetLoader): Note: At least for now it only supports Alembic files. """ - product_types = {"model", "pointcache", "animation"} - representations = {"abc"} + product_types = {"model", "pointcache", "animation", "usd"} + representations = {"abc", "usd"} - label = "Load Alembic" + label = "Load Cache" icon = "code-fork" color = "orange" @@ -53,10 +53,21 @@ class CacheModelLoader(plugin.AssetLoader): plugin.deselect_all() relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.ops.wm.alembic_import( - filepath=libpath, - relative_path=relative - ) + + if any(libpath.lower().endswith(ext) + for ext in [".usd", ".usda", ".usdc"]): + # USD + bpy.ops.wm.usd_import( + filepath=libpath, + relative_path=relative + ) + + else: + # Alembic + bpy.ops.wm.alembic_import( + filepath=libpath, + relative_path=relative + ) imported = lib.get_selection() diff --git a/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py index 6178578081..a49bb40d9a 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py @@ -43,7 +43,10 @@ class AbcCameraLoader(plugin.AssetLoader): def _process(self, libpath, asset_group, group_name): plugin.deselect_all() - bpy.ops.wm.alembic_import(filepath=libpath) + # Force the creation of the transform cache even if the camera + # doesn't have an animation. We use the cache to update the camera. + bpy.ops.wm.alembic_import( + filepath=libpath, always_add_cache_reader=True) objects = lib.get_selection() @@ -178,12 +181,33 @@ class AbcCameraLoader(plugin.AssetLoader): self.log.info("Library already loaded, not updating...") return - mat = asset_group.matrix_basis.copy() + for obj in asset_group.children: + found = False + for constraint in obj.constraints: + if constraint.type == "TRANSFORM_CACHE": + constraint.cache_file.filepath = libpath.as_posix() + found = True + break + if not found: + # This is to keep compatibility with cameras loaded with + # the old loader + # Create a new constraint for the cache file + constraint = obj.constraints.new("TRANSFORM_CACHE") + bpy.ops.cachefile.open(filepath=libpath.as_posix()) + constraint.cache_file = bpy.data.cache_files[-1] + constraint.cache_file.scale = 1.0 - self._remove(asset_group) - self._process(str(libpath), asset_group, object_name) + # This is a workaround to set the object path. Blender doesn't + # load the list of object paths until the object is evaluated. + # This is a hack to force the object to be evaluated. + # The modifier doesn't need to be removed because camera + # objects don't have modifiers. + obj.modifiers.new( + name='MeshSequenceCache', type='MESH_SEQUENCE_CACHE') + bpy.context.evaluated_depsgraph_get() - asset_group.matrix_basis = mat + constraint.object_path = ( + constraint.cache_file.object_paths[0].path) metadata["libpath"] = str(libpath) metadata["representation"] = repre_entity["id"] diff --git a/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py b/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py index d47c69a270..314ffd368a 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py +++ b/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py @@ -12,7 +12,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["blender"] families = ["model", "pointcache", "animation", "rig", "camera", "layout", - "blendScene"] + "blendScene", "usd"] label = "Collect Instance" def process(self, instance): diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py new file mode 100644 index 0000000000..1d4fa3d7ac --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py @@ -0,0 +1,90 @@ +import os + +import bpy + +from ayon_core.pipeline import publish +from ayon_core.hosts.blender.api import plugin, lib + + +class ExtractUSD(publish.Extractor): + """Extract as USD.""" + + label = "Extract USD" + hosts = ["blender"] + families = ["usd"] + + def process(self, instance): + + # Ignore runtime instances (e.g. USD layers) + # TODO: This is better done via more specific `families` + if not instance.data.get("transientData", {}).get("instance_node"): + return + + # Define extract output file path + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.usd" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.debug("Performing extraction..") + + # Select all members to "export selected" + plugin.deselect_all() + + selected = [] + for obj in instance: + if isinstance(obj, bpy.types.Object): + obj.select_set(True) + selected.append(obj) + + root = lib.get_highest_root(objects=instance[:]) + if not root: + instance_node = instance.data["transientData"]["instance_node"] + raise publish.KnownPublishError( + f"No root object found in instance: {instance_node.name}" + ) + self.log.debug(f"Exporting using active root: {root.name}") + + context = plugin.create_blender_context( + active=root, selected=selected) + + # Export USD + with bpy.context.temp_override(**context): + bpy.ops.wm.usd_export( + filepath=filepath, + selected_objects_only=True, + export_textures=False, + relative_paths=False, + export_animation=False, + export_hair=False, + export_uvmaps=True, + # TODO: add for new version of Blender (4+?) + # export_mesh_colors=True, + export_normals=True, + export_materials=True, + use_instancing=True + ) + + plugin.deselect_all() + + # Add representation + representation = { + 'name': 'usd', + 'ext': 'usd', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data.setdefault("representations", []).append(representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) + + +class ExtractModelUSD(ExtractUSD): + """Extract model as USD.""" + + label = "Extract USD (Model)" + hosts = ["blender"] + families = ["model"] + + # Driven by settings + optional = True diff --git a/client/ayon_core/hosts/fusion/api/action.py b/client/ayon_core/hosts/fusion/api/action.py index 1643f1ce03..a0c6aafcb5 100644 --- a/client/ayon_core/hosts/fusion/api/action.py +++ b/client/ayon_core/hosts/fusion/api/action.py @@ -58,3 +58,55 @@ class SelectInvalidAction(pyblish.api.Action): self.log.info( "Selecting invalid tools: %s" % ", ".join(sorted(names)) ) + + +class SelectToolAction(pyblish.api.Action): + """Select invalid output tool in Fusion when plug-in failed. + + """ + + label = "Select saver" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context( + context, + plugin=plugin, + ) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + tools = [] + for instance in errored_instances: + + tool = instance.data.get("tool") + if tool is not None: + tools.append(tool) + else: + self.log.warning( + "Plug-in returned to be invalid, " + f"but has no saver for instance {instance.name}." + ) + + if not tools: + # Assume relevant comp is current comp and clear selection + self.log.info("No invalid tools found.") + comp = get_current_comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + return + + # Assume a single comp + first_tool = tools[0] + comp = first_tool.Comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + names = set() + for tool in tools: + flow.Select(tool, True) + comp.SetActiveTool(tool) + names.add(tool.Name) + self.log.info( + "Selecting invalid tools: %s" % ", ".join(sorted(names)) + ) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py index 95d029aad4..9c04e59717 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py @@ -52,7 +52,7 @@ class CollectFusionRender( if product_type not in ["render", "image"]: continue - task_name = context.data["task"] + task_name = inst.data["task"] tool = inst.data["transientData"]["tool"] instance_families = inst.data.get("families", []) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py b/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..3aa6fb452f --- /dev/null +++ b/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""Validate if instance context is the same as publish context.""" + +import pyblish.api +from ayon_core.hosts.fusion.api.action import SelectToolAction +from ayon_core.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) + + +class ValidateInstanceInContextFusion(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validator to check if instance context matches context of publish. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + """ + # Similar to maya and houdini-equivalent `ValidateInstanceInContext` + + order = ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["fusion"] + actions = [SelectToolAction, RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + + instance_context = self.get_context(instance.data) + context = self.get_context(instance.context.data) + if instance_context != context: + context_label = "{} > {}".format(*context) + instance_label = "{} > {}".format(*instance_context) + + raise PublishValidationError( + message=( + "Instance '{}' publishes to different asset than current " + "context: {}. Current context: {}".format( + instance.name, instance_label, context_label + ) + ), + description=( + "## Publishing to a different asset\n" + "There are publish instances present which are publishing " + "into a different asset than your current context.\n\n" + "Usually this is not what you want but there can be cases " + "where you might want to publish into another asset or " + "shot. If that's the case you can disable the validation " + "on the instance to ignore it." + ) + ) + + @classmethod + def repair(cls, instance): + + create_context = instance.context.data["create_context"] + instance_id = instance.data.get("instance_id") + created_instance = create_context.get_instance_by_id( + instance_id + ) + if created_instance is None: + raise RuntimeError( + f"No CreatedInstances found with id '{instance_id} " + f"in {create_context.instances_by_id}" + ) + + context_asset, context_task = cls.get_context(instance.context.data) + created_instance["folderPath"] = context_asset + created_instance["task"] = context_task + create_context.save_changes() + + @staticmethod + def get_context(data): + """Return asset, task from publishing context data""" + return data["folderPath"], data["task"] diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index aaf99546c7..456a68f125 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -1110,10 +1110,7 @@ def apply_colorspace_project(): ''' # backward compatibility layer # TODO: remove this after some time - config_data = get_imageio_config( - project_name=get_current_project_name(), - host_name="hiero" - ) + config_data = get_current_context_imageio_config_preset() if config_data: presets.update({ diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_arnold_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_arnold_rop.py index f65b54a452..1208cfc1ea 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_arnold_rop.py @@ -13,11 +13,17 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Default extension ext = "exr" - # Default to split export and render jobs - export_job = True + # Default render target + render_target = "farm_split" def create(self, product_name, instance_data, pre_create_data): import hou + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in ["render_target", "review"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] # Remove the active, we are checking the bypass flag of the nodes instance_data.pop("active", None) @@ -25,8 +31,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 1 - # Submit for job publishing - instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateArnoldRop, self).create( product_name, @@ -51,7 +55,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): "ar_exr_half_precision": 1 # half precision } - if pre_create_data.get("export_job"): + if pre_create_data.get("render_target") == "farm_split": ass_filepath = \ "{export_dir}{product_name}/{product_name}.$F4.ass".format( export_dir=hou.text.expandString("$HIP/pyblish/ass/"), @@ -66,23 +70,41 @@ class CreateArnoldRop(plugin.HoudiniCreator): to_lock = ["productType", "id"] self.lock_parameters(instance_node, to_lock) - def get_pre_create_attr_defs(self): - attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs() + def get_instance_attr_defs(self): + """get instance attribute definitions. + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + "farm_split": "Farm Rendering - Split export & render jobs", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target), + ] + + def get_pre_create_attr_defs(self): image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] - return attrs + [ - BoolDef("farm", - label="Submitting to Farm", - default=True), - BoolDef("export_job", - label="Split export and render jobs", - default=self.export_job), + attrs = [ EnumDef("image_format", image_format_enum, default=self.ext, - label="Image Format Options") + label="Image Format Options"), ] + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_karma_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_karma_rop.py index e91ddbc0ac..48cf5057ab 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_karma_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_karma_rop.py @@ -11,15 +11,23 @@ class CreateKarmaROP(plugin.HoudiniCreator): product_type = "karma_rop" icon = "magic" + # Default render target + render_target = "farm" + def create(self, product_name, instance_data, pre_create_data): import hou # noqa + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + + for key in ["render_target", "review"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] instance_data.pop("active", None) instance_data.update({"node_type": "karma"}) # Add chunk size attribute instance_data["chunkSize"] = 10 - # Submit for job publishing - instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateKarmaROP, self).create( product_name, @@ -86,18 +94,40 @@ class CreateKarmaROP(plugin.HoudiniCreator): to_lock = ["productType", "id"] self.lock_parameters(instance_node, to_lock) - def get_pre_create_attr_defs(self): - attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs() + def get_instance_attr_defs(self): + """get instance attribute definitions. + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + + def get_pre_create_attr_defs(self): image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] - return attrs + [ - BoolDef("farm", - label="Submitting to Farm", - default=True), + attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs() + + attrs += [ EnumDef("image_format", image_format_enum, default="exr", @@ -112,5 +142,6 @@ class CreateKarmaROP(plugin.HoudiniCreator): decimals=0), BoolDef("cam_res", label="Camera Resolution", - default=False) + default=False), ] + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_mantra_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_mantra_rop.py index 64ecf428e9..05b4431aba 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_mantra_rop.py @@ -11,18 +11,22 @@ class CreateMantraROP(plugin.HoudiniCreator): product_type = "mantra_rop" icon = "magic" - # Default to split export and render jobs - export_job = True + # Default render target + render_target = "farm_split" def create(self, product_name, instance_data, pre_create_data): import hou # noqa + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in ["render_target", "review"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] instance_data.pop("active", None) instance_data.update({"node_type": "ifd"}) # Add chunk size attribute instance_data["chunkSize"] = 10 - # Submit for job publishing - instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateMantraROP, self).create( product_name, @@ -46,7 +50,7 @@ class CreateMantraROP(plugin.HoudiniCreator): "vm_picture": filepath, } - if pre_create_data.get("export_job"): + if pre_create_data.get("render_target") == "farm_split": ifd_filepath = \ "{export_dir}{product_name}/{product_name}.$F4.ifd".format( export_dir=hou.text.expandString("$HIP/pyblish/ifd/"), @@ -77,21 +81,40 @@ class CreateMantraROP(plugin.HoudiniCreator): to_lock = ["productType", "id"] self.lock_parameters(instance_node, to_lock) - def get_pre_create_attr_defs(self): - attrs = super(CreateMantraROP, self).get_pre_create_attr_defs() + def get_instance_attr_defs(self): + """get instance attribute definitions. + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + "farm_split": "Farm Rendering - Split export & render jobs", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] - return attrs + [ - BoolDef("farm", - label="Submitting to Farm", - default=True), - BoolDef("export_job", - label="Split export and render jobs", - default=self.export_job), + attrs = super(CreateMantraROP, self).get_pre_create_attr_defs() + + attrs += [ EnumDef("image_format", image_format_enum, default="exr", @@ -100,5 +123,6 @@ class CreateMantraROP(plugin.HoudiniCreator): label="Override Camera Resolution", tooltip="Override the current camera " "resolution, recommended for IPR.", - default=False) + default=False), ] + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_model.py b/client/ayon_core/hosts/houdini/plugins/create/create_model.py new file mode 100644 index 0000000000..74d067b133 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/create/create_model.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating Model product type. + +Note: + Currently, This creator plugin is the same as 'create_pointcache.py' + But renaming the product type to 'model'. + + It's purpose to support + Maya (load/publish model from maya to/from houdini). + + It's considered to support multiple representations in the future. +""" + +from ayon_core.hosts.houdini.api import plugin +from ayon_core.lib import BoolDef + +import hou + + + +class CreateModel(plugin.HoudiniCreator): + """Create Model""" + identifier = "io.openpype.creators.houdini.model" + label = "Model" + product_type = "model" + icon = "cube" + + def create(self, product_name, instance_data, pre_create_data): + instance_data.pop("active", None) + instance_data.update({"node_type": "alembic"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["farm"] = pre_create_data["farm"] + + instance = super(CreateModel, self).create( + product_name, + instance_data, + pre_create_data) + + instance_node = hou.node(instance.get("instance_node")) + parms = { + "use_sop_path": True, + "build_from_path": True, + "path_attrib": "path", + "prim_to_detail_pattern": "cbId", + "format": 2, + "facesets": 0, + "filename": hou.text.expandString( + "$HIP/pyblish/{}.abc".format(product_name)) + } + + if self.selected_nodes: + selected_node = self.selected_nodes[0] + + # Although Houdini allows ObjNode path on `sop_path` for the + # the ROP node we prefer it set to the SopNode path explicitly + + # Allow sop level paths (e.g. /obj/geo1/box1) + if isinstance(selected_node, hou.SopNode): + parms["sop_path"] = selected_node.path() + self.log.debug( + "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." + % selected_node.path() + ) + + # Allow object level paths to Geometry nodes (e.g. /obj/geo1) + # but do not allow other object level nodes types like cameras, etc. + elif isinstance(selected_node, hou.ObjNode) and \ + selected_node.type().name() in ["geo"]: + + # get the output node with the minimum + # 'outputidx' or the node with display flag + sop_path = self.get_obj_output(selected_node) + + if sop_path: + parms["sop_path"] = sop_path.path() + self.log.debug( + "Valid ObjNode selection, 'SOP Path' in ROP will be set to " + "the child path '%s'." + % sop_path.path() + ) + + if not parms.get("sop_path", None): + self.log.debug( + "Selection isn't valid. 'SOP Path' in ROP will be empty." + ) + else: + self.log.debug( + "No Selection. 'SOP Path' in ROP will be empty." + ) + + instance_node.setParms(parms) + instance_node.parm("trange").set(1) + + # Explicitly set f1 and f2 to frame start. + # Which forces the rop node to export one frame. + instance_node.parmTuple('f').deleteAllKeyframes() + fstart = int(hou.hscriptExpression("$FSTART")) + instance_node.parmTuple('f').set((fstart, fstart, 1)) + + # Lock any parameters in this list + to_lock = ["prim_to_detail_pattern"] + self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + + def get_obj_output(self, obj_node): + """Find output node with the smallest 'outputidx'.""" + + outputs = obj_node.subnetOutputs() + + # if obj_node is empty + if not outputs: + return + + # if obj_node has one output child whether its + # sop output node or a node with the render flag + elif len(outputs) == 1: + return outputs[0] + + # if there are more than one, then it have multiple output nodes + # return the one with the minimum 'outputidx' + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) + + def get_instance_attr_defs(self): + return [ + BoolDef("farm", + label="Submitting to Farm", + default=False) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + # Use same attributes as for instance attributes + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 1cd239e929..3ecb09ee9b 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -17,17 +17,21 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ext = "exr" multi_layered_mode = "No Multi-Layered EXR File" - # Default to split export and render jobs - split_render = True + # Default render target + render_target = "farm_split" def create(self, product_name, instance_data, pre_create_data): + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in ["render_target", "review"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] instance_data.pop("active", None) instance_data.update({"node_type": "Redshift_ROP"}) # Add chunk size attribute instance_data["chunkSize"] = 10 - # Submit for job publishing - instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateRedshiftROP, self).create( product_name, @@ -99,7 +103,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): rs_filepath = f"{export_dir}{product_name}/{product_name}.$F4.rs" parms["RS_archive_file"] = rs_filepath - if pre_create_data.get("split_render", self.split_render): + if pre_create_data.get("render_target") == "farm_split": parms["RS_archive_enable"] = 1 instance_node.setParms(parms) @@ -118,24 +122,44 @@ class CreateRedshiftROP(plugin.HoudiniCreator): return super(CreateRedshiftROP, self).remove_instances(instances) + def get_instance_attr_defs(self): + """get instance attribute definitions. + + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + "farm_split": "Farm Rendering - Split export & render jobs", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + def get_pre_create_attr_defs(self): - attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() + image_format_enum = [ "exr", "tif", "jpg", "png", ] + multi_layered_mode = [ "No Multi-Layered EXR File", "Full Multi-Layered EXR File" ] - - return attrs + [ - BoolDef("farm", - label="Submitting to Farm", - default=True), - BoolDef("split_render", - label="Split export and render jobs", - default=self.split_render), + attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() + attrs += [ EnumDef("image_format", image_format_enum, default=self.ext, @@ -143,5 +167,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator): EnumDef("multi_layered_mode", multi_layered_mode, default=self.multi_layered_mode, - label="Multi-Layered EXR") + label="Multi-Layered EXR"), ] + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py index 5ed9e848a7..9e4633e745 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py @@ -16,17 +16,21 @@ class CreateVrayROP(plugin.HoudiniCreator): icon = "magic" ext = "exr" - # Default to split export and render jobs - export_job = True + # Default render target + render_target = "farm_split" def create(self, product_name, instance_data, pre_create_data): + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in ["render_target", "review"]: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] instance_data.pop("active", None) instance_data.update({"node_type": "vray_renderer"}) # Add chunk size attribute instance_data["chunkSize"] = 10 - # Submit for job publishing - instance_data["farm"] = pre_create_data.get("farm") instance = super(CreateVrayROP, self).create( product_name, @@ -55,7 +59,7 @@ class CreateVrayROP(plugin.HoudiniCreator): "SettingsEXR_bits_per_channel": "16" # half precision } - if pre_create_data.get("export_job"): + if pre_create_data.get("render_target") == "farm_split": scene_filepath = \ "{export_dir}{product_name}/{product_name}.$F4.vrscene".format( export_dir=hou.text.expandString("$HIP/pyblish/vrscene/"), @@ -143,20 +147,41 @@ class CreateVrayROP(plugin.HoudiniCreator): return super(CreateVrayROP, self).remove_instances(instances) + def get_instance_attr_defs(self): + """get instance attribute definitions. + + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + "farm_split": "Farm Rendering - Split export & render jobs", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + def get_pre_create_attr_defs(self): - attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", "rad", "rat", "rta", "sgi", "tga", "tif", ] - return attrs + [ - BoolDef("farm", - label="Submitting to Farm", - default=True), - BoolDef("export_job", - label="Split export and render jobs", - default=self.export_job), + attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() + + attrs += [ EnumDef("image_format", image_format_enum, default=self.ext, @@ -172,3 +197,4 @@ class CreateVrayROP(plugin.HoudiniCreator): "if enabled", default=False) ] + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py b/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py index a958509e25..40a607e81a 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py @@ -95,7 +95,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): # write workfile information to context container. op_ctx = hou.node(CONTEXT_CONTAINER) if not op_ctx: - op_ctx = self.create_context_node() + op_ctx = self.host.create_context_node() workfile_data = {"workfile": current_instance.data_to_store()} imprint(op_ctx, workfile_data) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_arnold_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_arnold_rop.py index 7fe38555a3..53a3e52717 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -40,12 +40,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "ar_picture") render_products = [] - # Store whether we are splitting the render job (export + render) - split_render = bool(rop.parm("ar_ass_export_enable").eval()) - instance.data["splitRender"] = split_render export_prefix = None export_products = [] - if split_render: + if instance.data["splitRender"]: export_prefix = evalParmNoFrame( rop, "ar_ass_file", pad_character="0" ) @@ -68,7 +65,12 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): "": self.generate_expected_files(instance, beauty_product) } + # Assume it's a multipartExr Render. + multipartExr = True + num_aovs = rop.evalParm("ar_aovs") + # TODO: Check the following logic. + # as it always assumes that all AOV are not merged. for index in range(1, num_aovs + 1): # Skip disabled AOVs if not rop.evalParm("ar_enable_aov{}".format(index)): @@ -85,6 +87,14 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): files_by_aov[label] = self.generate_expected_files(instance, aov_product) + # Set to False as soon as we have a separated aov. + multipartExr = False + + # Review Logic expects this key to exist and be True + # if render is a multipart Exr. + # As long as we have one AOV then multipartExr should be True. + instance.data["multipartExr"] = multipartExr + for product in render_products: self.log.debug("Found render product: {}".format(product)) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_cache_farm.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_cache_farm.py index e931c7bf1b..e1782b2bb3 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_cache_farm.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_cache_farm.py @@ -11,7 +11,7 @@ class CollectDataforCache(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.11 families = ["ass", "pointcache", "mantraifd", "redshiftproxy", - "vdbcache"] + "vdbcache", "model"] hosts = ["houdini"] targets = ["local", "remote"] label = "Collect Data for Cache" @@ -43,10 +43,7 @@ class CollectDataforCache(pyblish.api.InstancePlugin): cache_files = {"_": instance.data["files"]} # Convert instance family to pointcache if it is bgeo or abc # because ??? - for family in instance.data["families"]: - if family == "bgeo" or "abc": - instance.data["productType"] = "pointcache" - break + self.log.debug(instance.data["families"]) instance.data.update({ "plugin": "Houdini", "publish": True diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_chunk_size.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_chunk_size.py index 3e2561dd6f..f0913f2f0a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_chunk_size.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_chunk_size.py @@ -10,7 +10,7 @@ class CollectChunkSize(pyblish.api.InstancePlugin, order = pyblish.api.CollectorOrder + 0.05 families = ["ass", "pointcache", "vdbcache", "mantraifd", - "redshiftproxy"] + "redshiftproxy", "model"] hosts = ["houdini"] targets = ["local", "remote"] label = "Collect Chunk Size" diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py new file mode 100644 index 0000000000..586aa2da57 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py @@ -0,0 +1,35 @@ +import pyblish.api + + +class CollectFarmInstances(pyblish.api.InstancePlugin): + """Collect instances for farm render.""" + + order = pyblish.api.CollectorOrder + families = ["mantra_rop", + "karma_rop", + "redshift_rop", + "arnold_rop", + "vray_rop"] + + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect farm instances" + + def process(self, instance): + + creator_attribute = instance.data["creator_attributes"] + + # Collect Render Target + if creator_attribute.get("render_target") not in { + "farm_split", "farm" + }: + instance.data["farm"] = False + instance.data["splitRender"] = False + self.log.debug("Render on farm is disabled. " + "Skipping farm collecting.") + return + + instance.data["farm"] = True + instance.data["splitRender"] = ( + creator_attribute.get("render_target") == "farm_split" + ) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_pointcache_type.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_type.py similarity index 51% rename from client/ayon_core/hosts/houdini/plugins/publish/collect_pointcache_type.py rename to client/ayon_core/hosts/houdini/plugins/publish/collect_instances_type.py index 3323e97c20..07851387fe 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_pointcache_type.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_type.py @@ -1,21 +1,24 @@ -"""Collector for pointcache types. +"""Collector for different types. -This will add additional family to pointcache instance based on +This will add additional families to different instance based on the creator_identifier parameter. """ import pyblish.api class CollectPointcacheType(pyblish.api.InstancePlugin): - """Collect data type for pointcache instance.""" + """Collect data type for different instances.""" order = pyblish.api.CollectorOrder hosts = ["houdini"] - families = ["pointcache"] - label = "Collect type of pointcache" + families = ["pointcache", "model"] + label = "Collect instances types" def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.bgeo": # noqa: E501 instance.data["families"] += ["bgeo"] - elif instance.data["creator_identifier"] == "io.openpype.creators.houdini.pointcache": # noqa: E501 + elif instance.data["creator_identifier"] in { + "io.openpype.creators.houdini.pointcache", + "io.openpype.creators.houdini.model" + }: instance.data["families"] += ["abc"] diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py index 78651b0c69..662ed7ae30 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -55,6 +55,12 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): beauty_product) } + # Review Logic expects this key to exist and be True + # if render is a multipart Exr. + # As long as we have one AOV then multipartExr should be True. + # By default karma render is a multipart Exr. + instance.data["multipartExr"] = True + filenames = list(render_products) instance.data["files"] = filenames instance.data["renderProducts"] = colorspace.ARenderProduct() diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_local_render_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_local_render_instances.py new file mode 100644 index 0000000000..474002e1ee --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_local_render_instances.py @@ -0,0 +1,137 @@ +import os +import pyblish.api +from ayon_core.pipeline.create import get_product_name +from ayon_core.pipeline.farm.patterning import match_aov_pattern +from ayon_core.pipeline.publish import ( + get_plugin_settings, + apply_plugin_settings_automatically +) + + +class CollectLocalRenderInstances(pyblish.api.InstancePlugin): + """Collect instances for local render. + + Agnostic Local Render Collector. + """ + + # this plugin runs after Collect Render Products + order = pyblish.api.CollectorOrder + 0.12 + families = ["mantra_rop", + "karma_rop", + "redshift_rop", + "arnold_rop", + "vray_rop"] + + hosts = ["houdini"] + label = "Collect local render instances" + + use_deadline_aov_filter = False + aov_filter = {"host_name": "houdini", + "value": [".*([Bb]eauty).*"]} + + @classmethod + def apply_settings(cls, project_settings): + # Preserve automatic settings applying logic + settings = get_plugin_settings(plugin=cls, + project_settings=project_settings, + log=cls.log, + category="houdini") + apply_plugin_settings_automatically(cls, settings, logger=cls.log) + + if not cls.use_deadline_aov_filter: + # get aov_filter from collector settings + # and restructure it as match_aov_pattern requires. + cls.aov_filter = { + cls.aov_filter["host_name"]: cls.aov_filter["value"] + } + else: + # get aov_filter from deadline settings + cls.aov_filter = project_settings["deadline"]["publish"]["ProcessSubmittedJobOnFarm"]["aov_filter"] + cls.aov_filter = { + item["name"]: item["value"] + for item in cls.aov_filter + } + + def process(self, instance): + + if instance.data["farm"]: + self.log.debug("Render on farm is enabled. " + "Skipping local render collecting.") + return + + # Create Instance for each AOV. + context = instance.context + expectedFiles = next(iter(instance.data["expectedFiles"]), {}) + + product_type = "render" # is always render + product_group = get_product_name( + context.data["projectName"], + context.data["taskEntity"]["name"], + context.data["taskEntity"]["taskType"], + context.data["hostName"], + product_type, + instance.data["productName"] + ) + + for aov_name, aov_filepaths in expectedFiles.items(): + product_name = product_group + + if aov_name: + product_name = "{}_{}".format(product_name, aov_name) + + # Create instance for each AOV + aov_instance = context.create_instance(product_name) + + # Prepare Representation for each AOV + aov_filenames = [os.path.basename(path) for path in aov_filepaths] + staging_dir = os.path.dirname(aov_filepaths[0]) + ext = aov_filepaths[0].split(".")[-1] + + # Decide if instance is reviewable + preview = False + if instance.data.get("multipartExr", False): + # Add preview tag because its multipartExr. + preview = True + else: + # Add Preview tag if the AOV matches the filter. + preview = match_aov_pattern( + "houdini", self.aov_filter, aov_filenames[0] + ) + + preview = preview and instance.data.get("review", False) + + # Support Single frame. + # The integrator wants single files to be a single + # filename instead of a list. + # More info: https://github.com/ynput/ayon-core/issues/238 + if len(aov_filenames) == 1: + aov_filenames = aov_filenames[0] + + aov_instance.data.update({ + # 'label': label, + "task": instance.data["task"], + "folderPath": instance.data["folderPath"], + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], + "productType": product_type, + "family": product_type, + "productName": product_name, + "productGroup": product_group, + "families": ["render.local.hou", "review"], + "instance_node": instance.data["instance_node"], + "representations": [ + { + "stagingDir": staging_dir, + "ext": ext, + "name": ext, + "tags": ["review"] if preview else [], + "files": aov_filenames, + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"] + } + ] + }) + + # Skip integrating original render instance. + # We are not removing it because it's used to trigger the render. + instance.data["integrate"] = False diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py index df9acc4b61..7b247768fc 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -44,12 +44,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "vm_picture") render_products = [] - # Store whether we are splitting the render job (export + render) - split_render = bool(rop.parm("soho_outputmode").eval()) - instance.data["splitRender"] = split_render export_prefix = None export_products = [] - if split_render: + if instance.data["splitRender"]: export_prefix = evalParmNoFrame( rop, "soho_diskfile", pad_character="0" ) @@ -74,6 +71,11 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): beauty_product) } + # Assume it's a multipartExr Render. + multipartExr = True + + # TODO: This logic doesn't take into considerations + # cryptomatte defined in 'Images > Cryptomatte' aov_numbers = rop.evalParm("vm_numaux") if aov_numbers > 0: # get the filenames of the AOVs @@ -93,6 +95,14 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa + # Set to False as soon as we have a separated aov. + multipartExr = False + + # Review Logic expects this key to exist and be True + # if render is a multipart Exr. + # As long as we have one AOV then multipartExr should be True. + instance.data["multipartExr"] = multipartExr + for product in render_products: self.log.debug("Found render product: %s" % product) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_output_node.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_output_node.py index 26381e065e..289222f32b 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_output_node.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_output_node.py @@ -15,7 +15,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): "usd", "usdrender", "redshiftproxy", - "staticMesh" + "staticMesh", + "model" ] hosts = ["houdini"] diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py index 55a55bb12a..ce90ae2413 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -42,11 +42,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "RS_outputFileNamePrefix") beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix") - # Store whether we are splitting the render job (export + render) - split_render = bool(rop.parm("RS_archive_enable").eval()) - instance.data["splitRender"] = split_render + export_products = [] - if split_render: + if instance.data["splitRender"]: export_prefix = evalParmNoFrame( rop, "RS_archive_file", pad_character="0" ) @@ -63,9 +61,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2") if full_exr_mode: # Ignore beauty suffix if full mode is enabled - # As this is what the rop does. + # As this is what the rop does. beauty_suffix = "" + # Assume it's a multipartExr Render. + multipartExr = True + # Default beauty/main layer AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=beauty_suffix @@ -75,7 +76,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): beauty_suffix: self.generate_expected_files(instance, beauty_product) } - + aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode() if aovs_rop: rop = aovs_rop @@ -98,13 +99,21 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \ not full_exr_mode: - + aov_product = self.get_render_product_name(aov_prefix, aov_suffix) render_products.append(aov_product) files_by_aov[aov_suffix] = self.generate_expected_files(instance, aov_product) # noqa + # Set to False as soon as we have a separated aov. + multipartExr = False + + # Review Logic expects this key to exist and be True + # if render is a multipart Exr. + # As long as we have one AOV then multipartExr should be True. + instance.data["multipartExr"] = multipartExr + for product in render_products: self.log.debug("Found render product: %s" % product) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_review_data.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_review_data.py index 9671945b9a..ed2de785a2 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_review_data.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_review_data.py @@ -8,7 +8,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): label = "Collect Review Data" # This specific order value is used so that # this plugin runs after CollectRopFrameRange - order = pyblish.api.CollectorOrder + 0.1 + # Also after CollectLocalRenderInstances + order = pyblish.api.CollectorOrder + 0.13 hosts = ["houdini"] families = ["review"] @@ -28,7 +29,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): ropnode_path = instance.data["instance_node"] ropnode = hou.node(ropnode_path) - camera_path = ropnode.parm("camera").eval() + # Get camera based on the instance_node type. + camera_path = self._get_camera_path(ropnode) camera_node = hou.node(camera_path) if not camera_node: self.log.warning("No valid camera node found on review node: " @@ -55,3 +57,29 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): # Store focal length in `burninDataMembers` burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length + + def _get_camera_path(self, ropnode): + """Get the camera path associated with the given rop node. + + This function evaluates the camera parameter according to the + type of the given rop node. + + Returns: + Union[str, None]: Camera path or None. + + This function can return empty string if the camera + path is empty i.e. no camera path. + """ + + if ropnode.type().name() in { + "opengl", "karma", "ifd", "arnold" + }: + return ropnode.parm("camera").eval() + + elif ropnode.type().name() == "Redshift_ROP": + return ropnode.parm("RS_renderCamera").eval() + + elif ropnode.type().name() == "vray_renderer": + return ropnode.parm("render_camera").eval() + + return None diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_reviewable_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_reviewable_instances.py new file mode 100644 index 0000000000..78dc5fe11a --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_reviewable_instances.py @@ -0,0 +1,22 @@ +import pyblish.api + + +class CollectReviewableInstances(pyblish.api.InstancePlugin): + """Collect Reviewable Instances. + + Basically, all instances of the specified families + with creator_attribure["review"] + """ + + order = pyblish.api.CollectorOrder + label = "Collect Reviewable Instances" + families = ["mantra_rop", + "karma_rop", + "redshift_rop", + "arnold_rop", + "vray_rop"] + + def process(self, instance): + creator_attribute = instance.data["creator_attributes"] + + instance.data["review"] = creator_attribute.get("review", False) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_vray_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_vray_rop.py index 62b7dcdd5d..c39b1db103 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -45,12 +45,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): render_products = [] # TODO: add render elements if render element - # Store whether we are splitting the render job in an export + render - split_render = rop.parm("render_export_mode").eval() == "2" - instance.data["splitRender"] = split_render export_prefix = None export_products = [] - if split_render: + if instance.data["splitRender"]: export_prefix = evalParmNoFrame( rop, "render_export_filepath", pad_character="0" ) @@ -70,6 +67,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): "": self.generate_expected_files(instance, beauty_product)} + # Assume it's a multipartExr Render. + multipartExr = True + if instance.data.get("RenderElement", True): render_element = self.get_render_element_name(rop, default_prefix) if render_element: @@ -77,7 +77,13 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(renderpass) files_by_aov[aov] = self.generate_expected_files( instance, renderpass) + # Set to False as soon as we have a separated aov. + multipartExr = False + # Review Logic expects this key to exist and be True + # if render is a multipart Exr. + # As long as we have one AOV then multipartExr should be True. + instance.data["multipartExr"] = multipartExr for product in render_products: self.log.debug("Found render product: %s" % product) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_opengl.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_opengl.py index 57bb8b881a..26a216e335 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_opengl.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_opengl.py @@ -19,6 +19,16 @@ class ExtractOpenGL(publish.Extractor, def process(self, instance): ropnode = hou.node(instance.data.get("instance_node")) + # This plugin is triggered when marking render as reviewable. + # Therefore, this plugin will run on over wrong instances. + # TODO: Don't run this plugin on wrong instances. + # This plugin should run only on review product type + # with instance node of opengl type. + if ropnode.type().name() != "opengl": + self.log.debug("Skipping OpenGl extraction. Rop node {} " + "is not an OpenGl node.".format(ropnode.path())) + return + output = ropnode.evalParm("picture") staging_dir = os.path.normpath(os.path.dirname(output)) instance.data["stagingDir"] = staging_dir diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_render.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_render.py new file mode 100644 index 0000000000..7b4762a25f --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_render.py @@ -0,0 +1,74 @@ +import pyblish.api + +from ayon_core.pipeline import publish +from ayon_core.hosts.houdini.api.lib import render_rop +import hou +import os + + +class ExtractRender(publish.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract Render" + hosts = ["houdini"] + families = ["mantra_rop", + "karma_rop", + "redshift_rop", + "arnold_rop", + "vray_rop"] + + def process(self, instance): + creator_attribute = instance.data["creator_attributes"] + product_type = instance.data["productType"] + rop_node = hou.node(instance.data.get("instance_node")) + + # Align split parameter value on rop node to the render target. + if instance.data["splitRender"]: + if product_type == "arnold_rop": + rop_node.setParms({"ar_ass_export_enable": 1}) + elif product_type == "mantra_rop": + rop_node.setParms({"soho_outputmode": 1}) + elif product_type == "redshift_rop": + rop_node.setParms({"RS_archive_enable": 1}) + elif product_type == "vray_rop": + rop_node.setParms({"render_export_mode": "2"}) + else: + if product_type == "arnold_rop": + rop_node.setParms({"ar_ass_export_enable": 0}) + elif product_type == "mantra_rop": + rop_node.setParms({"soho_outputmode": 0}) + elif product_type == "redshift_rop": + rop_node.setParms({"RS_archive_enable": 0}) + elif product_type == "vray_rop": + rop_node.setParms({"render_export_mode": "1"}) + + if instance.data.get("farm"): + self.log.debug("Render should be processed on farm, skipping local render.") + return + + if creator_attribute.get("render_target") == "local": + ropnode = hou.node(instance.data.get("instance_node")) + render_rop(ropnode) + + # `ExpectedFiles` is a list that includes one dict. + expected_files = instance.data["expectedFiles"][0] + # Each key in that dict is a list of files. + # Combine lists of files into one big list. + all_frames = [] + for value in expected_files.values(): + if isinstance(value, str): + all_frames.append(value) + elif isinstance(value, list): + all_frames.extend(value) + # Check missing frames. + # Frames won't exist if user cancels the render. + missing_frames = [ + frame + for frame in all_frames + if not os.path.exists(frame) + ] + if missing_frames: + # TODO: Use user friendly error reporting. + raise RuntimeError("Failed to complete render extraction. " + "Missing output files: {}".format( + missing_frames)) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/increment_current_file.py b/client/ayon_core/hosts/houdini/plugins/publish/increment_current_file.py index fe8fa25f10..3e9291d5c2 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/increment_current_file.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/increment_current_file.py @@ -17,11 +17,13 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] families = ["workfile", - "redshift_rop", - "arnold_rop", + "usdrender", "mantra_rop", "karma_rop", - "usdrender", + "redshift_rop", + "arnold_rop", + "vray_rop", + "render.local.hou", "publish.hou"] optional = True diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_export_is_a_single_frame.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_export_is_a_single_frame.py new file mode 100644 index 0000000000..b188055bc7 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_export_is_a_single_frame.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""Validator for checking that export is a single frame.""" +import pyblish.api +from ayon_core.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.hosts.houdini.api.action import SelectInvalidAction + + +class ValidateSingleFrame(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate Export is a Single Frame. + + It checks if rop node is exporting one frame. + This is mainly for Model product type. + """ + + families = ["model"] + hosts = ["houdini"] + label = "Validate Single Frame" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + frame_start = instance.data.get("frameStartHandle") + frame_end = instance.data.get("frameEndHandle") + + # This happens if instance node has no 'trange' parameter. + if frame_start is None or frame_end is None: + cls.log.debug( + "No frame data, skipping check.." + ) + return + + if frame_start != frame_end: + invalid.append(instance.data["instance_node"]) + cls.log.error( + "Invalid frame range on '%s'." + "You should use the same frame number for 'f1' " + "and 'f2' parameters.", + instance.data["instance_node"].path() + ) + + return invalid diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 289e00339b..9652367bfe 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -16,9 +16,13 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin, """Validate mesh is static. It checks if output node is time dependent. + this avoids getting different output from ROP node when extracted + from a different frame than the first frame. + (Might be overly restrictive though) """ - families = ["staticMesh"] + families = ["staticMesh", + "model"] hosts = ["houdini"] label = "Validate Mesh is Static" order = ValidateContentsOrder + 0.1 diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py index 38f1c4e176..5e59eb505f 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_mkpaths_toggled.py @@ -7,7 +7,7 @@ class ValidateIntermediateDirectoriesChecked(pyblish.api.InstancePlugin): """Validate Create Intermediate Directories is enabled on ROP node.""" order = pyblish.api.ValidatorOrder - families = ["pointcache", "camera", "vdbcache"] + families = ["pointcache", "camera", "vdbcache", "model"] hosts = ["houdini"] label = "Create Intermediate Directories Checked" diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py index e7f528ba57..fa532c5437 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -56,6 +56,18 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, def process(self, instance): + rop_node = hou.node(instance.data["instance_node"]) + + # This plugin is triggered when marking render as reviewable. + # Therefore, this plugin will run on over wrong instances. + # TODO: Don't run this plugin on wrong instances. + # This plugin should run only on review product type + # with instance node of opengl type. + if rop_node.type().name() != "opengl": + self.log.debug("Skipping Validation. Rop node {} " + "is not an OpenGl node.".format(rop_node.path())) + return + if not self.is_active(instance.data): return @@ -66,7 +78,6 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, ) return - rop_node = hou.node(instance.data["instance_node"]) if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_scene_review.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_scene_review.py index b6007d3f0f..0b09306b0d 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_scene_review.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_scene_review.py @@ -20,6 +20,16 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): report = [] instance_node = hou.node(instance.data.get("instance_node")) + # This plugin is triggered when marking render as reviewable. + # Therefore, this plugin will run on over wrong instances. + # TODO: Don't run this plugin on wrong instances. + # This plugin should run only on review product type + # with instance node of opengl type. + if instance_node.type().name() != "opengl": + self.log.debug("Skipping Validation. Rop node {} " + "is not an OpenGl node.".format(instance_node.path())) + return + invalid = self.get_invalid_scene_path(instance_node) if invalid: report.append(invalid) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_sop_output_node.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_sop_output_node.py index 61cf7596ac..d67192d28e 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -22,7 +22,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["pointcache", "vdbcache"] + families = ["pointcache", "vdbcache", "model"] hosts = ["houdini"] label = "Validate Output Node (SOP)" actions = [SelectROPAction, SelectInvalidAction] diff --git a/client/ayon_core/hosts/houdini/startup/OPmenu.xml b/client/ayon_core/hosts/houdini/startup/OPmenu.xml new file mode 100644 index 0000000000..0a7b265fa1 --- /dev/null +++ b/client/ayon_core/hosts/houdini/startup/OPmenu.xml @@ -0,0 +1,29 @@ + + + + + + + + opmenu.unsynchronize + + opmenu.vhda_create + + + + + + + + + + diff --git a/client/ayon_core/hosts/max/api/lib.py b/client/ayon_core/hosts/max/api/lib.py index d9a3af3336..f20f754248 100644 --- a/client/ayon_core/hosts/max/api/lib.py +++ b/client/ayon_core/hosts/max/api/lib.py @@ -6,12 +6,9 @@ import json from typing import Any, Dict, Union import six -import ayon_api from ayon_core.pipeline import ( get_current_project_name, - get_current_folder_path, - get_current_task_name, colorspace ) from ayon_core.settings import get_project_settings @@ -372,12 +369,8 @@ def reset_colorspace(): """ if int(get_max_version()) < 2024: return - project_name = get_current_project_name() - colorspace_mgr = rt.ColorPipelineMgr - project_settings = get_project_settings(project_name) - max_config_data = colorspace.get_imageio_config( - project_name, "max", project_settings) + max_config_data = colorspace.get_current_context_imageio_config_preset() if max_config_data: ocio_config_path = max_config_data["path"] colorspace_mgr = rt.ColorPipelineMgr @@ -392,10 +385,7 @@ def check_colorspace(): "because Max main window can't be found.") if int(get_max_version()) >= 2024: color_mgr = rt.ColorPipelineMgr - project_name = get_current_project_name() - project_settings = get_project_settings(project_name) - max_config_data = colorspace.get_imageio_config( - project_name, "max", project_settings) + max_config_data = colorspace.get_current_context_imageio_config_preset() if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"): if not is_headless(): from ayon_core.tools.utils import SimplePopup diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index dc13f47795..d9cfc3407f 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -52,11 +52,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._has_been_setup = True - def context_setting(): - return lib.set_context_setting() - - rt.callbacks.addScript(rt.Name('systemPostNew'), - context_setting) + rt.callbacks.addScript(rt.Name('systemPostNew'), on_new) rt.callbacks.addScript(rt.Name('filePostOpen'), lib.check_colorspace) @@ -163,6 +159,14 @@ def ls() -> list: yield lib.read(container) +def on_new(): + lib.set_context_setting() + if rt.checkForSave(): + rt.resetMaxFile(rt.Name("noPrompt")) + rt.clearUndoBuffer() + rt.redrawViews() + + def containerise(name: str, nodes: list, context, namespace=None, loader=None, suffix="_CON"): data = { diff --git a/client/ayon_core/hosts/maya/api/fbx.py b/client/ayon_core/hosts/maya/api/fbx.py index 939da4011b..fd1bf2c901 100644 --- a/client/ayon_core/hosts/maya/api/fbx.py +++ b/client/ayon_core/hosts/maya/api/fbx.py @@ -47,7 +47,7 @@ class FBXExtractor: "smoothMesh": bool, "instances": bool, # "referencedContainersContent": bool, # deprecated in Maya 2016+ - "bakeComplexAnimation": int, + "bakeComplexAnimation": bool, "bakeComplexStart": int, "bakeComplexEnd": int, "bakeComplexStep": int, @@ -59,6 +59,7 @@ class FBXExtractor: "constraints": bool, "lights": bool, "embeddedTextures": bool, + "includeChildren": bool, "inputConnections": bool, "upAxis": str, # x, y or z, "triangulate": bool, @@ -102,6 +103,7 @@ class FBXExtractor: "constraints": False, "lights": True, "embeddedTextures": False, + "includeChildren": True, "inputConnections": True, "upAxis": "y", "triangulate": False, diff --git a/client/ayon_core/hosts/maya/plugins/create/create_animation_pointcache.py b/client/ayon_core/hosts/maya/plugins/create/create_animation_pointcache.py index 08d50a1ab8..069762e4ae 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_animation_pointcache.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_animation_pointcache.py @@ -6,7 +6,6 @@ from ayon_core.lib import ( BoolDef, NumberDef, ) -from ayon_core.pipeline import CreatedInstance def _get_animation_attr_defs(cls): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image.py b/client/ayon_core/hosts/maya/plugins/load/load_image.py index ed2f3f7778..936db86f43 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image.py @@ -8,7 +8,7 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.load.utils import get_representation_path_from_context from ayon_core.pipeline.colorspace import ( get_imageio_file_rules_colorspace_from_filepath, - get_imageio_config, + get_current_context_imageio_config_preset, get_imageio_file_rules ) from ayon_core.settings import get_project_settings @@ -270,8 +270,7 @@ class FileNodeLoader(load.LoaderPlugin): host_name = get_current_host_name() project_settings = get_project_settings(project_name) - config_data = get_imageio_config( - project_name, host_name, + config_data = get_current_context_imageio_config_preset( project_settings=project_settings ) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py index ee66ed2fb7..77b5b79b5f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -35,7 +35,8 @@ class ExtractFBXAnimation(publish.Extractor): fbx_exporter = fbx.FBXExtractor(log=self.log) out_members = instance.data.get("animated_skeleton", []) # Export - instance.data["constraints"] = True + # TODO: need to set up the options for users to set up + # the flags they intended to export instance.data["skeletonDefinitions"] = True instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py deleted file mode 100644 index 2ba2bff6fc..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py +++ /dev/null @@ -1,71 +0,0 @@ -import pyblish.api -import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ( - PublishValidationError, - ValidateContentsOrder, - OptionalPyblishPluginMixin -) -from maya import cmds - - -class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validate all nodes in skeletonAnim_SET are referenced""" - - order = ValidateContentsOrder - hosts = ["maya"] - families = ["animation.fbx"] - label = "Animated Reference Rig" - accepted_controllers = ["transform", "locator"] - actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] - optional = False - - def process(self, instance): - if not self.is_active(instance.data): - return - animated_sets = instance.data.get("animated_skeleton", []) - if not animated_sets: - self.log.debug( - "No nodes found in skeletonAnim_SET. " - "Skipping validation of animated reference rig..." - ) - return - - for animated_reference in animated_sets: - is_referenced = cmds.referenceQuery( - animated_reference, isNodeReferenced=True) - if not bool(is_referenced): - raise PublishValidationError( - "All the content in skeletonAnim_SET" - " should be referenced nodes" - ) - invalid_controls = self.validate_controls(animated_sets) - if invalid_controls: - raise PublishValidationError( - "All the content in skeletonAnim_SET" - " should be transforms" - ) - - @classmethod - def validate_controls(self, set_members): - """Check if the controller set contains only accepted node types. - - Checks if all its set members are within the hierarchy of the root - Checks if the node types of the set members valid - - Args: - set_members: list of nodes of the skeleton_anim_set - hierarchy: list of nodes which reside under the root node - - Returns: - errors (list) - """ - - # Validate control types - invalid = [] - set_members = cmds.ls(set_members, long=True) - for node in set_members: - if cmds.nodeType(node) not in self.accepted_controllers: - invalid.append(node) - - return invalid diff --git a/client/ayon_core/hosts/unreal/lib.py b/client/ayon_core/hosts/unreal/lib.py index 37122b2096..185853a0aa 100644 --- a/client/ayon_core/hosts/unreal/lib.py +++ b/client/ayon_core/hosts/unreal/lib.py @@ -80,17 +80,21 @@ def get_engine_versions(env=None): def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path: """Get UE Editor executable path.""" ue_path = engine_path / "Engine/Binaries" + + ue_name = "UnrealEditor" + + # handle older versions of Unreal Engine + if engine_version.split(".")[0] == "4": + ue_name = "UE4Editor" + if platform.system().lower() == "windows": - if engine_version.split(".")[0] == "4": - ue_path /= "Win64/UE4Editor.exe" - elif engine_version.split(".")[0] == "5": - ue_path /= "Win64/UnrealEditor.exe" + ue_path /= f"Win64/{ue_name}.exe" elif platform.system().lower() == "linux": - ue_path /= "Linux/UE4Editor" + ue_path /= f"Linux/{ue_name}" elif platform.system().lower() == "darwin": - ue_path /= "Mac/UE4Editor" + ue_path /= f"Mac/{ue_name}" return ue_path diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index e436396c6c..e25d3479ee 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -139,6 +139,7 @@ from .path_tools import ( ) from .ayon_info import ( + is_in_ayon_launcher_process, is_running_from_build, is_using_ayon_console, is_staging_enabled, @@ -248,6 +249,7 @@ __all__ = [ "Logger", + "is_in_ayon_launcher_process", "is_running_from_build", "is_using_ayon_console", "is_staging_enabled", diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index fc09a7c90c..c4333fab95 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -1,4 +1,5 @@ import os +import sys import json import datetime import platform @@ -25,6 +26,18 @@ def get_ayon_launcher_version(): return content["__version__"] +def is_in_ayon_launcher_process(): + """Determine if current process is running from AYON launcher. + + Returns: + bool: True if running from AYON launcher. + + """ + ayon_executable_path = os.path.normpath(os.environ["AYON_EXECUTABLE"]) + executable_path = os.path.normpath(sys.executable) + return ayon_executable_path == executable_path + + def is_running_from_build(): """Determine if current process is running from build or code. diff --git a/client/ayon_core/modules/clockify/__init__.py b/client/ayon_core/modules/clockify/__init__.py deleted file mode 100644 index 98834b516c..0000000000 --- a/client/ayon_core/modules/clockify/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .clockify_module import ClockifyModule - -__all__ = ( - "ClockifyModule", -) diff --git a/client/ayon_core/modules/deadline/abstract_submit_deadline.py b/client/ayon_core/modules/deadline/abstract_submit_deadline.py index 00e51100bc..564966b6a0 100644 --- a/client/ayon_core/modules/deadline/abstract_submit_deadline.py +++ b/client/ayon_core/modules/deadline/abstract_submit_deadline.py @@ -29,15 +29,11 @@ from ayon_core.pipeline.publish.lib import ( JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) -# TODO both 'requests_post' and 'requests_get' should not set 'verify' based -# on environment variable. This should be done in a more controlled way, -# e.g. each deadline url could have checkbox to enabled/disable -# ssl verification. 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 server is + Disabling SSL certificate validation if ``verify`` kwarg is set to False. + This is useful when Deadline server is running with self-signed certificates and its certificate is not added to trusted certificates on client machines. @@ -46,10 +42,6 @@ def requests_post(*args, **kwargs): 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 - auth = kwargs.get("auth") if auth: kwargs["auth"] = tuple(auth) # explicit cast to tuple @@ -61,8 +53,8 @@ def 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 server is + Disabling SSL certificate validation if ``verify`` kwarg is set to False. + This is useful when Deadline server is running with self-signed certificates and its certificate is not added to trusted certificates on client machines. @@ -71,9 +63,6 @@ def requests_get(*args, **kwargs): 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 auth = kwargs.get("auth") if auth: kwargs["auth"] = tuple(auth) @@ -466,7 +455,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.aux_files = self.get_aux_files() auth = instance.data["deadline"]["auth"] - job_id = self.process_submission(auth) + verify = instance.data["deadline"]["verify"] + job_id = self.process_submission(auth, verify) self.log.info("Submitted job to Deadline: {}.".format(job_id)) # TODO: Find a way that's more generic and not render type specific @@ -479,10 +469,10 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, job_info=render_job_info, plugin_info=render_plugin_info ) - render_job_id = self.submit(payload, auth) + render_job_id = self.submit(payload, auth, verify) self.log.info("Render job id: %s", render_job_id) - def process_submission(self, auth=None): + def process_submission(self, auth=None, verify=True): """Process data for submission. This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload @@ -493,7 +483,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, """ payload = self.assemble_payload() - return self.submit(payload, auth) + return self.submit(payload, auth, verify) @abstractmethod def get_job_info(self): @@ -583,7 +573,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, "AuxFiles": aux_files or self.aux_files } - def submit(self, payload, auth): + def submit(self, payload, auth, verify): """Submit payload to Deadline API end-point. This takes payload in the form of JSON file and POST it to @@ -592,6 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, Args: payload (dict): dict to become json in deadline submission. auth (tuple): (username, password) + verify (bool): verify SSL certificate if present Returns: str: resulting Deadline job id. @@ -601,8 +592,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, """ url = "{}/api/jobs".format(self._deadline_url) - response = requests_post(url, json=payload, - auth=auth) + response = requests_post( + url, json=payload, auth=auth, verify=verify) if not response.ok: self.log.error("Submission failed!") self.log.error(response.status_code) diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py b/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py index 6923c2b16b..2592d358e5 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_pools.py @@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - hosts = ["aftereffects", - "fusion", - "harmony" - "nuke", - "maya", - "max", - "houdini"] + hosts = [ + "aftereffects", + "fusion", + "harmony", + "maya", + "max", + "houdini", + "nuke", + ] - families = ["render", - "rendering", - "render.farm", - "renderFarm", - "renderlayer", - "maxrender", - "usdrender", - "redshift_rop", - "arnold_rop", - "mantra_rop", - "karma_rop", - "vray_rop", - "publish.hou"] + families = [ + "render", + "prerender", + "rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender", + "usdrender", + "redshift_rop", + "arnold_rop", + "mantra_rop", + "karma_rop", + "vray_rop", + "publish.hou", + ] primary_pool = None secondary_pool = None diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_user_credentials.py b/client/ayon_core/modules/deadline/plugins/publish/collect_user_credentials.py index 8a6d29875a..5eb99be9a3 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_user_credentials.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_user_credentials.py @@ -82,7 +82,10 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): ) instance.data["deadline"]["auth"] = None - if not require_authentication: + instance.data["deadline"]["verify"] = ( + not deadline_info["not_verify_ssl"]) + + if not deadline_info["require_authentication"]: return # TODO import 'get_addon_site_settings' when available # in public 'ayon_api' diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py index f5805beb5c..311dbcedd5 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -174,8 +174,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance.data["toBeRenderedOn"] = "deadline" payload = self.assemble_payload() - return self.submit(payload, - auth=instance.data["deadline"]["auth"]) + auth = instance.data["deadline"]["auth"] + verify = instance.data["deadline"]["verify"] + return self.submit(payload, auth=auth, verify=verify) def from_published_scene(self): """ diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py index 2220442dac..a17bf0c3ef 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -193,9 +193,11 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): self.expected_files(instance, render_path) self.log.debug("__ expectedFiles: `{}`".format( instance.data["expectedFiles"])) - + auth = instance.data["deadline"]["auth"] + verify = instance.data["deadline"]["verify"] response = requests_post(self.deadline_url, json=payload, - auth=instance.data["deadline"]["require_authentication"]) + auth=auth, + verify=verify) if not response.ok: self.log.error( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index e9b93a47cd..6c70119628 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -242,7 +242,8 @@ class FusionSubmitDeadline( # E.g. http://192.168.0.1:8082/api/jobs url = "{}/api/jobs".format(deadline_url) auth = instance.data["deadline"]["auth"] - response = requests_post(url, json=payload, auth=auth) + verify = instance.data["deadline"]["verify"] + response = requests_post(url, json=payload, auth=auth, verify=verify) if not response.ok: raise Exception(response.text) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 597a3cfc55..590abc3f12 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -85,7 +85,7 @@ class HoudiniSubmitDeadline( priority = 50 chunk_size = 1 group = "" - + @classmethod def get_attribute_defs(cls): return [ @@ -188,7 +188,7 @@ class HoudiniSubmitDeadline( job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - + if split_render_job and is_export_job: job_info.Priority = attribute_values.get( "export_priority", self.export_priority @@ -309,6 +309,11 @@ class HoudiniSubmitDeadline( return attr.asdict(plugin_info) def process(self, instance): + if not instance.data["farm"]: + self.log.debug("Render on farm is disabled. " + "Skipping deadline submission.") + return + super(HoudiniSubmitDeadline, self).process(instance) # TODO: Avoid the need for this logic here, needed for submit publish diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py index e9f6c382c5..ababb01285 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_max_deadline.py @@ -181,19 +181,27 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, self.log.debug("Submitting 3dsMax render..") project_settings = instance.context.data["project_settings"] + auth = instance.data["deadline"]["auth"] + verify = instance.data["deadline"]["verify"] if instance.data.get("multiCamera"): self.log.debug("Submitting jobs for multiple cameras..") payload = self._use_published_name_for_multiples( payload_data, project_settings) job_infos, plugin_infos = payload for job_info, plugin_info in zip(job_infos, plugin_infos): - self.submit(self.assemble_payload(job_info, plugin_info), - instance.data["deadline"]["auth"]) + self.submit( + self.assemble_payload(job_info, plugin_info), + auth=auth, + verify=verify + ) else: payload = self._use_published_name(payload_data, project_settings) job_info, plugin_info = payload - self.submit(self.assemble_payload(job_info, plugin_info), - instance.data["deadline"]["auth"]) + self.submit( + self.assemble_payload(job_info, plugin_info), + auth=auth, + verify=verify + ) def _use_published_name(self, data, project_settings): # Not all hosts can import these modules. diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py index 250dc8b7ea..f1bc1cb2be 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -292,7 +292,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload - def process_submission(self, auth=None): + def process_submission(self, auth=None, verify=True): from maya import cmds instance = self._instance @@ -332,8 +332,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if "vrayscene" in instance.data["families"]: self.log.debug("Submitting V-Ray scene render..") vray_export_payload = self._get_vray_export_payload(payload_data) + export_job = self.submit(vray_export_payload, - instance.data["deadline"]["auth"]) + auth=auth, + verify=verify) payload = self._get_vray_render_payload(payload_data) @@ -353,7 +355,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Submit main render job job_info, plugin_info = payload self.submit(self.assemble_payload(job_info, plugin_info), - instance.data["deadline"]["auth"]) + auth=auth, + verify=verify) def _tile_render(self, payload): """Submit as tile render per frame with dependent assembly jobs.""" @@ -557,13 +560,18 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Submit assembly jobs assembly_job_ids = [] num_assemblies = len(assembly_payloads) + auth = instance.data["deadline"]["auth"] + verify = instance.data["deadline"]["verify"] for i, payload in enumerate(assembly_payloads): self.log.debug( "submitting assembly job {} of {}".format(i + 1, num_assemblies) ) - assembly_job_id = self.submit(payload, - instance.data["deadline"]["auth"]) + assembly_job_id = self.submit( + payload, + auth=auth, + verify=verify + ) assembly_job_ids.append(assembly_job_id) instance.data["assemblySubmissionJobs"] = assembly_job_ids diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index ef744ae1e1..db35c2ae67 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -424,8 +424,12 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, self.log.debug("__ expectedFiles: `{}`".format( instance.data["expectedFiles"])) auth = instance.data["deadline"]["auth"] - response = requests_post(self.deadline_url, json=payload, timeout=10, - auth=auth) + verify = instance.data["deadline"]["verify"] + response = requests_post(self.deadline_url, + json=payload, + timeout=10, + auth=auth, + verify=verify) if not response.ok: raise Exception(response.text) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index ce15eda9a0..103f1355da 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -210,8 +210,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, url = "{}/api/jobs".format(self.deadline_url) auth = instance.data["deadline"]["auth"] - response = requests_post(url, json=payload, timeout=10, - auth=auth) + verify = instance.data["deadline"]["verify"] + response = requests_post( + url, json=payload, timeout=10, auth=auth, verify=verify) if not response.ok: raise Exception(response.text) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 06dd62e18b..64313c5c4d 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -304,8 +304,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, url = "{}/api/jobs".format(self.deadline_url) auth = instance.data["deadline"]["auth"] - response = requests_post(url, json=payload, timeout=10, - auth=auth) + verify = instance.data["deadline"]["verify"] + response = requests_post( + url, json=payload, timeout=10, auth=auth, verify=verify) if not response.ok: raise Exception(response.text) @@ -467,8 +468,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, # Inject deadline url to instances to query DL for job id for overrides for inst in instances: - if not "deadline" in inst: - inst["deadline"] = {} inst["deadline"] = instance.data["deadline"] # publish job file diff --git a/client/ayon_core/modules/deadline/plugins/publish/validate_deadline_pools.py b/client/ayon_core/modules/deadline/plugins/publish/validate_deadline_pools.py index 5094b3deaf..2fb511bf51 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/client/ayon_core/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -72,7 +72,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, auth=auth, log=self.log) # some DL return "none" as a pool name - if not "none" in pools: + if "none" not in pools: pools.append("none") self.log.info("Available pools: {}".format(pools)) self.pools_per_url[deadline_url] = pools diff --git a/client/ayon_core/modules/royalrender/api.py b/client/ayon_core/modules/royalrender/api.py index a69f88c43c..ef715811c5 100644 --- a/client/ayon_core/modules/royalrender/api.py +++ b/client/ayon_core/modules/royalrender/api.py @@ -7,7 +7,7 @@ from ayon_core.lib import Logger, run_subprocess, AYONSettingsRegistry from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths from .rr_job import SubmitFile -from .rr_job import RRjob, SubmitterParameter # noqa F401 +from .rr_job import RRJob, SubmitterParameter # noqa F401 class Api: diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index efa3bbf968..099616ff4a 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -8,16 +8,20 @@ import tempfile import warnings from copy import deepcopy +import ayon_api + from ayon_core import AYON_CORE_ROOT from ayon_core.settings import get_project_settings from ayon_core.lib import ( + filter_profiles, StringTemplate, run_ayon_launcher_process, - Logger + Logger, ) -from ayon_core.pipeline import Anatomy from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS - +from ayon_core.pipeline import Anatomy +from ayon_core.pipeline.template_data import get_template_data +from ayon_core.pipeline.load import get_representation_path_with_anatomy log = Logger.get_logger(__name__) @@ -32,10 +36,6 @@ class CachedData: } -class DeprecatedWarning(DeprecationWarning): - pass - - def deprecated(new_destination): """Mark functions as deprecated. @@ -60,13 +60,13 @@ def deprecated(new_destination): @functools.wraps(decorated_func) def wrapper(*args, **kwargs): - warnings.simplefilter("always", DeprecatedWarning) + warnings.simplefilter("always", DeprecationWarning) warnings.warn( ( "Call to deprecated function '{}'" "\nFunction was moved or removed.{}" ).format(decorated_func.__name__, warning_message), - category=DeprecatedWarning, + category=DeprecationWarning, stacklevel=4 ) return decorated_func(*args, **kwargs) @@ -81,28 +81,54 @@ def deprecated(new_destination): def _make_temp_json_file(): """Wrapping function for json temp file """ + temporary_json_file = None try: # Store dumped json to temporary file - temporary_json_file = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( mode="w", suffix=".json", delete=False - ) - temporary_json_file.close() - temporary_json_filepath = temporary_json_file.name.replace( - "\\", "/" - ) + ) as tmpfile: + temporary_json_filepath = tmpfile.name.replace("\\", "/") yield temporary_json_filepath - except IOError as _error: + except IOError as exc: raise IOError( - "Unable to create temp json file: {}".format( - _error - ) + "Unable to create temp json file: {}".format(exc) ) finally: # Remove the temporary json - os.remove(temporary_json_filepath) + if temporary_json_file is not None: + os.remove(temporary_json_filepath) + + +def has_compatible_ocio_package(): + """Current process has available compatible 'PyOpenColorIO'. + + Returns: + bool: True if compatible package is available. + + """ + if CachedData.has_compatible_ocio_package is not None: + return CachedData.has_compatible_ocio_package + + is_compatible = False + try: + import PyOpenColorIO + + # Check if PyOpenColorIO is compatible + # - version 2.0.0 or higher is required + # NOTE version 1 does not have '__version__' attribute + if hasattr(PyOpenColorIO, "__version__"): + version_parts = PyOpenColorIO.__version__.split(".") + major = int(version_parts[0]) + is_compatible = (major, ) >= (2, ) + except ImportError: + pass + + CachedData.has_compatible_ocio_package = is_compatible + # compatible + return CachedData.has_compatible_ocio_package def get_ocio_config_script_path(): @@ -110,53 +136,58 @@ def get_ocio_config_script_path(): Returns: str: path string + """ - return os.path.normpath( - os.path.join( - AYON_CORE_ROOT, - "scripts", - "ocio_wrapper.py" - ) + return os.path.join( + os.path.normpath(AYON_CORE_ROOT), + "scripts", + "ocio_wrapper.py" ) def get_colorspace_name_from_filepath( - filepath, host_name, project_name, - config_data=None, file_rules=None, + filepath, + host_name, + project_name, + config_data, + file_rules=None, project_settings=None, validate=True ): """Get colorspace name from filepath Args: - filepath (str): path string, file rule pattern is tested on it - host_name (str): host name - project_name (str): project name - config_data (Optional[dict]): config path and template in dict. - Defaults to None. - file_rules (Optional[dict]): file rule data from settings. - Defaults to None. - project_settings (Optional[dict]): project settings. Defaults to None. + filepath (str): Path string, file rule pattern is tested on it. + host_name (str): Host name. + project_name (str): Project name. + config_data (dict): Config path and template in dict. + file_rules (Optional[dict]): File rule data from settings. + project_settings (Optional[dict]): Project settings. validate (Optional[bool]): should resulting colorspace be validated - with config file? Defaults to True. + with config file? Defaults to True. Returns: - str: name of colorspace - """ - project_settings, config_data, file_rules = _get_context_settings( - host_name, project_name, - config_data=config_data, file_rules=file_rules, - project_settings=project_settings - ) + Union[str, None]: name of colorspace + """ if not config_data: # in case global or host color management is not enabled return None + if file_rules is None: + if project_settings is None: + project_settings = get_project_settings(project_name) + file_rules = get_imageio_file_rules( + project_name, host_name, project_settings + ) + # use ImageIO file rules colorspace_name = get_imageio_file_rules_colorspace_from_filepath( - filepath, host_name, project_name, - config_data=config_data, file_rules=file_rules, + filepath, + host_name, + project_name, + config_data=config_data, + file_rules=file_rules, project_settings=project_settings ) @@ -182,47 +213,18 @@ def get_colorspace_name_from_filepath( # validate matching colorspace with config if validate: validate_imageio_colorspace_in_config( - config_data["path"], colorspace_name) + config_data["path"], colorspace_name + ) return colorspace_name -# TODO: remove this in future - backward compatibility -@deprecated("get_imageio_file_rules_colorspace_from_filepath") -def get_imageio_colorspace_from_filepath(*args, **kwargs): - return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) - -# TODO: remove this in future - backward compatibility -@deprecated("get_imageio_file_rules_colorspace_from_filepath") -def get_colorspace_from_filepath(*args, **kwargs): - return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) - - -def _get_context_settings( - host_name, project_name, - config_data=None, file_rules=None, - project_settings=None -): - project_settings = project_settings or get_project_settings( - project_name - ) - - config_data = config_data or get_imageio_config( - project_name, host_name, project_settings) - - # in case host color management is not enabled - if not config_data: - return (None, None, None) - - file_rules = file_rules or get_imageio_file_rules( - project_name, host_name, project_settings) - - return project_settings, config_data, file_rules - - def get_imageio_file_rules_colorspace_from_filepath( - filepath, host_name, project_name, - config_data=None, file_rules=None, + filepath, + host_name, + project_name, + config_data, + file_rules=None, project_settings=None ): """Get colorspace name from filepath @@ -230,28 +232,28 @@ def get_imageio_file_rules_colorspace_from_filepath( ImageIO Settings file rules are tested for matching rule. Args: - filepath (str): path string, file rule pattern is tested on it - host_name (str): host name - project_name (str): project name - config_data (Optional[dict]): config path and template in dict. - Defaults to None. - file_rules (Optional[dict]): file rule data from settings. - Defaults to None. - project_settings (Optional[dict]): project settings. Defaults to None. + filepath (str): Path string, file rule pattern is tested on it. + host_name (str): Host name. + project_name (str): Project name. + config_data (dict): Config path and template in dict. + file_rules (Optional[dict]): File rule data from settings. + project_settings (Optional[dict]): Project settings. Returns: - str: name of colorspace - """ - project_settings, config_data, file_rules = _get_context_settings( - host_name, project_name, - config_data=config_data, file_rules=file_rules, - project_settings=project_settings - ) + Union[str, None]: Name of colorspace. + """ if not config_data: # in case global or host color management is not enabled return None + if file_rules is None: + if project_settings is None: + project_settings = get_project_settings(project_name) + file_rules = get_imageio_file_rules( + project_name, host_name, project_settings + ) + # match file rule from path colorspace_name = None for file_rule in file_rules: @@ -282,26 +284,48 @@ def get_config_file_rules_colorspace_from_filepath(config_path, filepath): Returns: Union[str, None]: matching colorspace name + """ - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess + if has_compatible_ocio_package(): + result_data = _get_config_file_rules_colorspace_from_filepath( + config_path, filepath + ) + else: result_data = _get_wrapped_with_subprocess( - "colorspace", "get_config_file_rules_colorspace_from_filepath", + "get_config_file_rules_colorspace_from_filepath", config_path=config_path, filepath=filepath ) - if result_data: - return result_data[0] - - # TODO: refactor this so it is not imported but part of this file - from ayon_core.scripts.ocio_wrapper import _get_config_file_rules_colorspace_from_filepath # noqa: E501 - - result_data = _get_config_file_rules_colorspace_from_filepath( - config_path, filepath) if result_data: return result_data[0] + return None + + +def get_config_version_data(config_path): + """Return major and minor version info. + + Args: + config_path (str): path string leading to config.ocio + + Raises: + IOError: Input config does not exist. + + Returns: + dict: minor and major keys with values + + """ + if config_path not in CachedData.config_version_data: + if has_compatible_ocio_package(): + version_data = _get_config_version_data(config_path) + else: + version_data = _get_wrapped_with_subprocess( + "get_config_version_data", + config_path=config_path + ) + CachedData.config_version_data[config_path] = version_data + + return deepcopy(CachedData.config_version_data[config_path]) def parse_colorspace_from_filepath( @@ -344,10 +368,10 @@ def parse_colorspace_from_filepath( pattern = "|".join( # Allow to match spaces also as underscores because the # integrator replaces spaces with underscores in filenames - re.escape(colorspace) for colorspace in + re.escape(colorspace) # Sort by longest first so the regex matches longer matches # over smaller matches, e.g. matching 'Output - sRGB' over 'sRGB' - sorted(colorspaces, key=len, reverse=True) + for colorspace in sorted(colorspaces, key=len, reverse=True) ) return re.compile(pattern) @@ -395,6 +419,7 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): Returns: bool: True if exists + """ colorspaces = get_ocio_config_colorspaces(config_path)["colorspaces"] if colorspace_name not in colorspaces: @@ -405,28 +430,10 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): return True -# TODO: remove this in future - backward compatibility -@deprecated("_get_wrapped_with_subprocess") -def get_data_subprocess(config_path, data_type): - """[Deprecated] Get data via subprocess - - Wrapper for Python 2 hosts. +def _get_wrapped_with_subprocess(command, **kwargs): + """Get data via subprocess. Args: - config_path (str): path leading to config.ocio file - """ - return _get_wrapped_with_subprocess( - "config", data_type, in_path=config_path, - ) - - -def _get_wrapped_with_subprocess(command_group, command, **kwargs): - """Get data via subprocess - - Wrapper for Python 2 hosts. - - Args: - command_group (str): command group name command (str): command name **kwargs: command arguments @@ -436,14 +443,15 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): with _make_temp_json_file() as tmp_json_path: # Prepare subprocess arguments args = [ - "run", get_ocio_config_script_path(), - command_group, command + "run", + get_ocio_config_script_path(), + command ] - for key_, value_ in kwargs.items(): - args.extend(("--{}".format(key_), value_)) + for key, value in kwargs.items(): + args.extend(("--{}".format(key), value)) - args.append("--out_path") + args.append("--output_path") args.append(tmp_json_path) log.info("Executing: {}".format(" ".join(args))) @@ -451,55 +459,23 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): run_ayon_launcher_process(*args, logger=log) # return all colorspaces - with open(tmp_json_path, "r") as f_: - return json.load(f_) + with open(tmp_json_path, "r") as stream: + return json.load(stream) -# TODO: this should be part of ocio_wrapper.py -def compatibility_check(): - """Making sure PyOpenColorIO is importable""" - if CachedData.has_compatible_ocio_package is not None: - return CachedData.has_compatible_ocio_package - - try: - import PyOpenColorIO # noqa: F401 - CachedData.has_compatible_ocio_package = True - except ImportError: - CachedData.has_compatible_ocio_package = False - - # compatible - return CachedData.has_compatible_ocio_package - - -# TODO: this should be part of ocio_wrapper.py def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" - if not CachedData.config_version_data.get(config_path): - if compatibility_check(): - # TODO: refactor this so it is not imported but part of this file - from ayon_core.scripts.ocio_wrapper import _get_version_data - - CachedData.config_version_data[config_path] = \ - _get_version_data(config_path) - - else: - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - CachedData.config_version_data[config_path] = \ - _get_wrapped_with_subprocess( - "config", "get_version", config_path=config_path - ) + version_data = get_config_version_data(config_path) # check major version - if CachedData.config_version_data[config_path]["major"] != major: + if version_data["major"] != major: return False # check minor version - if minor and CachedData.config_version_data[config_path]["minor"] != minor: + if minor is not None and version_data["minor"] != minor: return False - # compatible return True @@ -514,23 +490,19 @@ def get_ocio_config_colorspaces(config_path): Returns: dict: colorspace and family in couple + """ - if not CachedData.ocio_config_colorspaces.get(config_path): - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - CachedData.ocio_config_colorspaces[config_path] = \ - _get_wrapped_with_subprocess( - "config", "get_colorspace", in_path=config_path - ) + if config_path not in CachedData.ocio_config_colorspaces: + if has_compatible_ocio_package(): + config_colorspaces = _get_ocio_config_colorspaces(config_path) else: - # TODO: refactor this so it is not imported but part of this file - from ayon_core.scripts.ocio_wrapper import _get_colorspace_data + config_colorspaces = _get_wrapped_with_subprocess( + "get_ocio_config_colorspaces", + config_path=config_path + ) + CachedData.ocio_config_colorspaces[config_path] = config_colorspaces - CachedData.ocio_config_colorspaces[config_path] = \ - _get_colorspace_data(config_path) - - return CachedData.ocio_config_colorspaces[config_path] + return deepcopy(CachedData.ocio_config_colorspaces[config_path]) def convert_colorspace_enumerator_item( @@ -540,11 +512,12 @@ def convert_colorspace_enumerator_item( """Convert colorspace enumerator item to dictionary Args: - colorspace_item (str): colorspace and family in couple - config_items (dict[str,dict]): colorspace data + colorspace_enum_item (str): Colorspace and family in couple. + config_items (dict[str,dict]): Colorspace data. Returns: dict: colorspace data + """ if "::" not in colorspace_enum_item: return None @@ -603,16 +576,18 @@ def get_colorspaces_enumerator_items( Families can be used for building menu and submenus in gui. Args: - config_items (dict[str,dict]): colorspace data coming from - `get_ocio_config_colorspaces` function - include_aliases (bool): include aliases in result - include_looks (bool): include looks in result - include_roles (bool): include roles in result + config_items (dict[str,dict]): Colorspace data coming from + `get_ocio_config_colorspaces` function. + include_aliases (Optional[bool]): Include aliases in result. + include_looks (Optional[bool]): Include looks in result. + include_roles (Optional[bool]): Include roles in result. + include_display_views (Optional[bool]): Include display views + in result. Returns: - list[tuple[str,str]]: colorspace and family in couple + list[tuple[str, str]]: Colorspace and family in couples. + """ - labeled_colorspaces = [] aliases = set() colorspaces = set() looks = set() @@ -622,86 +597,86 @@ def get_colorspaces_enumerator_items( if items_type == "colorspaces": for color_name, color_data in colorspace_items.items(): if color_data.get("aliases"): - aliases.update([ + aliases.update({ ( "aliases::{}".format(alias_name), "[alias] {} ({})".format(alias_name, color_name) ) for alias_name in color_data["aliases"] - ]) + }) colorspaces.add(( "{}::{}".format(items_type, color_name), "[colorspace] {}".format(color_name) )) elif items_type == "looks": - looks.update([ + looks.update({ ( "{}::{}".format(items_type, name), "[look] {} ({})".format(name, role_data["process_space"]) ) for name, role_data in colorspace_items.items() - ]) + }) elif items_type == "displays_views": - display_views.update([ + display_views.update({ ( "{}::{}".format(items_type, name), "[view (display)] {}".format(name) ) for name, _ in colorspace_items.items() - ]) + }) elif items_type == "roles": - roles.update([ + roles.update({ ( "{}::{}".format(items_type, name), "[role] {} ({})".format(name, role_data["colorspace"]) ) for name, role_data in colorspace_items.items() - ]) + }) - if roles and include_roles: - roles = sorted(roles, key=lambda x: x[0]) - labeled_colorspaces.extend(roles) + def _sort_key_getter(item): + """Use colorspace for sorting. - # add colorspaces as second so it is not first in menu - colorspaces = sorted(colorspaces, key=lambda x: x[0]) - labeled_colorspaces.extend(colorspaces) + Args: + item (tuple[str, str]): Item with colorspace and label. - if aliases and include_aliases: - aliases = sorted(aliases, key=lambda x: x[0]) - labeled_colorspaces.extend(aliases) + Returns: + str: Colorspace. - if looks and include_looks: - looks = sorted(looks, key=lambda x: x[0]) - labeled_colorspaces.extend(looks) + """ + return item[0] - if display_views and include_display_views: - display_views = sorted(display_views, key=lambda x: x[0]) - labeled_colorspaces.extend(display_views) + labeled_colorspaces = [] + if include_roles: + labeled_colorspaces.extend( + sorted(roles, key=_sort_key_getter) + ) + + # Add colorspaces after roles, so it is not first in menu + labeled_colorspaces.extend( + sorted(colorspaces, key=_sort_key_getter) + ) + + if include_aliases: + labeled_colorspaces.extend( + sorted(aliases, key=_sort_key_getter) + ) + + if include_looks: + labeled_colorspaces.extend( + sorted(looks, key=_sort_key_getter) + ) + + if include_display_views: + labeled_colorspaces.extend( + sorted(display_views, key=_sort_key_getter) + ) return labeled_colorspaces -# TODO: remove this in future - backward compatibility -@deprecated("_get_wrapped_with_subprocess") -def get_colorspace_data_subprocess(config_path): - """[Deprecated] Get colorspace data via subprocess - - Wrapper for Python 2 hosts. - - Args: - config_path (str): path leading to config.ocio file - - Returns: - dict: colorspace and family in couple - """ - return _get_wrapped_with_subprocess( - "config", "get_colorspace", in_path=config_path - ) - - def get_ocio_config_views(config_path): """Get all viewer data @@ -713,212 +688,346 @@ def get_ocio_config_views(config_path): Returns: dict: `display/viewer` and viewer data + """ - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - return _get_wrapped_with_subprocess( - "config", "get_views", in_path=config_path - ) + if has_compatible_ocio_package(): + return _get_ocio_config_views(config_path) - # TODO: refactor this so it is not imported but part of this file - from ayon_core.scripts.ocio_wrapper import _get_views_data - - return _get_views_data(config_path) - - -# TODO: remove this in future - backward compatibility -@deprecated("_get_wrapped_with_subprocess") -def get_views_data_subprocess(config_path): - """[Deprecated] Get viewers data via subprocess - - Wrapper for Python 2 hosts. - - Args: - config_path (str): path leading to config.ocio file - - Returns: - dict: `display/viewer` and viewer data - """ return _get_wrapped_with_subprocess( - "config", "get_views", in_path=config_path + "get_ocio_config_views", + config_path=config_path ) -def get_imageio_config( +def _get_global_config_data( project_name, host_name, - project_settings=None, - anatomy_data=None, + anatomy, + template_data, + imageio_global, + folder_id, + log, +): + """Get global config data. + + Global config from core settings is using profiles that are based on + host name, task name and task type. The filtered profile can define 3 + types of config sources: + 1. AYON ocio addon configs. + 2. Custom path to ocio config. + 3. Path to 'ocioconfig' representation on product. Name of product can be + defined in settings. Product name can be regex but exact match is + always preferred. + + None is returned when no profile is found, when path + + Args: + project_name (str): Project name. + host_name (str): Host name. + anatomy (Anatomy): Project anatomy object. + template_data (dict[str, Any]): Template data. + imageio_global (dict[str, Any]): Core imagio settings. + folder_id (Union[dict[str, Any], None]): Folder id. + log (logging.Logger): Logger object. + + Returns: + Union[dict[str, str], None]: Config data with path and template + or None. + + """ + task_name = task_type = None + task_data = template_data.get("task") + if task_data: + task_name = task_data["name"] + task_type = task_data["type"] + + filter_values = { + "task_names": task_name, + "task_types": task_type, + "host_names": host_name, + } + profile = filter_profiles( + imageio_global["ocio_config_profiles"], filter_values + ) + if profile is None: + log.info(f"No config profile matched filters {str(filter_values)}") + return None + + profile_type = profile["type"] + if profile_type in ("builtin_path", "custom_path"): + template = profile[profile_type] + result = StringTemplate.format_strict_template( + template, template_data + ) + normalized_path = str(result.normalized()) + if not os.path.exists(normalized_path): + log.warning(f"Path was not found '{normalized_path}'.") + return None + + return { + "path": normalized_path, + "template": template + } + + # TODO decide if this is the right name for representation + repre_name = "ocioconfig" + + folder_info = template_data.get("folder") + if not folder_info: + log.warning("Folder info is missing.") + return None + folder_path = folder_info["path"] + + product_name = profile["product_name"] + if folder_id is None: + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} + ) + if not folder_entity: + log.warning(f"Folder entity '{folder_path}' was not found..") + return None + folder_id = folder_entity["id"] + + product_entities_by_name = { + product_entity["name"]: product_entity + for product_entity in ayon_api.get_products( + project_name, + folder_ids={folder_id}, + product_name_regex=product_name, + fields={"id", "name"} + ) + } + if not product_entities_by_name: + log.debug( + f"No product entities were found for folder '{folder_path}' with" + f" product name filter '{product_name}'." + ) + return None + + # Try to use exact match first, otherwise use first available product + product_entity = product_entities_by_name.get(product_name) + if product_entity is None: + product_entity = next(iter(product_entities_by_name.values())) + + product_name = product_entity["name"] + # Find last product version + version_entity = ayon_api.get_last_version_by_product_id( + project_name, + product_id=product_entity["id"], + fields={"id"} + ) + if not version_entity: + log.info( + f"Product '{product_name}' does not have available any versions." + ) + return None + + # Find 'ocioconfig' representation entity + repre_entity = ayon_api.get_representation_by_name( + project_name, + representation_name=repre_name, + version_id=version_entity["id"], + ) + if not repre_entity: + log.debug( + f"Representation '{repre_name}'" + f" not found on product '{product_name}'." + ) + return None + + path = get_representation_path_with_anatomy(repre_entity, anatomy) + template = repre_entity["attrib"]["template"] + return { + "path": path, + "template": template, + } + + +def get_imageio_config_preset( + project_name, + folder_path, + task_name, + host_name, anatomy=None, - env=None + project_settings=None, + template_data=None, + env=None, + folder_id=None, ): """Returns config data from settings - Config path is formatted in `path` key - and original settings input is saved into `template` key. + Output contains 'path' key and 'template' key holds its template. + + Template data can be prepared with 'get_template_data'. Args: - project_name (str): project name - host_name (str): host name + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + host_name (str): Host name. + anatomy (Optional[Anatomy]): Project anatomy object. project_settings (Optional[dict]): Project settings. - anatomy_data (Optional[dict]): anatomy formatting data. - anatomy (Optional[Anatomy]): Anatomy object. - env (Optional[dict]): Environment variables. + template_data (Optional[dict]): Template data used for + template formatting. + env (Optional[dict]): Environment variables. Environments are used + for template formatting too. Values from 'os.environ' are used + when not provided. + folder_id (Optional[str]): Folder id. Is used only when config path + is received from published representation. Is autofilled when + not provided. Returns: dict: config path data or empty dict + """ - project_settings = project_settings or get_project_settings(project_name) - anatomy = anatomy or Anatomy(project_name) - - if not anatomy_data: - from ayon_core.pipeline.context_tools import ( - get_current_context_template_data) - anatomy_data = get_current_context_template_data() - - formatting_data = deepcopy(anatomy_data) - - # Add project roots to anatomy data - formatting_data["root"] = anatomy.roots - formatting_data["platform"] = platform.system().lower() + if not project_settings: + project_settings = get_project_settings(project_name) # Get colorspace settings imageio_global, imageio_host = _get_imageio_settings( - project_settings, host_name) + project_settings, host_name + ) + # Global color management must be enabled to be able to use host settings + if not imageio_global["activate_global_color_management"]: + log.info("Colorspace management is disabled globally.") + return {} # Host 'ocio_config' is optional host_ocio_config = imageio_host.get("ocio_config") or {} - - # Global color management must be enabled to be able to use host settings - activate_color_management = imageio_global.get( - "activate_global_color_management") - # TODO: remove this in future - backward compatibility - # For already saved overrides from previous version look for 'enabled' - # on host settings. - if activate_color_management is None: - activate_color_management = host_ocio_config.get("enabled", False) - - if not activate_color_management: - # if global settings are disabled return empty dict because - # it is expected that no colorspace management is needed - log.info("Colorspace management is disabled globally.") - return {} + # TODO remove + # - backward compatibility when host settings had only 'enabled' flag + # the flag was split into 'activate_global_color_management' + # and 'override_global_config' + host_ocio_config_enabled = host_ocio_config.get("enabled", False) # Check if host settings group is having 'activate_host_color_management' # - if it does not have activation key then default it to True so it uses # global settings - # This is for backward compatibility. - # TODO: in future rewrite this to be more explicit activate_host_color_management = imageio_host.get( - "activate_host_color_management") - - # TODO: remove this in future - backward compatibility + "activate_host_color_management" + ) if activate_host_color_management is None: - activate_host_color_management = host_ocio_config.get("enabled", False) + activate_host_color_management = host_ocio_config_enabled if not activate_host_color_management: # if host settings are disabled return False because # it is expected that no colorspace management is needed log.info( - "Colorspace management for host '{}' is disabled.".format( - host_name) + f"Colorspace management for host '{host_name}' is disabled." ) return {} - # get config path from either global or host settings - # depending on override flag + project_entity = None + if anatomy is None: + project_entity = ayon_api.get_project(project_name) + anatomy = Anatomy(project_name, project_entity=project_entity) + + if env is None: + env = dict(os.environ.items()) + + if template_data: + template_data = deepcopy(template_data) + else: + if not project_entity: + project_entity = ayon_api.get_project(project_name) + + folder_entity = task_entity = folder_id = None + if folder_path: + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + folder_id = folder_entity["id"] + + if folder_id and task_name: + task_entity = ayon_api.get_task_by_name( + project_name, folder_id, task_name + ) + template_data = get_template_data( + project_entity, + folder_entity, + task_entity, + host_name, + project_settings, + ) + + # Add project roots to anatomy data + template_data["root"] = anatomy.roots + template_data["platform"] = platform.system().lower() + + # Add environment variables to template data + template_data.update(env) + + # Get config path from core or host settings + # - based on override flag in host settings # TODO: in future rewrite this to be more explicit override_global_config = host_ocio_config.get("override_global_config") if override_global_config is None: - # for already saved overrides from previous version - # TODO: remove this in future - backward compatibility - override_global_config = host_ocio_config.get("enabled") + override_global_config = host_ocio_config_enabled - if override_global_config: - config_data = _get_config_data( - host_ocio_config["filepath"], formatting_data, env + if not override_global_config: + config_data = _get_global_config_data( + project_name, + host_name, + anatomy, + template_data, + imageio_global, + folder_id, + log, ) else: - # get config path from global - config_global = imageio_global["ocio_config"] - config_data = _get_config_data( - config_global["filepath"], formatting_data, env + config_data = _get_host_config_data( + host_ocio_config["filepath"], template_data ) if not config_data: raise FileExistsError( - "No OCIO config found in settings. It is " - "either missing or there is typo in path inputs" + "No OCIO config found in settings. It is" + " either missing or there is typo in path inputs" ) return config_data -def _get_config_data(path_list, anatomy_data, env=None): +def _get_host_config_data(templates, template_data): """Return first existing path in path list. - If template is used in path inputs, - then it is formatted by anatomy data - and environment variables + Use template data to fill possible formatting in paths. Args: - path_list (list[str]): list of abs paths - anatomy_data (dict): formatting data - env (Optional[dict]): Environment variables. + templates (list[str]): List of templates to config paths. + template_data (dict): Template data used to format templates. Returns: - dict: config data + Union[dict, None]: Config data or 'None' if templates are empty + or any path exists. + """ - formatting_data = deepcopy(anatomy_data) - - environment_vars = env or dict(**os.environ) - - # format the path for potential env vars - formatting_data.update(environment_vars) - - # first try host config paths - for path_ in path_list: - formatted_path = _format_path(path_, formatting_data) - - if not os.path.exists(formatted_path): + for template in templates: + formatted_path = StringTemplate.format_template( + template, template_data + ) + if not formatted_path.solved: continue - return { - "path": os.path.normpath(formatted_path), - "template": path_ - } - - -def _format_path(template_path, formatting_data): - """Single template path formatting. - - Args: - template_path (str): template string - formatting_data (dict): data to be used for - template formatting - - Returns: - str: absolute formatted path - """ - # format path for anatomy keys - formatted_path = StringTemplate(template_path).format( - formatting_data) - - return os.path.abspath(formatted_path) + path = os.path.abspath(formatted_path) + if os.path.exists(path): + return { + "path": os.path.normpath(path), + "template": template + } def get_imageio_file_rules(project_name, host_name, project_settings=None): """Get ImageIO File rules from project settings Args: - project_name (str): project name - host_name (str): host name - project_settings (dict, optional): project settings. - Defaults to None. + project_name (str): Project name. + host_name (str): Host name. + project_settings (Optional[dict]): Project settings. Returns: list[dict[str, Any]]: file rules data + """ project_settings = project_settings or get_project_settings(project_name) @@ -960,7 +1069,7 @@ def get_remapped_colorspace_to_native( """Return native colorspace name. Args: - ocio_colorspace_name (str | None): ocio colorspace name + ocio_colorspace_name (str | None): OCIO colorspace name. host_name (str): Host name. imageio_host_settings (dict[str, Any]): ImageIO host settings. @@ -968,16 +1077,15 @@ def get_remapped_colorspace_to_native( Union[str, None]: native colorspace name defined in remapping or None """ - CachedData.remapping.setdefault(host_name, {}) - if CachedData.remapping[host_name].get("to_native") is None: + host_mapping = CachedData.remapping.setdefault(host_name, {}) + if "to_native" not in host_mapping: remapping_rules = imageio_host_settings["remapping"]["rules"] - CachedData.remapping[host_name]["to_native"] = { + host_mapping["to_native"] = { rule["ocio_name"]: rule["host_native_name"] for rule in remapping_rules } - return CachedData.remapping[host_name]["to_native"].get( - ocio_colorspace_name) + return host_mapping["to_native"].get(ocio_colorspace_name) def get_remapped_colorspace_from_native( @@ -992,30 +1100,29 @@ def get_remapped_colorspace_from_native( Returns: Union[str, None]: Ocio colorspace name defined in remapping or None. - """ - CachedData.remapping.setdefault(host_name, {}) - if CachedData.remapping[host_name].get("from_native") is None: + """ + host_mapping = CachedData.remapping.setdefault(host_name, {}) + if "from_native" not in host_mapping: remapping_rules = imageio_host_settings["remapping"]["rules"] - CachedData.remapping[host_name]["from_native"] = { + host_mapping["from_native"] = { rule["host_native_name"]: rule["ocio_name"] for rule in remapping_rules } - return CachedData.remapping[host_name]["from_native"].get( - host_native_colorspace_name) + return host_mapping["from_native"].get(host_native_colorspace_name) def _get_imageio_settings(project_settings, host_name): """Get ImageIO settings for global and host Args: - project_settings (dict): project settings. - Defaults to None. - host_name (str): host name + project_settings (dict[str, Any]): Project settings. + host_name (str): Host name. Returns: - tuple[dict, dict]: image io settings for global and host + tuple[dict, dict]: Image io settings for global and host. + """ # get image io from global and host_name imageio_global = project_settings["core"]["imageio"] @@ -1033,27 +1140,41 @@ def get_colorspace_settings_from_publish_context(context_data): Returns: tuple | bool: config, file rules or None + """ if "imageioSettings" in context_data and context_data["imageioSettings"]: return context_data["imageioSettings"] project_name = context_data["projectName"] + folder_path = context_data["folderPath"] + task_name = context_data["task"] host_name = context_data["hostName"] - anatomy_data = context_data["anatomyData"] - project_settings_ = context_data["project_settings"] + anatomy = context_data["anatomy"] + template_data = context_data["anatomyData"] + project_settings = context_data["project_settings"] + folder_id = None + folder_entity = context_data.get("folderEntity") + if folder_entity: + folder_id = folder_entity["id"] - config_data = get_imageio_config( - project_name, host_name, - project_settings=project_settings_, - anatomy_data=anatomy_data + config_data = get_imageio_config_preset( + project_name, + folder_path, + task_name, + host_name, + anatomy=anatomy, + project_settings=project_settings, + template_data=template_data, + folder_id=folder_id, ) # caching invalid state, so it's not recalculated all the time file_rules = None if config_data: file_rules = get_imageio_file_rules( - project_name, host_name, - project_settings=project_settings_ + project_name, + host_name, + project_settings=project_settings ) # caching settings for future instance processing @@ -1063,18 +1184,13 @@ def get_colorspace_settings_from_publish_context(context_data): def set_colorspace_data_to_representation( - representation, context_data, + representation, + context_data, colorspace=None, log=None ): """Sets colorspace data to representation. - Args: - representation (dict): publishing representation - context_data (publish.Context.data): publishing context data - colorspace (str, optional): colorspace name. Defaults to None. - log (logging.Logger, optional): logger instance. Defaults to None. - Example: ``` { @@ -1089,6 +1205,12 @@ def set_colorspace_data_to_representation( } ``` + Args: + representation (dict): publishing representation + context_data (publish.Context.data): publishing context data + colorspace (Optional[str]): Colorspace name. + log (Optional[logging.Logger]): logger instance. + """ log = log or Logger.get_logger(__name__) @@ -1122,12 +1244,15 @@ def set_colorspace_data_to_representation( filename = filename[0] # get matching colorspace from rules - colorspace = colorspace or get_imageio_colorspace_from_filepath( - filename, host_name, project_name, - config_data=config_data, - file_rules=file_rules, - project_settings=project_settings - ) + if colorspace is None: + colorspace = get_imageio_file_rules_colorspace_from_filepath( + filename, + host_name, + project_name, + config_data=config_data, + file_rules=file_rules, + project_settings=project_settings + ) # infuse data to representation if colorspace: @@ -1149,47 +1274,330 @@ def get_display_view_colorspace_name(config_path, display, view): view (str): view name e.g. "sRGB" Returns: - view color space name (str) e.g. "Output - sRGB" + str: View color space name. e.g. "Output - sRGB" + """ - - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - return get_display_view_colorspace_subprocess(config_path, - display, view) - - from ayon_core.scripts.ocio_wrapper import _get_display_view_colorspace_name # noqa - - return _get_display_view_colorspace_name(config_path, display, view) + if has_compatible_ocio_package(): + return _get_display_view_colorspace_name( + config_path, display, view + ) + return _get_wrapped_with_subprocess( + "get_display_view_colorspace_name", + config_path=config_path, + display=display, + view=view + ) -def get_display_view_colorspace_subprocess(config_path, display, view): - """Returns the colorspace attribute of the (display, view) pair - via subprocess. +# --- Implementation of logic using 'PyOpenColorIO' --- +def _get_ocio_config(config_path): + """Helper function to create OCIO config object. + + Args: + config_path (str): Path to config. + + Returns: + PyOpenColorIO.Config: OCIO config for the confing path. + + """ + import PyOpenColorIO + + config_path = os.path.abspath(config_path) + + if not os.path.isfile(config_path): + raise IOError("Input path should be `config.ocio` file") + + return PyOpenColorIO.Config.CreateFromFile(config_path) + + +def _get_config_file_rules_colorspace_from_filepath(config_path, filepath): + """Return found colorspace data found in v2 file rules. + + Args: + config_path (str): path string leading to config.ocio + filepath (str): path string leading to v2 file rules + + Raises: + IOError: Input config does not exist. + + Returns: + dict: aggregated available colorspaces + + """ + config = _get_ocio_config(config_path) + + # TODO: use `parseColorSpaceFromString` instead if ocio v1 + return config.getColorSpaceFromFilepath(str(filepath)) + + +def _get_config_version_data(config_path): + """Return major and minor version info. + + Args: + config_path (str): path string leading to config.ocio + + Raises: + IOError: Input config does not exist. + + Returns: + dict: minor and major keys with values + + """ + config = _get_ocio_config(config_path) + + return { + "major": config.getMajorVersion(), + "minor": config.getMinorVersion() + } + + +def _get_display_view_colorspace_name(config_path, display, view): + """Returns the colorspace attribute of the (display, view) pair. Args: config_path (str): path string leading to config.ocio display (str): display name e.g. "ACES" view (str): view name e.g. "sRGB" + Raises: + IOError: Input config does not exist. + Returns: - view color space name (str) e.g. "Output - sRGB" + str: view color space name e.g. "Output - sRGB" + + """ + config = _get_ocio_config(config_path) + return config.getDisplayViewColorSpaceName(display, view) + + +def _get_ocio_config_colorspaces(config_path): + """Return all found colorspace data. + + Args: + config_path (str): path string leading to config.ocio + + Raises: + IOError: Input config does not exist. + + Returns: + dict: aggregated available colorspaces + + """ + config = _get_ocio_config(config_path) + + colorspace_data = { + "roles": {}, + "colorspaces": { + color.getName(): { + "family": color.getFamily(), + "categories": list(color.getCategories()), + "aliases": list(color.getAliases()), + "equalitygroup": color.getEqualityGroup(), + } + for color in config.getColorSpaces() + }, + "displays_views": { + f"{view} ({display})": { + "display": display, + "view": view + + } + for display in config.getDisplays() + for view in config.getViews(display) + }, + "looks": {} + } + + # add looks + looks = config.getLooks() + if looks: + colorspace_data["looks"] = { + look.getName(): {"process_space": look.getProcessSpace()} + for look in looks + } + + # add roles + roles = config.getRoles() + if roles: + colorspace_data["roles"] = { + role: {"colorspace": colorspace} + for (role, colorspace) in roles + } + + return colorspace_data + + +def _get_ocio_config_views(config_path): + """Return all found viewer data. + + Args: + config_path (str): path string leading to config.ocio + + Raises: + IOError: Input config does not exist. + + Returns: + dict: aggregated available viewers + + """ + config = _get_ocio_config(config_path) + + output = {} + for display in config.getDisplays(): + for view in config.getViews(display): + colorspace = config.getDisplayViewColorSpaceName(display, view) + # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa + if colorspace == "": + colorspace = display + + output[f"{display}/{view}"] = { + "display": display, + "view": view, + "colorspace": colorspace + } + + return output + + +# --- Current context functions --- +def get_current_context_imageio_config_preset( + anatomy=None, + project_settings=None, + template_data=None, + env=None, +): + """Get ImageIO config preset for current context. + + Args: + anatomy (Optional[Anatomy]): Current project anatomy. + project_settings (Optional[dict[str, Any]]): Current project settings. + template_data (Optional[dict[str, Any]]): Prepared template data + for current context. + env (Optional[dict[str, str]]): Custom environment variable values. + + Returns: + dict: ImageIO config preset. + + """ + from .context_tools import get_current_context, get_current_host_name + + context = get_current_context() + host_name = get_current_host_name() + return get_imageio_config_preset( + context["project_name"], + context["folder_path"], + context["task_name"], + host_name, + anatomy=anatomy, + project_settings=project_settings, + template_data=template_data, + env=env, + ) + + +# --- Deprecated functions --- +@deprecated("has_compatible_ocio_package") +def compatibility_check(): + """Making sure PyOpenColorIO is importable + + Deprecated: + Deprecated since '0.3.2'. Use `has_compatible_ocio_package` instead. """ - with _make_temp_json_file() as tmp_json_path: - # Prepare subprocess arguments - args = [ - "run", get_ocio_config_script_path(), - "config", "get_display_view_colorspace_name", - "--in_path", config_path, - "--out_path", tmp_json_path, - "--display", display, - "--view", view - ] - log.debug("Executing: {}".format(" ".join(args))) + return has_compatible_ocio_package() - run_ayon_launcher_process(*args, logger=log) - # return default view colorspace name - with open(tmp_json_path, "r") as f: - return json.load(f) +@deprecated("get_imageio_file_rules_colorspace_from_filepath") +def get_imageio_colorspace_from_filepath(*args, **kwargs): + return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) + + +@deprecated("get_imageio_file_rules_colorspace_from_filepath") +def get_colorspace_from_filepath(*args, **kwargs): + return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) + + +@deprecated("_get_wrapped_with_subprocess") +def get_colorspace_data_subprocess(config_path): + """[Deprecated] Get colorspace data via subprocess + + Deprecated: + Deprecated since OpenPype. Use `_get_wrapped_with_subprocess` instead. + + Args: + config_path (str): path leading to config.ocio file + + Returns: + dict: colorspace and family in couple + """ + return _get_wrapped_with_subprocess( + "get_ocio_config_colorspaces", + config_path=config_path + ) + + +@deprecated("_get_wrapped_with_subprocess") +def get_views_data_subprocess(config_path): + """[Deprecated] Get viewers data via subprocess + + Deprecated: + Deprecated since OpenPype. Use `_get_wrapped_with_subprocess` instead. + + Args: + config_path (str): path leading to config.ocio file + + Returns: + dict: `display/viewer` and viewer data + + """ + return _get_wrapped_with_subprocess( + "get_ocio_config_views", + config_path=config_path + ) + + +@deprecated("get_imageio_config_preset") +def get_imageio_config( + project_name, + host_name, + project_settings=None, + anatomy_data=None, + anatomy=None, + env=None +): + """Returns config data from settings + + Config path is formatted in `path` key + and original settings input is saved into `template` key. + + Deprecated: + Deprecated since '0.3.1' . Use `get_imageio_config_preset` instead. + + Args: + project_name (str): project name + host_name (str): host name + project_settings (Optional[dict]): Project settings. + anatomy_data (Optional[dict]): anatomy formatting data. + anatomy (Optional[Anatomy]): Anatomy object. + env (Optional[dict]): Environment variables. + + Returns: + dict: config path data or empty dict + + """ + if not anatomy_data: + from .context_tools import get_current_context_template_data + anatomy_data = get_current_context_template_data() + + task_name = anatomy_data.get("task", {}).get("name") + folder_path = anatomy_data.get("folder", {}).get("path") + return get_imageio_config_preset( + project_name, + folder_path, + task_name, + host_name, + anatomy=anatomy, + project_settings=project_settings, + template_data=anatomy_data, + env=env, + ) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 33567d7280..c32d04c44c 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -459,36 +459,6 @@ def is_representation_from_latest(representation): ) -def get_template_data_from_session(session=None, settings=None): - """Template data for template fill from session keys. - - Args: - session (Union[Dict[str, str], None]): The Session to use. If not - provided use the currently active global Session. - settings (Optional[Dict[str, Any]]): Prepared studio or project - settings. - - Returns: - Dict[str, Any]: All available data from session. - """ - - if session is not None: - project_name = session["AYON_PROJECT_NAME"] - folder_path = session["AYON_FOLDER_PATH"] - task_name = session["AYON_TASK_NAME"] - host_name = session["AYON_HOST_NAME"] - else: - context = get_current_context() - project_name = context["project_name"] - folder_path = context["folder_path"] - task_name = context["task_name"] - host_name = get_current_host_name() - - return get_template_data_with_names( - project_name, folder_path, task_name, host_name, settings - ) - - def get_current_context_template_data(settings=None): """Prepare template data for current context. diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index 526c7d35c5..d5f06d6a59 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -73,8 +73,8 @@ def get_folder_template_data(folder_entity, project_name): - 'parent' - direct parent name, project name used if is under project - Required document fields: - Folder: 'path' -> Plan to require: 'folderType' + Required entity fields: + Folder: 'path', 'folderType' Args: folder_entity (Dict[str, Any]): Folder entity. @@ -101,6 +101,8 @@ def get_folder_template_data(folder_entity, project_name): return { "folder": { "name": folder_name, + "type": folder_entity["folderType"], + "path": path, }, "asset": folder_name, "hierarchy": hierarchy, diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index f8cc81e718..ad5a5d43fc 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -33,6 +33,7 @@ import collections import pyblish.api import ayon_api +from ayon_core.pipeline.template_data import get_folder_template_data from ayon_core.pipeline.version_start import get_versioning_start @@ -383,24 +384,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # - 'folder', 'hierarchy', 'parent', 'folder' folder_entity = instance.data.get("folderEntity") if folder_entity: - folder_name = folder_entity["name"] - folder_path = folder_entity["path"] - hierarchy_parts = folder_path.split("/") - hierarchy_parts.pop(0) - hierarchy_parts.pop(-1) - parent_name = project_entity["name"] - if hierarchy_parts: - parent_name = hierarchy_parts[-1] - - hierarchy = "/".join(hierarchy_parts) - anatomy_data.update({ - "asset": folder_name, - "hierarchy": hierarchy, - "parent": parent_name, - "folder": { - "name": folder_name, - }, - }) + folder_data = get_folder_template_data( + folder_entity, + project_entity["name"] + ) + anatomy_data.update(folder_data) return if instance.data.get("newAssetPublishing"): @@ -418,6 +406,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): "parent": parent_name, "folder": { "name": folder_name, + "path": instance.data["folderPath"], + # TODO get folder type from hierarchy + # Using 'Shot' is current default behavior of editorial + # (or 'newAssetPublishing') publishing. + "type": "Shot", }, }) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 9ae96e1a20..865b566e6e 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -108,69 +108,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): label = "Integrate Asset" order = pyblish.api.IntegratorOrder - families = ["workfile", - "pointcache", - "pointcloud", - "proxyAbc", - "camera", - "animation", - "model", - "maxScene", - "mayaAscii", - "mayaScene", - "setdress", - "layout", - "ass", - "assProxy", - "vdbcache", - "scene", - "vrayproxy", - "vrayscene_layer", - "render", - "prerender", - "imagesequence", - "review", - "rendersetup", - "rig", - "plate", - "look", - "ociolook", - "audio", - "yetiRig", - "yeticache", - "nukenodes", - "gizmo", - "source", - "matchmove", - "image", - "assembly", - "fbx", - "gltf", - "textures", - "action", - "harmony.template", - "harmony.palette", - "editorial", - "background", - "camerarig", - "redshiftproxy", - "effect", - "xgen", - "hda", - "usd", - "staticMesh", - "skeletalMesh", - "mvLook", - "mvUsd", - "mvUsdComposition", - "mvUsdOverride", - "online", - "uasset", - "blendScene", - "yeticacheUE", - "tycache", - "csv_ingest_file", - ] default_template_name = "publish" @@ -360,7 +297,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Compute the resource file infos once (files belonging to the # version instance instead of an individual representation) so - # we can re-use those file infos per representation + # we can reuse those file infos per representation resource_file_infos = self.get_files_info( resource_destinations, anatomy ) diff --git a/client/ayon_core/plugins/publish/validate_containers.py b/client/ayon_core/plugins/publish/validate_containers.py index bd21ec9693..520e7a7ce9 100644 --- a/client/ayon_core/plugins/publish/validate_containers.py +++ b/client/ayon_core/plugins/publish/validate_containers.py @@ -1,6 +1,11 @@ import pyblish.api + +from ayon_core.lib import filter_profiles +from ayon_core.host import ILoadHost from ayon_core.pipeline.load import any_outdated_containers from ayon_core.pipeline import ( + get_current_host_name, + registered_host, PublishXmlValidationError, OptionalPyblishPluginMixin ) @@ -18,17 +23,50 @@ class ShowInventory(pyblish.api.Action): host_tools.show_scene_inventory() -class ValidateContainers(OptionalPyblishPluginMixin, - pyblish.api.ContextPlugin): - +class ValidateOutdatedContainers( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Containers are must be updated to latest version on publish.""" label = "Validate Outdated Containers" order = pyblish.api.ValidatorOrder - hosts = ["maya", "houdini", "nuke", "harmony", "photoshop", "aftereffects"] + optional = True actions = [ShowInventory] + @classmethod + def apply_settings(cls, settings): + # Disable plugin if host does not inherit from 'ILoadHost' + # - not a host that can load containers + host = registered_host() + if not isinstance(host, ILoadHost): + cls.enabled = False + return + + # Disable if no profile is found for the current host + profiles = ( + settings + ["core"] + ["publish"] + ["ValidateOutdatedContainers"] + ["plugin_state_profiles"] + ) + profile = filter_profiles( + profiles, {"host_names": get_current_host_name()} + ) + if not profile: + cls.enabled = False + return + + # Apply settings from profile + for attr_name in { + "enabled", + "optional", + "active", + }: + setattr(cls, attr_name, profile[attr_name]) + def process(self, context): if not self.is_active(context.data): return diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py index 9031194e8c..25a5757330 100644 --- a/client/ayon_core/plugins/publish/validate_version.py +++ b/client/ayon_core/plugins/publish/validate_version.py @@ -1,8 +1,10 @@ import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError +from ayon_core.pipeline.publish import ( + PublishValidationError, OptionalPyblishPluginMixin +) -class ValidateVersion(pyblish.api.InstancePlugin): +class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate instance version. AYON does not allow overwriting previously published versions. @@ -18,6 +20,9 @@ class ValidateVersion(pyblish.api.InstancePlugin): active = True def process(self, instance): + if not self.is_active(instance.data): + return + version = instance.data.get("version") latest_version = instance.data.get("latestVersion") diff --git a/client/ayon_core/scripts/ocio_wrapper.py b/client/ayon_core/scripts/ocio_wrapper.py index 0a78e33c1f..0414fc59ce 100644 --- a/client/ayon_core/scripts/ocio_wrapper.py +++ b/client/ayon_core/scripts/ocio_wrapper.py @@ -1,28 +1,31 @@ """OpenColorIO Wrapper. -Only to be interpreted by Python 3. It is run in subprocess in case -Python 2 hosts needs to use it. Or it is used as module for Python 3 -processing. - -Providing functionality: -- get_colorspace - console command - python 2 - - returning all available color spaces - found in input config path. -- _get_colorspace_data - python 3 - module function - - returning all available colorspaces - found in input config path. -- get_views - console command - python 2 - - returning all available viewers - found in input config path. -- _get_views_data - python 3 - module function - - returning all available viewers - found in input config path. +Receive OpenColorIO information and store it in JSON format for processed +that don't have access to OpenColorIO or their version of OpenColorIO is +not compatible. """ -import click import json from pathlib import Path -import PyOpenColorIO as ocio + +import click + +from ayon_core.pipeline.colorspace import ( + has_compatible_ocio_package, + get_display_view_colorspace_name, + get_config_file_rules_colorspace_from_filepath, + get_config_version_data, + get_ocio_config_views, + get_ocio_config_colorspaces, +) + + +def _save_output_to_json_file(output, output_path): + json_path = Path(output_path) + with open(json_path, "w") as stream: + json.dump(output, stream) + + print(f"Data are saved to '{json_path}'") @click.group() @@ -30,404 +33,185 @@ def main(): pass # noqa: WPS100 -@main.group() -def config(): - """Config related commands group - - Example of use: - > pyton.exe ./ocio_wrapper.py config *args - """ - pass # noqa: WPS100 - - -@main.group() -def colorspace(): - """Colorspace related commands group - - Example of use: - > pyton.exe ./ocio_wrapper.py config *args - """ - pass # noqa: WPS100 - - -@config.command( - name="get_colorspace", - help=( - "return all colorspaces from config file " - "--path input arg is required" - ) -) -@click.option("--in_path", required=True, - help="path where to read ocio config file", - type=click.Path(exists=True)) -@click.option("--out_path", required=True, - help="path where to write output json file", - type=click.Path()) -def get_colorspace(in_path, out_path): +@main.command( + name="get_ocio_config_colorspaces", + help="return all colorspaces from config file") +@click.option( + "--config_path", + required=True, + help="OCIO config path to read ocio config file.", + type=click.Path(exists=True)) +@click.option( + "--output_path", + required=True, + help="path where to write output json file", + type=click.Path()) +def _get_ocio_config_colorspaces(config_path, output_path): """Aggregate all colorspace to file. - Python 2 wrapped console command - Args: - in_path (str): config file path string - out_path (str): temp json file path string + config_path (str): config file path string + output_path (str): temp json file path string Example of use: > pyton.exe ./ocio_wrapper.py config get_colorspace - --in_path= --out_path= + --config_path --output_path """ - json_path = Path(out_path) - - out_data = _get_colorspace_data(in_path) - - with open(json_path, "w") as f_: - json.dump(out_data, f_) - - print(f"Colorspace data are saved to '{json_path}'") - - -def _get_colorspace_data(config_path): - """Return all found colorspace data. - - Args: - config_path (str): path string leading to config.ocio - - Raises: - IOError: Input config does not exist. - - Returns: - dict: aggregated available colorspaces - """ - config_path = Path(config_path) - - if not config_path.is_file(): - raise IOError( - f"Input path `{config_path}` should be `config.ocio` file") - - config = ocio.Config().CreateFromFile(str(config_path)) - - colorspace_data = { - "roles": {}, - "colorspaces": { - color.getName(): { - "family": color.getFamily(), - "categories": list(color.getCategories()), - "aliases": list(color.getAliases()), - "equalitygroup": color.getEqualityGroup(), - } - for color in config.getColorSpaces() - }, - "displays_views": { - f"{view} ({display})": { - "display": display, - "view": view - - } - for display in config.getDisplays() - for view in config.getViews(display) - }, - "looks": {} - } - - # add looks - looks = config.getLooks() - if looks: - colorspace_data["looks"] = { - look.getName(): {"process_space": look.getProcessSpace()} - for look in looks - } - - # add roles - roles = config.getRoles() - if roles: - colorspace_data["roles"] = { - role: {"colorspace": colorspace} - for (role, colorspace) in roles - } - - return colorspace_data - - -@config.command( - name="get_views", - help=( - "return all viewers from config file " - "--path input arg is required" + _save_output_to_json_file( + get_ocio_config_colorspaces(config_path), + output_path ) -) -@click.option("--in_path", required=True, - help="path where to read ocio config file", - type=click.Path(exists=True)) -@click.option("--out_path", required=True, - help="path where to write output json file", - type=click.Path()) -def get_views(in_path, out_path): + + +@main.command( + name="get_ocio_config_views", + help="All viewers from config file") +@click.option( + "--config_path", + required=True, + help="OCIO config path to read ocio config file.", + type=click.Path(exists=True)) +@click.option( + "--output_path", + required=True, + help="path where to write output json file", + type=click.Path()) +def _get_ocio_config_views(config_path, output_path): """Aggregate all viewers to file. - Python 2 wrapped console command - Args: - in_path (str): config file path string - out_path (str): temp json file path string + config_path (str): config file path string + output_path (str): temp json file path string Example of use: > pyton.exe ./ocio_wrapper.py config get_views \ - --in_path= --out_path= + --config_path --output """ - json_path = Path(out_path) - - out_data = _get_views_data(in_path) - - with open(json_path, "w") as f_: - json.dump(out_data, f_) - - print(f"Viewer data are saved to '{json_path}'") - - -def _get_views_data(config_path): - """Return all found viewer data. - - Args: - config_path (str): path string leading to config.ocio - - Raises: - IOError: Input config does not exist. - - Returns: - dict: aggregated available viewers - """ - config_path = Path(config_path) - - if not config_path.is_file(): - raise IOError("Input path should be `config.ocio` file") - - config = ocio.Config().CreateFromFile(str(config_path)) - - data_ = {} - for display in config.getDisplays(): - for view in config.getViews(display): - colorspace = config.getDisplayViewColorSpaceName(display, view) - # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa - if colorspace == "": - colorspace = display - - data_[f"{display}/{view}"] = { - "display": display, - "view": view, - "colorspace": colorspace - } - - return data_ - - -@config.command( - name="get_version", - help=( - "return major and minor version from config file " - "--config_path input arg is required" - "--out_path input arg is required" + _save_output_to_json_file( + get_ocio_config_views(config_path), + output_path ) -) -@click.option("--config_path", required=True, - help="path where to read ocio config file", - type=click.Path(exists=True)) -@click.option("--out_path", required=True, - help="path where to write output json file", - type=click.Path()) -def get_version(config_path, out_path): - """Get version of config. - Python 2 wrapped console command + +@main.command( + name="get_config_version_data", + help="Get major and minor version from config file") +@click.option( + "--config_path", + required=True, + help="OCIO config path to read ocio config file.", + type=click.Path(exists=True)) +@click.option( + "--output_path", + required=True, + help="path where to write output json file", + type=click.Path()) +def _get_config_version_data(config_path, output_path): + """Get version of config. Args: config_path (str): ocio config file path string - out_path (str): temp json file path string + output_path (str): temp json file path string Example of use: > pyton.exe ./ocio_wrapper.py config get_version \ - --config_path= --out_path= + --config_path --output_path """ - json_path = Path(out_path) - - out_data = _get_version_data(config_path) - - with open(json_path, "w") as f_: - json.dump(out_data, f_) - - print(f"Config version data are saved to '{json_path}'") - - -def _get_version_data(config_path): - """Return major and minor version info. - - Args: - config_path (str): path string leading to config.ocio - - Raises: - IOError: Input config does not exist. - - Returns: - dict: minor and major keys with values - """ - config_path = Path(config_path) - - if not config_path.is_file(): - raise IOError("Input path should be `config.ocio` file") - - config = ocio.Config().CreateFromFile(str(config_path)) - - return { - "major": config.getMajorVersion(), - "minor": config.getMinorVersion() - } - - -@colorspace.command( - name="get_config_file_rules_colorspace_from_filepath", - help=( - "return colorspace from filepath " - "--config_path - ocio config file path (input arg is required) " - "--filepath - any file path (input arg is required) " - "--out_path - temp json file path (input arg is required)" + _save_output_to_json_file( + get_config_version_data(config_path), + output_path ) -) -@click.option("--config_path", required=True, - help="path where to read ocio config file", - type=click.Path(exists=True)) -@click.option("--filepath", required=True, - help="path to file to get colorspace from", - type=click.Path()) -@click.option("--out_path", required=True, - help="path where to write output json file", - type=click.Path()) -def get_config_file_rules_colorspace_from_filepath( - config_path, filepath, out_path + + +@main.command( + name="get_config_file_rules_colorspace_from_filepath", + help="Colorspace file rules from filepath") +@click.option( + "--config_path", + required=True, + help="OCIO config path to read ocio config file.", + type=click.Path(exists=True)) +@click.option( + "--filepath", + required=True, + help="Path to file to get colorspace from.", + type=click.Path()) +@click.option( + "--output_path", + required=True, + help="Path where to write output json file.", + type=click.Path()) +def _get_config_file_rules_colorspace_from_filepath( + config_path, filepath, output_path ): """Get colorspace from file path wrapper. - Python 2 wrapped console command - Args: config_path (str): config file path string filepath (str): path string leading to file - out_path (str): temp json file path string + output_path (str): temp json file path string Example of use: - > pyton.exe ./ocio_wrapper.py \ + > python.exe ./ocio_wrapper.py \ colorspace get_config_file_rules_colorspace_from_filepath \ - --config_path= --filepath= --out_path= + --config_path --filepath --output_path """ - json_path = Path(out_path) - - colorspace = _get_config_file_rules_colorspace_from_filepath( - config_path, filepath) - - with open(json_path, "w") as f_: - json.dump(colorspace, f_) - - print(f"Colorspace name is saved to '{json_path}'") + _save_output_to_json_file( + get_config_file_rules_colorspace_from_filepath(config_path, filepath), + output_path + ) -def _get_config_file_rules_colorspace_from_filepath(config_path, filepath): - """Return found colorspace data found in v2 file rules. - - Args: - config_path (str): path string leading to config.ocio - filepath (str): path string leading to v2 file rules - - Raises: - IOError: Input config does not exist. - - Returns: - dict: aggregated available colorspaces - """ - config_path = Path(config_path) - - if not config_path.is_file(): - raise IOError( - f"Input path `{config_path}` should be `config.ocio` file") - - config = ocio.Config().CreateFromFile(str(config_path)) - - # TODO: use `parseColorSpaceFromString` instead if ocio v1 - colorspace = config.getColorSpaceFromFilepath(str(filepath)) - - return colorspace - - -def _get_display_view_colorspace_name(config_path, display, view): - """Returns the colorspace attribute of the (display, view) pair. - - Args: - config_path (str): path string leading to config.ocio - display (str): display name e.g. "ACES" - view (str): view name e.g. "sRGB" - - - Raises: - IOError: Input config does not exist. - - Returns: - view color space name (str) e.g. "Output - sRGB" - """ - - config_path = Path(config_path) - - if not config_path.is_file(): - raise IOError("Input path should be `config.ocio` file") - - config = ocio.Config.CreateFromFile(str(config_path)) - colorspace = config.getDisplayViewColorSpaceName(display, view) - - return colorspace - - -@config.command( +@main.command( name="get_display_view_colorspace_name", help=( - "return default view colorspace name " - "for the given display and view " - "--path input arg is required" - ) -) -@click.option("--in_path", required=True, - help="path where to read ocio config file", - type=click.Path(exists=True)) -@click.option("--out_path", required=True, - help="path where to write output json file", - type=click.Path()) -@click.option("--display", required=True, - help="display name", - type=click.STRING) -@click.option("--view", required=True, - help="view name", - type=click.STRING) -def get_display_view_colorspace_name(in_path, out_path, - display, view): + "Default view colorspace name for the given display and view" + )) +@click.option( + "--config_path", + required=True, + help="path where to read ocio config file", + type=click.Path(exists=True)) +@click.option( + "--display", + required=True, + help="Display name", + type=click.STRING) +@click.option( + "--view", + required=True, + help="view name", + type=click.STRING) +@click.option( + "--output_path", + required=True, + help="path where to write output json file", + type=click.Path()) +def _get_display_view_colorspace_name( + config_path, display, view, output_path +): """Aggregate view colorspace name to file. Wrapper command for processes without access to OpenColorIO Args: - in_path (str): config file path string - out_path (str): temp json file path string + config_path (str): config file path string + output_path (str): temp json file path string display (str): display name e.g. "ACES" view (str): view name e.g. "sRGB" Example of use: > pyton.exe ./ocio_wrapper.py config \ - get_display_view_colorspace_name --in_path= \ - --out_path= --display= --view= + get_display_view_colorspace_name --config_path \ + --output_path --display --view """ + _save_output_to_json_file( + get_display_view_colorspace_name(config_path, display, view), + output_path + ) - out_data = _get_display_view_colorspace_name(in_path, - display, - view) - with open(out_path, "w") as f: - json.dump(out_data, f) - - print(f"Display view colorspace saved to '{out_path}'") - -if __name__ == '__main__': +if __name__ == "__main__": + if not has_compatible_ocio_package(): + raise RuntimeError("OpenColorIO is not available.") main() diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 19a38bee21..89dd881a10 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -5,7 +5,7 @@ import ayon_api import six from ayon_core.style import get_default_entity_icon_color -from ayon_core.lib import CacheItem +from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" @@ -17,6 +17,49 @@ class AbstractHierarchyController: pass +class StatusItem: + """Item representing status of project. + + Args: + name (str): Status name ("Not ready"). + color (str): Status color in hex ("#434a56"). + short (str): Short status name ("NRD"). + icon (str): Icon name in MaterialIcons ("fiber_new"). + state (Literal["not_started", "in_progress", "done", "blocked"]): + Status state. + + """ + def __init__(self, name, color, short, icon, state): + self.name = name + self.color = color + self.short = short + self.icon = icon + self.state = state + + def to_data(self): + return { + "name": self.name, + "color": self.color, + "short": self.short, + "icon": self.icon, + "state": self.state, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + @classmethod + def from_project_item(cls, status_data): + return cls( + name=status_data["name"], + color=status_data["color"], + short=status_data["shortName"], + icon=status_data["icon"], + state=status_data["state"], + ) + + class ProjectItem: """Item representing folder entity on a server. @@ -40,6 +83,23 @@ class ProjectItem: } self.icon = icon + @classmethod + def from_entity(cls, project_entity): + """Creates folder item from entity. + + Args: + project_entity (dict[str, Any]): Project entity. + + Returns: + ProjectItem: Project item. + + """ + return cls( + project_entity["name"], + project_entity["active"], + project_entity["library"], + ) + def to_data(self): """Converts folder item to data. @@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects): """ return [ - ProjectItem(project["name"], project["active"], project["library"]) + ProjectItem.from_entity(project) for project in projects ] @@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): self._projects_cache = CacheItem(default_factory=list) - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache = NestedCacheItem( + levels=1, default_factory=list + ) + self._projects_by_name = NestedCacheItem( + levels=1, default_factory=list + ) self._is_refreshing = False self._controller = controller def reset(self): self._projects_cache.reset() - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache.reset() + self._projects_by_name.reset() def refresh(self): + """Refresh project items. + + This method will requery list of ProjectItem returned by + 'get_project_items'. + + To reset all cached items use 'reset' method. + """ self._refresh_projects_cache() def get_project_items(self, sender): @@ -117,12 +188,51 @@ class ProjectsModel(object): return self._projects_cache.get_data() def get_project_entity(self, project_name): - if project_name not in self._projects_by_name: + """Get project entity. + + Args: + project_name (str): Project name. + + Returns: + Union[dict[str, Any], None]: Project entity or None if project + was not found by name. + + """ + project_cache = self._projects_by_name[project_name] + if not project_cache.is_valid: entity = None if project_name: entity = ayon_api.get_project(project_name) - self._projects_by_name[project_name] = entity - return self._projects_by_name[project_name] + project_cache.update_data(entity) + return project_cache.get_data() + + def get_project_status_items(self, project_name, sender): + """Get project status items. + + Args: + project_name (str): Project name. + sender (Union[str, None]): Name of sender who asked for items. + + Returns: + list[StatusItem]: Status items for project. + + """ + statuses_cache = self._project_statuses_cache[project_name] + if not statuses_cache.is_valid: + with self._project_statuses_refresh_event_manager( + sender, project_name + ): + project_entity = None + if project_name: + project_entity = self.get_project_entity(project_name) + statuses = [] + if project_entity: + statuses = [ + StatusItem.from_project_item(status) + for status in project_entity["statuses"] + ] + statuses_cache.update_data(statuses) + return statuses_cache.get_data() @contextlib.contextmanager def _project_refresh_event_manager(self, sender): @@ -143,6 +253,23 @@ class ProjectsModel(object): ) self._is_refreshing = False + @contextlib.contextmanager + def _project_statuses_refresh_event_manager(self, sender, project_name): + self._controller.emit_event( + "projects.statuses.refresh.started", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "projects.statuses.refresh.finished", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + def _refresh_projects_cache(self, sender=None): if self._is_refreshing: return None diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index a225827418..2ffce13292 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -290,6 +290,34 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): painter.drawPixmap(extender_x, extender_y, pix) +class ActionsProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + def lessThan(self, left, right): + # Sort by action order and then by label + left_value = left.data(ACTION_SORT_ROLE) + right_value = right.data(ACTION_SORT_ROLE) + + # Values are same -> use super sorting + if left_value == right_value: + # Default behavior is using DisplayRole + return super().lessThan(left, right) + + # Validate 'None' values + if right_value is None: + return True + if left_value is None: + return False + # Sort values and handle incompatible types + try: + return left_value < right_value + except TypeError: + return True + + class ActionsWidget(QtWidgets.QWidget): def __init__(self, controller, parent): super(ActionsWidget, self).__init__(parent) @@ -316,10 +344,7 @@ class ActionsWidget(QtWidgets.QWidget): model = ActionsQtModel(controller) - proxy_model = QtCore.QSortFilterProxyModel() - proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - proxy_model.setSortRole(ACTION_SORT_ROLE) - + proxy_model = ActionsProxyModel() proxy_model.setSourceModel(model) view.setModel(proxy_model) @@ -359,7 +384,8 @@ class ActionsWidget(QtWidgets.QWidget): def _on_model_refresh(self): self._proxy_model.sort(0) # Force repaint all items - self._view.update() + viewport = self._view.viewport() + viewport.update() def _on_animation(self): time_now = time.time() diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 7a7d335092..509db4d037 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -114,6 +114,7 @@ class VersionItem: thumbnail_id (Union[str, None]): Thumbnail id. published_time (Union[str, None]): Published time in format '%Y%m%dT%H%M%SZ'. + status (Union[str, None]): Status name. author (Union[str, None]): Author. frame_range (Union[str, None]): Frame range. duration (Union[int, None]): Duration. @@ -132,6 +133,7 @@ class VersionItem: thumbnail_id, published_time, author, + status, frame_range, duration, handles, @@ -146,6 +148,7 @@ class VersionItem: self.is_hero = is_hero self.published_time = published_time self.author = author + self.status = status self.frame_range = frame_range self.duration = duration self.handles = handles @@ -185,6 +188,7 @@ class VersionItem: "is_hero": self.is_hero, "published_time": self.published_time, "author": self.author, + "status": self.status, "frame_range": self.frame_range, "duration": self.duration, "handles": self.handles, @@ -488,6 +492,27 @@ class FrontendLoaderController(_BaseLoaderController): pass + @abstractmethod + def get_project_status_items(self, project_name, sender=None): + """Items for all projects available on server. + + Triggers event topics "projects.statuses.refresh.started" and + "projects.statuses.refresh.finished" with data: + { + "sender": sender, + "project_name": project_name + } + + Args: + project_name (Union[str, None]): Project name. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[StatusItem]: List of status items. + """ + + pass + @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 0c9bb369c7..35188369c2 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) + def get_project_status_items(self, project_name, sender=None): + return self._projects_model.get_project_status_items( + project_name, sender + ) + def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index a3bbc30a09..c9325c4480 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -58,6 +58,7 @@ def version_item_from_entity(version): thumbnail_id=version["thumbnailId"], published_time=published_time, author=author, + status=version["status"], frame_range=frame_range, duration=duration, handles=handles, @@ -526,8 +527,11 @@ class ProductsModel: products = list(ayon_api.get_products(project_name, **kwargs)) product_ids = {product["id"] for product in products} + # Add 'status' to fields -> fixed in ayon-python-api 1.0.4 + fields = ayon_api.get_default_fields_for_type("version") + fields.add("status") versions = ayon_api.get_versions( - project_name, product_ids=product_ids + project_name, product_ids=product_ids, fields=fields ) return self._create_product_items( diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 12ed1165ae..1ac19b53eb 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -6,6 +6,9 @@ from ayon_core.tools.utils.lib import format_version from .products_model import ( PRODUCT_ID_ROLE, VERSION_NAME_EDIT_ROLE, + VERSION_STATUS_NAME_ROLE, + VERSION_STATUS_SHORT_ROLE, + VERSION_STATUS_COLOR_ROLE, VERSION_ID_ROLE, PRODUCT_IN_SCENE_ROLE, ACTIVE_SITE_ICON_ROLE, @@ -104,7 +107,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): style = QtWidgets.QApplication.style() style.drawControl( - style.CE_ItemViewItem, option, painter, option.widget + QtWidgets.QCommonStyle.CE_ItemViewItem, + option, + painter, + option.widget ) painter.save() @@ -116,9 +122,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): pen.setColor(fg_color) painter.setPen(pen) - text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_rect = style.subElementRect( + QtWidgets.QCommonStyle.SE_ItemViewItemText, + option + ) text_margin = style.proxy().pixelMetric( - style.PM_FocusFrameHMargin, option, option.widget + QtWidgets.QCommonStyle.PM_FocusFrameHMargin, + option, + option.widget ) + 1 painter.drawText( @@ -194,6 +205,57 @@ class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate): option.palette.setBrush(QtGui.QPalette.Text, color) +class StatusDelegate(QtWidgets.QStyledItemDelegate): + """Delegate showing status name and short name.""" + + def paint(self, painter, option, index): + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + QtWidgets.QCommonStyle.CE_ItemViewItem, + option, + painter, + option.widget + ) + + painter.save() + + text_rect = style.subElementRect( + QtWidgets.QCommonStyle.SE_ItemViewItemText, + option + ) + text_margin = style.proxy().pixelMetric( + QtWidgets.QCommonStyle.PM_FocusFrameHMargin, + option, + option.widget + ) + 1 + padded_text_rect = text_rect.adjusted( + text_margin, 0, - text_margin, 0 + ) + + fm = QtGui.QFontMetrics(option.font) + text = index.data(VERSION_STATUS_NAME_ROLE) + if padded_text_rect.width() < fm.width(text): + text = index.data(VERSION_STATUS_SHORT_ROLE) + + status_color = index.data(VERSION_STATUS_COLOR_ROLE) + fg_color = QtGui.QColor(status_color) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + painter.drawText( + padded_text_rect, + option.displayAlignment, + text + ) + + painter.restore() + + class SiteSyncDelegate(QtWidgets.QStyledItemDelegate): """Paints icons and downloaded representation ration for both sites.""" diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index b465679c3b..f309473d10 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -22,18 +22,21 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11 VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12 VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13 VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14 -VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15 -VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16 -VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17 -VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18 -VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19 -VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20 -VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21 -ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22 -REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 -REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24 -SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25 -SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 18 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 19 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 20 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 21 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 22 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 23 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 24 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 25 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 27 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 28 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29 class ProductsModel(QtGui.QStandardItemModel): @@ -44,6 +47,7 @@ class ProductsModel(QtGui.QStandardItemModel): "Product type", "Folder", "Version", + "Status", "Time", "Author", "Frames", @@ -69,11 +73,35 @@ class ProductsModel(QtGui.QStandardItemModel): ] ] - version_col = column_labels.index("Version") - published_time_col = column_labels.index("Time") + product_name_col = column_labels.index("Product name") + product_type_col = column_labels.index("Product type") folders_label_col = column_labels.index("Folder") + version_col = column_labels.index("Version") + status_col = column_labels.index("Status") + published_time_col = column_labels.index("Time") + author_col = column_labels.index("Author") + frame_range_col = column_labels.index("Frames") + duration_col = column_labels.index("Duration") + handles_col = column_labels.index("Handles") + step_col = column_labels.index("Step") in_scene_col = column_labels.index("In scene") sitesync_avail_col = column_labels.index("Availability") + _display_role_mapping = { + product_name_col: QtCore.Qt.DisplayRole, + product_type_col: PRODUCT_TYPE_ROLE, + folders_label_col: FOLDER_LABEL_ROLE, + version_col: VERSION_NAME_ROLE, + status_col: VERSION_STATUS_NAME_ROLE, + published_time_col: VERSION_PUBLISH_TIME_ROLE, + author_col: VERSION_AUTHOR_ROLE, + frame_range_col: VERSION_FRAME_RANGE_ROLE, + duration_col: VERSION_DURATION_ROLE, + handles_col: VERSION_HANDLES_ROLE, + step_col: VERSION_STEP_ROLE, + in_scene_col: PRODUCT_IN_SCENE_ROLE, + sitesync_avail_col: VERSION_AVAILABLE_ROLE, + + } def __init__(self, controller): super(ProductsModel, self).__init__() @@ -96,6 +124,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = None self._last_folder_ids = [] + self._last_project_statuses = {} def get_product_item_indexes(self): return [ @@ -141,6 +170,15 @@ class ProductsModel(QtGui.QStandardItemModel): if not index.isValid(): return None + if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE): + status_name = self.data(index, VERSION_STATUS_NAME_ROLE) + status_item = self._last_project_statuses.get(status_name) + if status_item is None: + return "" + if role == VERSION_STATUS_SHORT_ROLE: + return status_item.short + return status_item.color + col = index.column() if col == 0: return super(ProductsModel, self).data(index, role) @@ -168,29 +206,8 @@ class ProductsModel(QtGui.QStandardItemModel): if role == QtCore.Qt.DisplayRole: if not index.data(PRODUCT_ID_ROLE): return None - if col == self.version_col: - role = VERSION_NAME_ROLE - elif col == 1: - role = PRODUCT_TYPE_ROLE - elif col == 2: - role = FOLDER_LABEL_ROLE - elif col == 4: - role = VERSION_PUBLISH_TIME_ROLE - elif col == 5: - role = VERSION_AUTHOR_ROLE - elif col == 6: - role = VERSION_FRAME_RANGE_ROLE - elif col == 7: - role = VERSION_DURATION_ROLE - elif col == 8: - role = VERSION_HANDLES_ROLE - elif col == 9: - role = VERSION_STEP_ROLE - elif col == 10: - role = PRODUCT_IN_SCENE_ROLE - elif col == 11: - role = VERSION_AVAILABLE_ROLE - else: + role = self._display_role_mapping.get(col) + if role is None: return None index = self.index(index.row(), 0, index.parent()) @@ -312,6 +329,7 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.published_time, VERSION_PUBLISH_TIME_ROLE ) model_item.setData(version_item.author, VERSION_AUTHOR_ROLE) + model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE) model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE) model_item.setData(version_item.duration, VERSION_DURATION_ROLE) model_item.setData(version_item.handles, VERSION_HANDLES_ROLE) @@ -393,6 +411,11 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = project_name self._last_folder_ids = folder_ids + status_items = self._controller.get_project_status_items(project_name) + self._last_project_statuses = { + status_item.name: status_item + for status_item in status_items + } active_site_icon_def = self._controller.get_active_site_icon_def( project_name diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index d9f027153e..3a30d83d52 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -22,7 +22,8 @@ from .products_model import ( from .products_delegates import ( VersionDelegate, LoadedInSceneDelegate, - SiteSyncDelegate + StatusDelegate, + SiteSyncDelegate, ) from .actions_utils import show_actions_menu @@ -89,6 +90,7 @@ class ProductsWidget(QtWidgets.QWidget): 90, # Product type 130, # Folder label 60, # Version + 100, # Status 125, # Time 75, # Author 75, # Frames @@ -128,20 +130,19 @@ class ProductsWidget(QtWidgets.QWidget): products_view.setColumnWidth(idx, width) version_delegate = VersionDelegate() - products_view.setItemDelegateForColumn( - products_model.version_col, version_delegate) - time_delegate = PrettyTimeDelegate() - products_view.setItemDelegateForColumn( - products_model.published_time_col, time_delegate) - + status_delegate = StatusDelegate() in_scene_delegate = LoadedInSceneDelegate() - products_view.setItemDelegateForColumn( - products_model.in_scene_col, in_scene_delegate) - sitesync_delegate = SiteSyncDelegate() - products_view.setItemDelegateForColumn( - products_model.sitesync_avail_col, sitesync_delegate) + + for col, delegate in ( + (products_model.version_col, version_delegate), + (products_model.published_time_col, time_delegate), + (products_model.status_col, status_delegate), + (products_model.in_scene_col, in_scene_delegate), + (products_model.sitesync_avail_col, sitesync_delegate), + ): + products_view.setItemDelegateForColumn(col, delegate) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -175,6 +176,7 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate = version_delegate self._time_delegate = time_delegate + self._status_delegate = status_delegate self._in_scene_delegate = in_scene_delegate self._sitesync_delegate = sitesync_delegate diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 3a6f4679fa..8529a53b06 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -335,9 +335,7 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super(LoaderWindow, self).closeEvent(event) - # Deselect project so current context will be selected - # on next 'showEvent' - self._controller.set_selected_project(None) + self._reset_on_show = True def keyPressEvent(self, event): diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 6e43050c05..5937ffa4da 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -723,7 +723,6 @@ class ProjectPushItemProcess: dst_project_name = self._item.dst_project_name dst_folder_id = self._item.dst_folder_id dst_task_name = self._item.dst_task_name - dst_task_name_low = dst_task_name.lower() new_folder_name = self._item.new_folder_name if not dst_folder_id and not new_folder_name: self._status.set_failed( @@ -765,7 +764,7 @@ class ProjectPushItemProcess: dst_project_name, folder_ids=[folder_entity["id"]] ) } - task_info = folder_tasks.get(dst_task_name_low) + task_info = folder_tasks.get(dst_task_name.lower()) if not task_info: self._status.set_failed( f"Could find task with name \"{dst_task_name}\"" diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a60de0493a..e4297e2000 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.1-dev.1" +__version__ = "0.3.3-dev.1" diff --git a/client/pyproject.toml b/client/pyproject.toml index 1a0ad7e5f2..5e811321f8 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -16,7 +16,7 @@ aiohttp_json_rpc = "*" # TVPaint server aiohttp-middlewares = "^2.0.0" wsrpc_aiohttp = "^3.1.1" # websocket server Click = "^8" -OpenTimelineIO = "0.14.1" +OpenTimelineIO = "0.16.0" opencolorio = "2.2.1" Pillow = "9.5.0" pynput = "^1.7.2" # Timers manager - TODO remove diff --git a/package.py b/package.py index 79450d029f..73f7174b6f 100644 --- a/package.py +++ b/package.py @@ -1,11 +1,12 @@ name = "core" title = "Core" -version = "0.3.1-dev.1" +version = "0.3.3-dev.1" client_dir = "ayon_core" plugin_for = ["ayon_server"] -requires = [ - "~ayon_server-1.0.3+<2.0.0", -] +ayon_server_version = ">=1.0.3,<2.0.0" +ayon_launcher_version = ">=1.0.2" +ayon_required_addons = {} +ayon_compatible_addons = {} diff --git a/server/__init__.py b/server/__init__.py index 152cc77218..79f505ccd5 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,3 +1,5 @@ +from typing import Any + from ayon_server.addons import BaseServerAddon from .settings import CoreSettings, DEFAULT_VALUES @@ -9,3 +11,53 @@ class CoreAddon(BaseServerAddon): async def get_default_settings(self): settings_model_cls = self.get_settings_model() return settings_model_cls(**DEFAULT_VALUES) + + async def convert_settings_overrides( + self, + source_version: str, + overrides: dict[str, Any], + ) -> dict[str, Any]: + self._convert_imagio_configs_0_3_1(overrides) + # Use super conversion + return await super().convert_settings_overrides( + source_version, overrides + ) + + def _convert_imagio_configs_0_3_1(self, overrides): + """Imageio config settings did change to profiles since 0.3.1. .""" + imageio_overrides = overrides.get("imageio") or {} + if ( + "ocio_config" not in imageio_overrides + or "filepath" not in imageio_overrides["ocio_config"] + ): + return + + ocio_config = imageio_overrides.pop("ocio_config") + + filepath = ocio_config["filepath"] + if not filepath: + return + first_filepath = filepath[0] + ocio_config_profiles = imageio_overrides.setdefault( + "ocio_config_profiles", [] + ) + base_value = { + "type": "builtin_path", + "product_name": "", + "host_names": [], + "task_names": [], + "task_types": [], + "custom_path": "", + "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio" + } + if first_filepath in ( + "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio", + ): + base_value["type"] = "builtin_path" + base_value["builtin_path"] = first_filepath + else: + base_value["type"] = "custom_path" + base_value["custom_path"] = first_filepath + + ocio_config_profiles.append(base_value) diff --git a/server/settings/main.py b/server/settings/main.py index 28a69e182d..40e16e7e91 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -54,9 +54,67 @@ class CoreImageIOFileRulesModel(BaseSettingsModel): return value -class CoreImageIOConfigModel(BaseSettingsModel): - filepath: list[str] = SettingsField( - default_factory=list, title="Config path" +def _ocio_config_profile_types(): + return [ + {"value": "builtin_path", "label": "AYON built-in OCIO config"}, + {"value": "custom_path", "label": "Path to OCIO config"}, + {"value": "product_name", "label": "Published product"}, + ] + + +def _ocio_built_in_paths(): + return [ + { + "value": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "label": "ACES 1.2", + "description": "Aces 1.2 OCIO config file." + }, + { + "value": "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio", + "label": "Nuke default", + }, + ] + + +class CoreImageIOConfigProfilesModel(BaseSettingsModel): + _layout = "expanded" + host_names: list[str] = SettingsField( + default_factory=list, + title="Host names" + ) + task_types: list[str] = SettingsField( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names" + ) + type: str = SettingsField( + title="Profile type", + enum_resolver=_ocio_config_profile_types, + conditionalEnum=True, + default="builtin_path", + section="---", + ) + builtin_path: str = SettingsField( + "ACES 1.2", + title="Built-in OCIO config", + enum_resolver=_ocio_built_in_paths, + ) + custom_path: str = SettingsField( + "", + title="OCIO config path", + description="Path to OCIO config. Anatomy formatting is supported.", + ) + product_name: str = SettingsField( + "", + title="Product name", + description=( + "Published product name to get OCIO config from. " + "Partial match is supported." + ), ) @@ -65,9 +123,8 @@ class CoreImageIOBaseModel(BaseSettingsModel): False, title="Enable Color Management" ) - ocio_config: CoreImageIOConfigModel = SettingsField( - default_factory=CoreImageIOConfigModel, - title="OCIO config" + ocio_config_profiles: list[CoreImageIOConfigProfilesModel] = SettingsField( + default_factory=list, title="OCIO config profiles" ) file_rules: CoreImageIOFileRulesModel = SettingsField( default_factory=CoreImageIOFileRulesModel, @@ -186,12 +243,17 @@ class CoreSettings(BaseSettingsModel): DEFAULT_VALUES = { "imageio": { "activate_global_color_management": False, - "ocio_config": { - "filepath": [ - "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", - "{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio" - ] - }, + "ocio_config_profiles": [ + { + "host_names": [], + "task_types": [], + "task_names": [], + "type": "builtin_path", + "builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio", + "custom_path": "", + "product_name": "", + } + ], "file_rules": { "activate_global_file_rules": False, "rules": [ @@ -199,42 +261,57 @@ DEFAULT_VALUES = { "name": "example", "pattern": ".*(beauty).*", "colorspace": "ACES - ACEScg", - "ext": "exr" + "ext": "exr", } - ] - } + ], + }, }, "studio_name": "", "studio_code": "", - "environments": "{\n\"STUDIO_SW\": {\n \"darwin\": \"/mnt/REPO_SW\",\n \"linux\": \"/mnt/REPO_SW\",\n \"windows\": \"P:/REPO_SW\"\n }\n}", + "environments": json.dumps( + { + "STUDIO_SW": { + "darwin": "/mnt/REPO_SW", + "linux": "/mnt/REPO_SW", + "windows": "P:/REPO_SW" + } + }, + indent=4 + ), "tools": DEFAULT_TOOLS_VALUES, "version_start_category": { "profiles": [] }, "publish": DEFAULT_PUBLISH_VALUES, - "project_folder_structure": json.dumps({ - "__project_root__": { - "prod": {}, - "resources": { - "footage": { - "plates": {}, - "offline": {} + "project_folder_structure": json.dumps( + { + "__project_root__": { + "prod": {}, + "resources": { + "footage": { + "plates": {}, + "offline": {} + }, + "audio": {}, + "art_dept": {} }, - "audio": {}, - "art_dept": {} - }, - "editorial": {}, - "assets": { - "characters": {}, - "locations": {} - }, - "shots": {} - } - }, indent=4), + "editorial": {}, + "assets": { + "characters": {}, + "locations": {} + }, + "shots": {} + } + }, + indent=4 + ), "project_plugins": { "windows": [], "darwin": [], "linux": [] }, - "project_environments": "{}" + "project_environments": json.dumps( + {}, + indent=4 + ) } diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index e61bf6986b..61e73ce912 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -59,6 +59,33 @@ class CollectFramesFixDefModel(BaseSettingsModel): ) +class ValidateOutdatedContainersProfile(BaseSettingsModel): + _layout = "expanded" + # Filtering + host_names: list[str] = SettingsField( + default_factory=list, + title="Host names" + ) + # Profile values + enabled: bool = SettingsField(True, title="Enabled") + optional: bool = SettingsField(True, title="Optional") + active: bool = SettingsField(True, title="Active") + + +class ValidateOutdatedContainersModel(BaseSettingsModel): + """Validate if Publishing intent was selected. + + It is possible to disable validation for specific publishing context + with profiles. + """ + + _isGroup = True + plugin_state_profiles: list[ValidateOutdatedContainersProfile] = SettingsField( + default_factory=list, + title="Plugin enable state profiles", + ) + + class ValidateIntentProfile(BaseSettingsModel): _layout = "expanded" hosts: list[str] = SettingsField(default_factory=list, title="Host names") @@ -770,6 +797,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidateBaseModel, title="Validate Version" ) + ValidateOutdatedContainers: ValidateOutdatedContainersModel = SettingsField( + default_factory=ValidateOutdatedContainersModel, + title="Validate Containers" + ) ValidateIntent: ValidateIntentModel = SettingsField( default_factory=ValidateIntentModel, title="Validate Intent" @@ -855,6 +886,25 @@ DEFAULT_PUBLISH_VALUES = { "optional": False, "active": True }, + "ValidateOutdatedContainers": { + "plugin_state_profiles": [ + { + # Default host names are based on original + # filter of ValidateContainer pyblish plugin + "host_names": [ + "maya", + "houdini", + "nuke", + "harmony", + "photoshop", + "aftereffects" + ], + "enabled": True, + "optional": True, + "active": True + } + ] + }, "ValidateIntent": { "enabled": False, "profiles": [] diff --git a/server_addon/aftereffects/package.py b/server_addon/aftereffects/package.py index a680b37602..7a2f9bc7af 100644 --- a/server_addon/aftereffects/package.py +++ b/server_addon/aftereffects/package.py @@ -1,3 +1,3 @@ name = "aftereffects" title = "AfterEffects" -version = "0.1.3" +version = "0.1.4" diff --git a/server_addon/aftereffects/server/settings/publish_plugins.py b/server_addon/aftereffects/server/settings/publish_plugins.py index 61d67f26d3..a9f30c6686 100644 --- a/server_addon/aftereffects/server/settings/publish_plugins.py +++ b/server_addon/aftereffects/server/settings/publish_plugins.py @@ -22,12 +22,6 @@ class ValidateSceneSettingsModel(BaseSettingsModel): ) -class ValidateContainersModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - optional: bool = SettingsField(True, title="Optional") - active: bool = SettingsField(True, title="Active") - - class AfterEffectsPublishPlugins(BaseSettingsModel): CollectReview: CollectReviewPluginModel = SettingsField( default_factory=CollectReviewPluginModel, @@ -37,10 +31,6 @@ class AfterEffectsPublishPlugins(BaseSettingsModel): default_factory=ValidateSceneSettingsModel, title="Validate Scene Settings", ) - ValidateContainers: ValidateContainersModel = SettingsField( - default_factory=ValidateContainersModel, - title="Validate Containers", - ) AE_PUBLISH_PLUGINS_DEFAULTS = { @@ -58,9 +48,4 @@ AE_PUBLISH_PLUGINS_DEFAULTS = { ".*" ] }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True, - } } diff --git a/server_addon/blender/package.py b/server_addon/blender/package.py index 667076e533..d2c02a4909 100644 --- a/server_addon/blender/package.py +++ b/server_addon/blender/package.py @@ -1,3 +1,3 @@ name = "blender" title = "Blender" -version = "0.1.8" +version = "0.1.9" diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index e998d7b057..8db8c5be46 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -151,6 +151,10 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=ExtractPlayblastModel, title="Extract Playblast" ) + ExtractModelUSD: ValidatePluginModel = SettingsField( + default_factory=ValidatePluginModel, + title="Extract Model USD" + ) DEFAULT_BLENDER_PUBLISH_SETTINGS = { @@ -348,5 +352,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { }, indent=4 ) + }, + "ExtractModelUSD": { + "enabled": True, + "optional": True, + "active": True } } diff --git a/server_addon/clockify/client/ayon_clockify/__init__.py b/server_addon/clockify/client/ayon_clockify/__init__.py new file mode 100644 index 0000000000..75fb87494e --- /dev/null +++ b/server_addon/clockify/client/ayon_clockify/__init__.py @@ -0,0 +1,5 @@ +from .addon import ClockifyAddon + +__all__ = ( + "ClockifyAddon", +) diff --git a/client/ayon_core/modules/clockify/clockify_module.py b/server_addon/clockify/client/ayon_clockify/addon.py similarity index 93% rename from client/ayon_core/modules/clockify/clockify_module.py rename to server_addon/clockify/client/ayon_clockify/addon.py index d2ee4f1e1e..ce91b2be70 100644 --- a/client/ayon_core/modules/clockify/clockify_module.py +++ b/server_addon/clockify/client/ayon_clockify/addon.py @@ -2,12 +2,12 @@ import os import threading import time -from ayon_core.modules import AYONAddon, ITrayModule, IPluginPaths +from ayon_core.addon import AYONAddon, ITrayAddon, IPluginPaths from .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH -class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): +class ClockifyAddon(AYONAddon, ITrayAddon, IPluginPaths): name = "clockify" def initialize(self, studio_settings): @@ -31,7 +31,7 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): # TimersManager attributes # - set `timers_manager_connector` only in `tray_init` self.timers_manager_connector = None - self._timers_manager_module = None + self._timer_manager_addon = None @property def clockify_api(self): @@ -87,7 +87,7 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): return {"actions": [actions_path]} def get_ftrack_event_handler_paths(self): - """Function for Ftrack module to add ftrack event handler paths.""" + """Function for ftrack addon to add ftrack event handler paths.""" return { "user": [CLOCKIFY_FTRACK_USER_PATH], "server": [CLOCKIFY_FTRACK_SERVER_PATH], @@ -206,19 +206,19 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): self.action_stop_timer.setVisible(self.bool_timer_run) # --- TimersManager connection methods --- - def register_timers_manager(self, timer_manager_module): + def register_timers_manager(self, timer_manager_addon): """Store TimersManager for future use.""" - self._timers_manager_module = timer_manager_module + self._timer_manager_addon = timer_manager_addon def timer_started(self, data): """Tell TimersManager that timer started.""" - if self._timers_manager_module is not None: - self._timers_manager_module.timer_started(self.id, data) + if self._timer_manager_addon is not None: + self._timer_manager_addon.timer_started(self.id, data) def timer_stopped(self): """Tell TimersManager that timer stopped.""" - if self._timers_manager_module is not None: - self._timers_manager_module.timer_stopped(self.id) + if self._timer_manager_addon is not None: + self._timer_manager_addon.timer_stopped(self.id) def stop_timer(self): """Called from TimersManager to stop timer.""" diff --git a/client/ayon_core/modules/clockify/clockify_api.py b/server_addon/clockify/client/ayon_clockify/clockify_api.py similarity index 99% rename from client/ayon_core/modules/clockify/clockify_api.py rename to server_addon/clockify/client/ayon_clockify/clockify_api.py index 2e1d8f008f..38ca6cdb66 100644 --- a/client/ayon_core/modules/clockify/clockify_api.py +++ b/server_addon/clockify/client/ayon_clockify/clockify_api.py @@ -1,15 +1,17 @@ import os import json import datetime + import requests + +from ayon_core.lib.local_settings import AYONSecureRegistry +from ayon_core.lib import Logger + from .constants import ( CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, ) -from ayon_core.lib.local_settings import AYONSecureRegistry -from ayon_core.lib import Logger - class ClockifyAPI: log = Logger.get_logger(__name__) diff --git a/client/ayon_core/modules/clockify/constants.py b/server_addon/clockify/client/ayon_clockify/constants.py similarity index 100% rename from client/ayon_core/modules/clockify/constants.py rename to server_addon/clockify/client/ayon_clockify/constants.py diff --git a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py b/server_addon/clockify/client/ayon_clockify/ftrack/server/action_clockify_sync_server.py similarity index 97% rename from client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py rename to server_addon/clockify/client/ayon_clockify/ftrack/server/action_clockify_sync_server.py index 7854f0ceba..ed83fed287 100644 --- a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/server_addon/clockify/client/ayon_clockify/ftrack/server/action_clockify_sync_server.py @@ -1,7 +1,9 @@ import os import json -from openpype_modules.ftrack.lib import ServerAction -from openpype_modules.clockify.clockify_api import ClockifyAPI + +from ayon_clockify.clockify_api import ClockifyAPI + +from ayon_ftrack.lib import ServerAction class SyncClockifyServer(ServerAction): diff --git a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py b/server_addon/clockify/client/ayon_clockify/ftrack/user/action_clockify_sync_local.py similarity index 87% rename from client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py rename to server_addon/clockify/client/ayon_clockify/ftrack/user/action_clockify_sync_local.py index 4701653a0b..05a94e56fd 100644 --- a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py +++ b/server_addon/clockify/client/ayon_clockify/ftrack/user/action_clockify_sync_local.py @@ -1,25 +1,20 @@ import json -from openpype_modules.ftrack.lib import BaseAction, statics_icon -from openpype_modules.clockify.clockify_api import ClockifyAPI +from ayon_clockify.clockify_api import ClockifyAPI +from ayon_ftrack.lib import BaseAction, statics_icon class SyncClockifyLocal(BaseAction): - '''Synchronise project names and task types.''' + """Synchronise project names and task types.""" - #: Action identifier. - identifier = 'clockify.sync.local' - #: Action label. - label = 'Sync To Clockify (local)' - #: Action description. - description = 'Synchronise data to Clockify workspace' - #: roles that are allowed to register this action + identifier = "clockify.sync.local" + label = "Sync To Clockify" + description = "Synchronise data to Clockify workspace" role_list = ["Administrator", "project Manager"] - #: icon icon = statics_icon("app_icons", "clockify-white.png") def __init__(self, *args, **kwargs): super(SyncClockifyLocal, self).__init__(*args, **kwargs) - #: CLockifyApi + self.clockify_api = ClockifyAPI() def discover(self, session, entities, event): @@ -56,7 +51,7 @@ class SyncClockifyLocal(BaseAction): 'user': user, 'status': 'running', 'data': json.dumps({ - 'description': 'Sync Ftrack to Clockify' + 'description': 'Sync ftrack to Clockify' }) }) session.commit() diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py b/server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifyStart.py similarity index 96% rename from client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py rename to server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifyStart.py index 8381c7d73e..d69d0371c0 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py +++ b/server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifyStart.py @@ -1,7 +1,8 @@ import ayon_api +from ayon_clockify.clockify_api import ClockifyAPI + from ayon_core.pipeline import LauncherAction -from openpype_modules.clockify.clockify_api import ClockifyAPI class ClockifyStart(LauncherAction): diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py b/server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifySync.py similarity index 97% rename from client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py rename to server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifySync.py index 5388f47c98..a32f2a8082 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py +++ b/server_addon/clockify/client/ayon_clockify/launcher_actions/ClockifySync.py @@ -1,6 +1,6 @@ import ayon_api -from openpype_modules.clockify.clockify_api import ClockifyAPI +from ayon_clockify.clockify_api import ClockifyAPI from ayon_core.pipeline import LauncherAction diff --git a/server_addon/clockify/client/ayon_clockify/version.py b/server_addon/clockify/client/ayon_clockify/version.py new file mode 100644 index 0000000000..0e6e40cb7d --- /dev/null +++ b/server_addon/clockify/client/ayon_clockify/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +"""Package declaring AYON addon 'clockify' version.""" +__version__ = "0.2.0" diff --git a/client/ayon_core/modules/clockify/widgets.py b/server_addon/clockify/client/ayon_clockify/widgets.py similarity index 100% rename from client/ayon_core/modules/clockify/widgets.py rename to server_addon/clockify/client/ayon_clockify/widgets.py diff --git a/server_addon/clockify/package.py b/server_addon/clockify/package.py index bcf9425b3f..61e0685191 100644 --- a/server_addon/clockify/package.py +++ b/server_addon/clockify/package.py @@ -1,3 +1,9 @@ name = "clockify" title = "Clockify" -version = "0.1.1" +version = "0.2.0" +client_dir = "ayon_clockify" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {} diff --git a/server_addon/create_ayon_addons.py b/server_addon/create_ayon_addons.py index f0a36d4740..749077d2a8 100644 --- a/server_addon/create_ayon_addons.py +++ b/server_addon/create_ayon_addons.py @@ -47,7 +47,7 @@ plugin_for = ["ayon_server"] """ CLIENT_VERSION_CONTENT = '''# -*- coding: utf-8 -*- -"""Package declaring AYON core addon version.""" +"""Package declaring AYON addon '{}' version.""" __version__ = "{}" ''' @@ -183,6 +183,7 @@ def create_addon_zip( def prepare_client_code( + addon_name: str, addon_dir: Path, addon_output_dir: Path, addon_version: str @@ -211,7 +212,9 @@ def prepare_client_code( version_path = subpath / "version.py" if version_path.exists(): with open(version_path, "w") as stream: - stream.write(CLIENT_VERSION_CONTENT.format(addon_version)) + stream.write( + CLIENT_VERSION_CONTENT.format(addon_name, addon_version) + ) zip_filepath = private_dir / "client.zip" with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf: @@ -262,7 +265,9 @@ def create_addon_package( server_dir, addon_output_dir / "server", dirs_exist_ok=True ) - prepare_client_code(addon_dir, addon_output_dir, addon_version) + prepare_client_code( + package.name, addon_dir, addon_output_dir, addon_version + ) if create_zip: create_addon_zip( diff --git a/server_addon/deadline/package.py b/server_addon/deadline/package.py index 25ba1c1166..e26734c813 100644 --- a/server_addon/deadline/package.py +++ b/server_addon/deadline/package.py @@ -1,3 +1,3 @@ name = "deadline" title = "Deadline" -version = "0.1.11" +version = "0.1.12" diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py index 5d42b9b1ef..47ad72a86f 100644 --- a/server_addon/deadline/server/settings/main.py +++ b/server_addon/deadline/server/settings/main.py @@ -38,10 +38,9 @@ class ServerItemSubmodel(BaseSettingsModel): name: str = SettingsField(title="Name") value: str = SettingsField(title="Url") require_authentication: bool = SettingsField( - False, - title="Require authentication") - ssl: bool = SettingsField(False, - title="SSL") + False, title="Require authentication") + not_verify_ssl: bool = SettingsField( + False, title="Don't verify SSL") class DeadlineSettings(BaseSettingsModel): @@ -78,7 +77,7 @@ DEFAULT_VALUES = { "name": "default", "value": "http://127.0.0.1:8082", "require_authentication": False, - "ssl": False + "not_verify_ssl": False } ], "deadline_server": "default", diff --git a/server_addon/harmony/package.py b/server_addon/harmony/package.py index 83e88e7d57..00824cedef 100644 --- a/server_addon/harmony/package.py +++ b/server_addon/harmony/package.py @@ -1,3 +1,3 @@ name = "harmony" title = "Harmony" -version = "0.1.2" +version = "0.1.3" diff --git a/server_addon/harmony/server/settings/main.py b/server_addon/harmony/server/settings/main.py index 9c780b63c2..8a72c966d8 100644 --- a/server_addon/harmony/server/settings/main.py +++ b/server_addon/harmony/server/settings/main.py @@ -45,11 +45,6 @@ DEFAULT_HARMONY_SETTING = { "optional": True, "active": True }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, "ValidateSceneSettings": { "enabled": True, "optional": True, diff --git a/server_addon/harmony/server/settings/publish_plugins.py b/server_addon/harmony/server/settings/publish_plugins.py index c9e7c515e4..2d976389f6 100644 --- a/server_addon/harmony/server/settings/publish_plugins.py +++ b/server_addon/harmony/server/settings/publish_plugins.py @@ -18,14 +18,6 @@ class ValidateAudioPlugin(BaseSettingsModel): active: bool = SettingsField(True, title="Active") -class ValidateContainersPlugin(BaseSettingsModel): - """Check if loaded container is scene are latest versions.""" - _isGroup = True - enabled: bool = True - optional: bool = SettingsField(False, title="Optional") - active: bool = SettingsField(True, title="Active") - - class ValidateSceneSettingsPlugin(BaseSettingsModel): """Validate if FrameStart, FrameEnd and Resolution match shot data in DB. Use regular expressions to limit validations only on particular asset @@ -63,11 +55,6 @@ class HarmonyPublishPlugins(BaseSettingsModel): default_factory=ValidateAudioPlugin, ) - ValidateContainers: ValidateContainersPlugin = SettingsField( - title="Validate Containers", - default_factory=ValidateContainersPlugin, - ) - ValidateSceneSettings: ValidateSceneSettingsPlugin = SettingsField( title="Validate Scene Settings", default_factory=ValidateSceneSettingsPlugin, diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 4e441c76ae..06b034da38 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,3 +1,3 @@ name = "houdini" title = "Houdini" -version = "0.2.13" +version = "0.2.15" diff --git a/server_addon/houdini/server/settings/create.py b/server_addon/houdini/server/settings/create.py index 203ca4f9d6..cd1e110c23 100644 --- a/server_addon/houdini/server/settings/create.py +++ b/server_addon/houdini/server/settings/create.py @@ -57,6 +57,9 @@ class CreatePluginsModel(BaseSettingsModel): CreateMantraROP: CreatorModel = SettingsField( default_factory=CreatorModel, title="Create Mantra ROP") + CreateModel: CreatorModel = SettingsField( + default_factory=CreatorModel, + title="Create Model") CreatePointCache: CreatorModel = SettingsField( default_factory=CreatorModel, title="Create PointCache (Abc)") @@ -124,6 +127,10 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "enabled": True, "default_variants": ["Main"] }, + "CreateModel": { + "enabled": True, + "default_variants": ["Main"] + }, "CreatePointCache": { "enabled": True, "default_variants": ["Main"] diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 8e0e7f7795..4a0c022f23 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -1,4 +1,7 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField +) # Publish Plugins @@ -20,6 +23,27 @@ class CollectChunkSizeModel(BaseSettingsModel): title="Frames Per Task") +class AOVFilterSubmodel(BaseSettingsModel): + """You should use the same host name you are using for Houdini.""" + host_name: str = SettingsField("", title="Houdini Host name") + value: list[str] = SettingsField( + default_factory=list, + title="AOV regex" + ) + +class CollectLocalRenderInstancesModel(BaseSettingsModel): + + use_deadline_aov_filter: bool = SettingsField( + False, + title="Use Deadline AOV Filter" + ) + + aov_filter: AOVFilterSubmodel = SettingsField( + default_factory=AOVFilterSubmodel, + title="Reviewable products filter" + ) + + class ValidateWorkfilePathsModel(BaseSettingsModel): enabled: bool = SettingsField(title="Enabled") optional: bool = SettingsField(title="Optional") @@ -49,10 +73,10 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=CollectChunkSizeModel, title="Collect Chunk Size." ) - ValidateContainers: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Latest Containers.", - section="Validators") + CollectLocalRenderInstances: CollectLocalRenderInstancesModel = SettingsField( + default_factory=CollectLocalRenderInstancesModel, + title="Collect Local Render Instances." + ) ValidateInstanceInContextHoudini: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Validate Instance is in same Context.") @@ -82,10 +106,14 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "optional": True, "chunk_size": 999999 }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True + "CollectLocalRenderInstances": { + "use_deadline_aov_filter": False, + "aov_filter" : { + "host_name": "houdini", + "value": [ + ".*([Bb]eauty).*" + ] + } }, "ValidateInstanceInContextHoudini": { "enabled": True, diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index fe3e3039f5..4537c23eaa 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,3 +1,3 @@ name = "maya" title = "Maya" -version = "0.1.18" +version = "0.1.20" diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 20523b2ca9..9c552e17fa 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -634,10 +634,6 @@ class PublishersModel(BaseSettingsModel): title="Validate Instance In Context", section="Validators" ) - ValidateContainers: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Containers" - ) ValidateFrameRange: ValidateFrameRangeModel = SettingsField( default_factory=ValidateFrameRangeModel, title="Validate Frame Range" @@ -917,10 +913,6 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Rig Controllers", ) - ValidateAnimatedReferenceRig: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Animated Reference Rig", - ) ValidateAnimationContent: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Validate Animation Content", @@ -1063,11 +1055,6 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": True, "active": True }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, "ValidateFrameRange": { "enabled": True, "optional": True, @@ -1447,11 +1434,6 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": True, "active": True }, - "ValidateAnimatedReferenceRig": { - "enabled": True, - "optional": False, - "active": True - }, "ValidateAnimationContent": { "enabled": True, "optional": False, diff --git a/client/ayon_core/hosts/nuke/__init__.py b/server_addon/nuke/client/ayon_nuke/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/__init__.py rename to server_addon/nuke/client/ayon_nuke/__init__.py diff --git a/client/ayon_core/hosts/nuke/addon.py b/server_addon/nuke/client/ayon_nuke/addon.py similarity index 100% rename from client/ayon_core/hosts/nuke/addon.py rename to server_addon/nuke/client/ayon_nuke/addon.py diff --git a/client/ayon_core/hosts/nuke/api/__init__.py b/server_addon/nuke/client/ayon_nuke/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/__init__.py rename to server_addon/nuke/client/ayon_nuke/api/__init__.py diff --git a/client/ayon_core/hosts/nuke/api/actions.py b/server_addon/nuke/client/ayon_nuke/api/actions.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/actions.py rename to server_addon/nuke/client/ayon_nuke/api/actions.py diff --git a/client/ayon_core/hosts/nuke/api/command.py b/server_addon/nuke/client/ayon_nuke/api/command.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/command.py rename to server_addon/nuke/client/ayon_nuke/api/command.py diff --git a/client/ayon_core/hosts/nuke/api/constants.py b/server_addon/nuke/client/ayon_nuke/api/constants.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/constants.py rename to server_addon/nuke/client/ayon_nuke/api/constants.py diff --git a/client/ayon_core/hosts/nuke/api/gizmo_menu.py b/server_addon/nuke/client/ayon_nuke/api/gizmo_menu.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/gizmo_menu.py rename to server_addon/nuke/client/ayon_nuke/api/gizmo_menu.py diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/server_addon/nuke/client/ayon_nuke/api/lib.py similarity index 98% rename from client/ayon_core/hosts/nuke/api/lib.py rename to server_addon/nuke/client/ayon_nuke/api/lib.py index e3505a16f2..09dab4687a 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/server_addon/nuke/client/ayon_nuke/api/lib.py @@ -43,7 +43,9 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.context_tools import ( get_current_context_custom_workfile_template ) -from ayon_core.pipeline.colorspace import get_imageio_config +from ayon_core.pipeline.colorspace import ( + get_current_context_imageio_config_preset +) from ayon_core.pipeline.workfile import BuildWorkfile from . import gizmo_menu from .constants import ASSIST @@ -352,7 +354,7 @@ def imprint(node, data, tab=None): Examples: ``` import nuke - from ayon_core.hosts.nuke.api import lib + from ayon_nuke.api import lib node = nuke.createNode("NoOp") data = { @@ -417,7 +419,7 @@ def add_publish_knob(node): return node -@deprecated("ayon_core.hosts.nuke.api.lib.set_node_data") +@deprecated("ayon_nuke.api.lib.set_node_data") def set_avalon_knob_data(node, data=None, prefix="avalon:"): """[DEPRECATED] Sets data into nodes's avalon knob @@ -483,7 +485,7 @@ def set_avalon_knob_data(node, data=None, prefix="avalon:"): return node -@deprecated("ayon_core.hosts.nuke.api.lib.get_node_data") +@deprecated("ayon_nuke.api.lib.get_node_data") def get_avalon_knob_data(node, prefix="avalon:", create=True): """[DEPRECATED] Gets a data from nodes's avalon knob @@ -1022,6 +1024,18 @@ def script_name(): return nuke.root().knob("name").value() +def add_button_render_on_farm(node): + name = "renderOnFarm" + label = "Render On Farm" + value = ( + "from ayon_nuke.api.utils import submit_render_on_farm;" + "submit_render_on_farm(nuke.thisNode())" + ) + knob = nuke.PyScript_Knob(name, label, value) + knob.clearFlag(nuke.STARTLINE) + node.addKnob(knob) + + def add_button_write_to_read(node): name = "createReadNode" label = "Read From Rendered" @@ -1144,6 +1158,17 @@ def create_write_node( Return: node (obj): group node with avalon data as Knobs ''' + # Ensure name does not contain any invalid characters. + special_chars = re.escape("!@#$%^&*()=[]{}|\\;',.<>/?~+-") + special_chars_regex = re.compile(f"[{special_chars}]") + found_special_characters = list(special_chars_regex.findall(name)) + + msg = ( + f"Special characters found in name \"{name}\": " + f"{' '.join(found_special_characters)}" + ) + assert not found_special_characters, msg + prenodes = prenodes or [] # filtering variables @@ -1268,6 +1293,10 @@ def create_write_node( link.setFlag(0x1000) GN.addKnob(link) + # Adding render farm submission button. + if data.get("render_on_farm", False): + add_button_render_on_farm(GN) + # adding write to read button add_button_write_to_read(GN) @@ -1552,10 +1581,7 @@ class WorkfileSettings(object): imageio_host (dict): host colorspace configurations ''' - config_data = get_imageio_config( - project_name=get_current_project_name(), - host_name="nuke" - ) + config_data = get_current_context_imageio_config_preset() workfile_settings = imageio_host["workfile"] color_management = workfile_settings["color_management"] @@ -2443,7 +2469,7 @@ def _launch_workfile_app(): host_tools.show_workfiles(parent=None, on_top=True) -@deprecated("ayon_core.hosts.nuke.api.lib.start_workfile_template_builder") +@deprecated("ayon_nuke.api.lib.start_workfile_template_builder") def process_workfile_builder(): """ [DEPRECATED] Process workfile builder on nuke start diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/server_addon/nuke/client/ayon_nuke/api/pipeline.py similarity index 98% rename from client/ayon_core/hosts/nuke/api/pipeline.py rename to server_addon/nuke/client/ayon_nuke/api/pipeline.py index d35a2e89e0..ad8e17b1f6 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/server_addon/nuke/client/ayon_nuke/api/pipeline.py @@ -28,7 +28,7 @@ from ayon_core.pipeline import ( ) from ayon_core.pipeline.workfile import BuildWorkfile from ayon_core.tools.utils import host_tools -from ayon_core.hosts.nuke import NUKE_ROOT_DIR +from ayon_nuke import NUKE_ROOT_DIR from ayon_core.tools.workfile_template_build import open_template_ui from .lib import ( @@ -188,10 +188,10 @@ def reload_config(): """ for module in ( - "ayon_core.hosts.nuke.api.actions", - "ayon_core.hosts.nuke.api.menu", - "ayon_core.hosts.nuke.api.plugin", - "ayon_core.hosts.nuke.api.lib", + "ayon_nuke.api.actions", + "ayon_nuke.api.menu", + "ayon_nuke.api.plugin", + "ayon_nuke.api.lib", ): log.info("Reloading module: {}...".format(module)) diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/server_addon/nuke/client/ayon_nuke/api/plugin.py similarity index 97% rename from client/ayon_core/hosts/nuke/api/plugin.py rename to server_addon/nuke/client/ayon_nuke/api/plugin.py index fb56dec833..4f05cd41b9 100644 --- a/client/ayon_core/hosts/nuke/api/plugin.py +++ b/server_addon/nuke/client/ayon_nuke/api/plugin.py @@ -572,8 +572,11 @@ class ExporterReview(object): self.fhead = self.fhead.replace("#", "")[:-1] def get_representation_data( - self, tags=None, range=False, - custom_tags=None, colorspace=None + self, + tags=None, + range=False, + custom_tags=None, + colorspace=None, ): """ Add representation data to self.data @@ -584,6 +587,8 @@ class ExporterReview(object): Defaults to False. custom_tags (list[str], optional): user inputted custom tags. Defaults to None. + colorspace (str, optional): colorspace name. + Defaults to None. """ add_tags = tags or [] repre = { @@ -591,7 +596,13 @@ class ExporterReview(object): "ext": self.ext, "files": self.file, "stagingDir": self.staging_dir, - "tags": [self.name.replace("_", "-")] + add_tags + "tags": [self.name.replace("_", "-")] + add_tags, + "data": { + # making sure that once intermediate file is published + # as representation, we will be able to then identify it + # from representation.data.isIntermediate + "isIntermediate": True + }, } if custom_tags: @@ -778,6 +789,7 @@ class ExporterReviewMov(ExporterReview): # deal with now lut defined in viewer lut self.viewer_lut_raw = klass.viewer_lut_raw self.write_colorspace = instance.data["colorspace"] + self.color_channels = instance.data["color_channels"] self.name = name or "baked" self.ext = ext or "mov" @@ -834,9 +846,10 @@ class ExporterReviewMov(ExporterReview): self.log.info("Nodes exported...") return path - def generate_mov(self, farm=False, **kwargs): + def generate_mov(self, farm=False, delete=True, **kwargs): # colorspace data - colorspace = None + colorspace = self.write_colorspace + # get colorspace settings # get colorspace data from context config_data, _ = get_colorspace_settings_from_publish_context( @@ -947,6 +960,8 @@ class ExporterReviewMov(ExporterReview): self.log.debug("Path: {}".format(self.path)) write_node["file"].setValue(str(self.path)) write_node["file_type"].setValue(str(self.ext)) + write_node["channels"].setValue(str(self.color_channels)) + # Knobs `meta_codec` and `mov64_codec` are not available on centos. # TODO shouldn't this come from settings on outputs? try: @@ -987,11 +1002,16 @@ class ExporterReviewMov(ExporterReview): self.render(write_node.name()) # ---------- generate representation data + tags = ["review", "need_thumbnail"] + + if delete: + tags.append("delete") + self.get_representation_data( - tags=["review", "need_thumbnail", "delete"] + add_tags, + tags=tags + add_tags, custom_tags=add_custom_tags, range=True, - colorspace=colorspace + colorspace=colorspace, ) self.log.debug("Representation... `{}`".format(self.data)) @@ -1030,7 +1050,7 @@ def convert_to_valid_instaces(): } return mapping[product_type] - from ayon_core.hosts.nuke.api import workio + from ayon_nuke.api import workio task_name = get_current_task_name() diff --git a/client/ayon_core/hosts/nuke/api/utils.py b/server_addon/nuke/client/ayon_nuke/api/utils.py similarity index 63% rename from client/ayon_core/hosts/nuke/api/utils.py rename to server_addon/nuke/client/ayon_nuke/api/utils.py index 1bfc1919fa..646bb0ece1 100644 --- a/client/ayon_core/hosts/nuke/api/utils.py +++ b/server_addon/nuke/client/ayon_nuke/api/utils.py @@ -3,9 +3,15 @@ import re import nuke -from ayon_core import resources +import pyblish.util +import pyblish.api from qtpy import QtWidgets +from ayon_core import resources +from ayon_core.pipeline import registered_host +from ayon_core.tools.utils import show_message_dialog +from ayon_core.pipeline.create import CreateContext + def set_context_favorites(favorites=None): """ Adding favorite folders to nuke's browser @@ -142,3 +148,77 @@ def is_headless(): bool: headless """ return QtWidgets.QApplication.instance() is None + + +def submit_render_on_farm(node): + # Ensure code is executed in root context. + if nuke.root() == nuke.thisNode(): + _submit_render_on_farm(node) + else: + # If not in root context, move to the root context and then execute the + # code. + with nuke.root(): + _submit_render_on_farm(node) + + +def _submit_render_on_farm(node): + """Render on farm submission + + This function prepares the context for farm submission, validates it, + extracts relevant data, copies the current workfile to a timestamped copy, + and submits the job to the farm. + + Args: + node (Node): The node for which the farm submission is being made. + """ + + host = registered_host() + create_context = CreateContext(host) + + # Ensure CreateInstance is enabled. + for instance in create_context.instances: + if node.name() != instance.transient_data["node"].name(): + continue + + instance.data["active"] = True + + context = pyblish.api.Context() + context.data["create_context"] = create_context + # Used in pyblish plugin to determine which instance to publish. + context.data["node_name"] = node.name() + # Used in pyblish plugins to determine whether to run or not. + context.data["render_on_farm"] = True + + # Since we need to bypass version validation and incrementing, we need to + # remove the plugins from the list that are responsible for these tasks. + plugins = pyblish.api.discover() + blacklist = ["IncrementScriptVersion", "ValidateVersion"] + plugins = [ + plugin + for plugin in plugins + if plugin.__name__ not in blacklist + ] + + context = pyblish.util.publish(context, plugins=plugins) + + error_message = "" + success = True + for result in context.data["results"]: + if result["success"]: + continue + + success = False + + err = result["error"] + error_message += "\n" + error_message += err.formatted_traceback + + if not success: + show_message_dialog( + "Publish Errors", error_message, level="critical" + ) + return + + show_message_dialog( + "Submission Successful", "Submission to the farm was successful." + ) diff --git a/client/ayon_core/hosts/nuke/api/workfile_template_builder.py b/server_addon/nuke/client/ayon_nuke/api/workfile_template_builder.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/workfile_template_builder.py rename to server_addon/nuke/client/ayon_nuke/api/workfile_template_builder.py diff --git a/client/ayon_core/hosts/nuke/api/workio.py b/server_addon/nuke/client/ayon_nuke/api/workio.py similarity index 100% rename from client/ayon_core/hosts/nuke/api/workio.py rename to server_addon/nuke/client/ayon_nuke/api/workio.py diff --git a/client/ayon_core/hosts/nuke/hooks/pre_nukeassist_setup.py b/server_addon/nuke/client/ayon_nuke/hooks/pre_nukeassist_setup.py similarity index 100% rename from client/ayon_core/hosts/nuke/hooks/pre_nukeassist_setup.py rename to server_addon/nuke/client/ayon_nuke/hooks/pre_nukeassist_setup.py diff --git a/client/ayon_core/hosts/nuke/plugins/__init__.py b/server_addon/nuke/client/ayon_nuke/plugins/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/__init__.py rename to server_addon/nuke/client/ayon_nuke/plugins/__init__.py diff --git a/client/ayon_core/hosts/nuke/plugins/create/__init__.py b/server_addon/nuke/client/ayon_nuke/plugins/create/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/create/__init__.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/__init__.py diff --git a/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py b/server_addon/nuke/client/ayon_nuke/plugins/create/convert_legacy.py similarity index 93% rename from client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/convert_legacy.py index 8fb8abfbbf..65e719d15b 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/convert_legacy.py @@ -1,12 +1,12 @@ from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( INSTANCE_DATA_KNOB, get_node_data, get_avalon_knob_data, NODE_TAB_NAME, ) -from ayon_core.hosts.nuke.api.plugin import convert_to_valid_instaces +from ayon_nuke.api.plugin import convert_to_valid_instaces import nuke diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_backdrop.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_backdrop.py similarity index 95% rename from client/ayon_core/hosts/nuke/plugins/create/create_backdrop.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_backdrop.py index cefd9501ec..f97b9efeb6 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_backdrop.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_backdrop.py @@ -1,6 +1,6 @@ from nukescripts import autoBackdrop -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( NukeCreator, maintained_selection, select_nodes @@ -10,6 +10,8 @@ from ayon_core.hosts.nuke.api import ( class CreateBackdrop(NukeCreator): """Add Publishable Backdrop""" + settings_category = "nuke" + identifier = "create_backdrop" label = "Nukenodes (backdrop)" product_type = "nukenodes" diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_camera.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_camera.py similarity index 95% rename from client/ayon_core/hosts/nuke/plugins/create/create_camera.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_camera.py index 764de84dcf..69e5b9c676 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_camera.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_camera.py @@ -1,10 +1,10 @@ import nuke -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( create_camera_node_by_version ) @@ -12,6 +12,8 @@ from ayon_core.hosts.nuke.api.lib import ( class CreateCamera(NukeCreator): """Add Publishable Camera""" + settings_category = "nuke" + identifier = "create_camera" label = "Camera (3d)" product_type = "camera" diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_gizmo.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_gizmo.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/create/create_gizmo.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_gizmo.py index ccc6aa13bd..6be7cd58db 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_gizmo.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_gizmo.py @@ -1,5 +1,5 @@ import nuke -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection @@ -9,6 +9,8 @@ from ayon_core.hosts.nuke.api import ( class CreateGizmo(NukeCreator): """Add Publishable Group as gizmo""" + settings_category = "nuke" + identifier = "create_gizmo" label = "Gizmo (group)" product_type = "gizmo" diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_model.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_model.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/create/create_model.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_model.py index 507b7a1b57..b7d7b740c2 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_model.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_model.py @@ -1,5 +1,5 @@ import nuke -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( NukeCreator, NukeCreatorError, maintained_selection @@ -9,6 +9,8 @@ from ayon_core.hosts.nuke.api import ( class CreateModel(NukeCreator): """Add Publishable Camera""" + settings_category = "nuke" + identifier = "create_model" label = "Model (3d)" product_type = "model" diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_source.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_source.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/create/create_source.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_source.py index ac6b8f694b..1579cebb1d 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_source.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_source.py @@ -1,7 +1,7 @@ import nuke import six import sys -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( INSTANCE_DATA_KNOB, NukeCreator, NukeCreatorError, @@ -15,6 +15,8 @@ from ayon_core.pipeline import ( class CreateSource(NukeCreator): """Add Publishable Read with source""" + settings_category = "nuke" + identifier = "create_source" label = "Source (read)" product_type = "source" diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_image.py similarity index 91% rename from client/ayon_core/hosts/nuke/plugins/create/create_write_image.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_write_image.py index 770726e34f..2268817e76 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_image.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_image.py @@ -11,11 +11,14 @@ from ayon_core.lib import ( UISeparatorDef, EnumDef ) -from ayon_core.hosts.nuke import api as napi -from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs +from ayon_nuke import api as napi +from ayon_nuke.api.plugin import exposed_write_knobs class CreateWriteImage(napi.NukeWriteCreator): + + settings_category = "nuke" + identifier = "create_write_image" label = "Image (write)" product_type = "image" @@ -65,12 +68,16 @@ class CreateWriteImage(napi.NukeWriteCreator): ) def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"]["CreateWriteImage"] # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_prerender.py similarity index 90% rename from client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_write_prerender.py index 96ac2fac9c..014e91e81c 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_prerender.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_prerender.py @@ -8,11 +8,14 @@ from ayon_core.pipeline import ( from ayon_core.lib import ( BoolDef ) -from ayon_core.hosts.nuke import api as napi -from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs +from ayon_nuke import api as napi +from ayon_nuke.api.plugin import exposed_write_knobs class CreateWritePrerender(napi.NukeWriteCreator): + + settings_category = "nuke" + identifier = "create_write_prerender" label = "Prerender (write)" product_type = "prerender" @@ -46,11 +49,17 @@ class CreateWritePrerender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"] + settings = settings["CreateWritePrerender"] + # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_render.py similarity index 89% rename from client/ayon_core/hosts/nuke/plugins/create/create_write_render.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/create_write_render.py index 24bddb3d26..bed081c882 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/create_write_render.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/create_write_render.py @@ -8,11 +8,14 @@ from ayon_core.pipeline import ( from ayon_core.lib import ( BoolDef ) -from ayon_core.hosts.nuke import api as napi -from ayon_core.hosts.nuke.api.plugin import exposed_write_knobs +from ayon_nuke import api as napi +from ayon_nuke.api.plugin import exposed_write_knobs class CreateWriteRender(napi.NukeWriteCreator): + + settings_category = "nuke" + identifier = "create_write_render" label = "Render (write)" product_type = "render" @@ -40,11 +43,16 @@ class CreateWriteRender(napi.NukeWriteCreator): return attr_defs def create_instance_node(self, product_name, instance_data): + settings = self.project_settings["nuke"]["create"]["CreateWriteRender"] + # add fpath_template write_data = { "creator": self.__class__.__name__, "productName": product_name, - "fpath_template": self.temp_rendering_path_template + "fpath_template": self.temp_rendering_path_template, + "render_on_farm": ( + "render_on_farm" in settings["instance_attributes"] + ) } write_data.update(instance_data) diff --git a/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py b/server_addon/nuke/client/ayon_nuke/plugins/create/workfile_creator.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py rename to server_addon/nuke/client/ayon_nuke/plugins/create/workfile_creator.py index b9d83a2b48..463d898224 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/create/workfile_creator.py @@ -1,11 +1,11 @@ import ayon_api -import ayon_core.hosts.nuke.api as api +import ayon_nuke.api as api from ayon_core.pipeline import ( AutoCreator, CreatedInstance, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( INSTANCE_DATA_KNOB, set_node_data ) @@ -13,6 +13,9 @@ import nuke class WorkfileCreator(AutoCreator): + + settings_category = "nuke" + identifier = "workfile" product_type = "workfile" diff --git a/client/ayon_core/hosts/nuke/plugins/inventory/repair_old_loaders.py b/server_addon/nuke/client/ayon_nuke/plugins/inventory/repair_old_loaders.py similarity index 94% rename from client/ayon_core/hosts/nuke/plugins/inventory/repair_old_loaders.py rename to server_addon/nuke/client/ayon_nuke/plugins/inventory/repair_old_loaders.py index 7bb5c8ef20..11d65d4b8c 100644 --- a/client/ayon_core/hosts/nuke/plugins/inventory/repair_old_loaders.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/inventory/repair_old_loaders.py @@ -1,6 +1,6 @@ from ayon_core.lib import Logger from ayon_core.pipeline import InventoryAction -from ayon_core.hosts.nuke.api.lib import set_avalon_knob_data +from ayon_nuke.api.lib import set_avalon_knob_data class RepairOldLoaders(InventoryAction): diff --git a/client/ayon_core/hosts/nuke/plugins/inventory/select_containers.py b/server_addon/nuke/client/ayon_nuke/plugins/inventory/select_containers.py similarity index 88% rename from client/ayon_core/hosts/nuke/plugins/inventory/select_containers.py rename to server_addon/nuke/client/ayon_nuke/plugins/inventory/select_containers.py index 2fa9c06984..f67c8c16e9 100644 --- a/client/ayon_core/hosts/nuke/plugins/inventory/select_containers.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/inventory/select_containers.py @@ -1,5 +1,5 @@ from ayon_core.pipeline import InventoryAction -from ayon_core.hosts.nuke.api.command import viewer_update_and_undo_stop +from ayon_nuke.api.command import viewer_update_and_undo_stop class SelectContainers(InventoryAction): diff --git a/client/ayon_core/hosts/nuke/plugins/load/actions.py b/server_addon/nuke/client/ayon_nuke/plugins/load/actions.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/load/actions.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/actions.py index 53cb03087b..a4e2b156a3 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/actions.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/actions.py @@ -4,7 +4,7 @@ from ayon_core.lib import Logger from ayon_core.pipeline import load -from ayon_core.hosts.nuke.api import lib +from ayon_nuke.api import lib log = Logger.get_logger(__name__) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_backdrop.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_backdrop.py index 7d823919dc..054a56d041 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_backdrop.py @@ -6,7 +6,7 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( find_free_space_to_paste_nodes, maintained_selection, reset_selection, @@ -14,8 +14,8 @@ from ayon_core.hosts.nuke.api.lib import ( get_avalon_knob_data, set_avalon_knob_data ) -from ayon_core.hosts.nuke.api.command import viewer_update_and_undo_stop -from ayon_core.hosts.nuke.api import containerise, update_container +from ayon_nuke.api.command import viewer_update_and_undo_stop +from ayon_nuke.api import containerise, update_container class LoadBackdropNodes(load.LoaderPlugin): @@ -25,6 +25,8 @@ class LoadBackdropNodes(load.LoaderPlugin): representations = {"*"} extensions = {"nk"} + settings_category = "nuke" + label = "Import Nuke Nodes" order = 0 icon = "eye" @@ -62,7 +64,7 @@ class LoadBackdropNodes(load.LoaderPlugin): } # add attributes from the version to imprint to metadata knob - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # getting file path @@ -206,7 +208,7 @@ class LoadBackdropNodes(load.LoaderPlugin): "colorspaceInput": colorspace, } - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # adding nodes to node graph diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_camera_abc.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_camera_abc.py index 14c54c3adc..3930cf52fa 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_camera_abc.py @@ -5,12 +5,12 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( maintained_selection ) @@ -24,6 +24,8 @@ class AlembicCameraLoader(load.LoaderPlugin): representations = {"*"} extensions = {"abc"} + settings_category = "nuke" + label = "Load Alembic Camera" icon = "camera" color = "orange" @@ -48,7 +50,7 @@ class AlembicCameraLoader(load.LoaderPlugin): "frameEnd": last, "version": version_entity["version"], } - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # getting file path @@ -123,7 +125,7 @@ class AlembicCameraLoader(load.LoaderPlugin): } # add attributes from the version to imprint to metadata knob - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # getting file path diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_clip.py similarity index 90% rename from client/ayon_core/hosts/nuke/plugins/load/load_clip.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_clip.py index df8f2ab018..d1e38eea6b 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_clip.py @@ -9,13 +9,14 @@ from ayon_core.pipeline import ( get_representation_path, ) from ayon_core.pipeline.colorspace import ( - get_imageio_file_rules_colorspace_from_filepath + get_imageio_file_rules_colorspace_from_filepath, + get_current_context_imageio_config_preset, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( get_imageio_input_colorspace, maintained_selection ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop, @@ -25,7 +26,7 @@ from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) -from ayon_core.hosts.nuke.api import plugin +from ayon_nuke.api import plugin class LoadClip(plugin.NukeLoader): @@ -47,6 +48,8 @@ class LoadClip(plugin.NukeLoader): ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) ) + settings_category = "nuke" + label = "Load Clip" order = -20 icon = "file-video-o" @@ -60,7 +63,8 @@ class LoadClip(plugin.NukeLoader): # option gui options_defaults = { "start_at_workfile": True, - "add_retime": True + "add_retime": True, + "deep_exr": False } node_name_template = "{class_name}_{ext}" @@ -77,6 +81,11 @@ class LoadClip(plugin.NukeLoader): "add_retime", help="Load with retime", default=cls.options_defaults["add_retime"] + ), + qargparse.Boolean( + "deep_exr", + help="Read with deep exr", + default=cls.options_defaults["deep_exr"] ) ] @@ -112,6 +121,9 @@ class LoadClip(plugin.NukeLoader): add_retime = options.get( "add_retime", self.options_defaults["add_retime"]) + deep_exr = options.get( + "deep_exr", self.options_defaults["deep_exr"]) + repre_id = repre_entity["id"] self.log.debug( @@ -152,13 +164,21 @@ class LoadClip(plugin.NukeLoader): return read_name = self._get_node_name(context) - - # Create the Loader with the filename path set - read_node = nuke.createNode( - "Read", - "name {}".format(read_name), - inpanel=False - ) + read_node = None + if deep_exr: + # Create the Loader with the filename path set + read_node = nuke.createNode( + "DeepRead", + "name {}".format(read_name), + inpanel=False + ) + else: + # Create the Loader with the filename path set + read_node = nuke.createNode( + "Read", + "name {}".format(read_name), + inpanel=False + ) # get colorspace colorspace = ( @@ -170,14 +190,14 @@ class LoadClip(plugin.NukeLoader): # we will switch off undo-ing with viewer_update_and_undo_stop(): read_node["file"].setValue(filepath) - - self.set_colorspace_to_node( - read_node, - filepath, - project_name, - version_entity, - repre_entity - ) + if read_node.Class() == "Read": + self.set_colorspace_to_node( + read_node, + filepath, + project_name, + version_entity, + repre_entity + ) self._set_range_to_node( read_node, first, last, start_at_workfile, slate_frames @@ -197,7 +217,6 @@ class LoadClip(plugin.NukeLoader): "frameStart", "frameEnd", "source", - "author", "fps", "handleStart", "handleEnd", @@ -328,13 +347,14 @@ class LoadClip(plugin.NukeLoader): # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): - self.set_colorspace_to_node( - read_node, - filepath, - project_name, - version_entity, - repre_entity - ) + if read_node.Class() == "Read": + self.set_colorspace_to_node( + read_node, + filepath, + project_name, + version_entity, + repre_entity + ) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -347,8 +367,7 @@ class LoadClip(plugin.NukeLoader): "source": version_attributes.get("source"), "handleStart": str(self.handle_start), "handleEnd": str(self.handle_end), - "fps": str(version_attributes.get("fps")), - "author": version_attributes.get("author") + "fps": str(version_attributes.get("fps")) } last_version_entity = ayon_api.get_last_version_by_product_id( @@ -547,9 +566,10 @@ class LoadClip(plugin.NukeLoader): f"Colorspace from representation colorspaceData: {colorspace}" ) + config_data = get_current_context_imageio_config_preset() # check if any filerules are not applicable new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa - filepath, "nuke", project_name + filepath, "nuke", project_name, config_data=config_data ) self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}") diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_effects.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_effects.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/load/load_effects.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_effects.py index a87c81295a..e923a02424 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_effects.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_effects.py @@ -8,7 +8,7 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -22,13 +22,14 @@ class LoadEffects(load.LoaderPlugin): representations = {"*"} extensions = {"json"} + settings_category = "nuke" + label = "Load Effects - nodes" order = 0 icon = "cc" color = "white" ignore_attr = ["useLifetime"] - def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node @@ -69,7 +70,6 @@ class LoadEffects(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] @@ -189,7 +189,6 @@ class LoadEffects(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps", ]: data_imprint[k] = version_attributes[k] diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_effects_ip.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_effects_ip.py index 8fa1347598..ce7e7debeb 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_effects_ip.py @@ -8,8 +8,8 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api import lib -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import lib +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -23,6 +23,8 @@ class LoadEffectsInputProcess(load.LoaderPlugin): representations = {"*"} extensions = {"json"} + settings_category = "nuke" + label = "Load Effects - Input Process" order = 0 icon = "eye" @@ -69,7 +71,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] @@ -192,7 +193,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo.py index 95f85bacfc..1c91af0c1c 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo.py @@ -5,13 +5,13 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( maintained_selection, get_avalon_knob_data, set_avalon_knob_data, swap_node_with_dependency, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -25,6 +25,8 @@ class LoadGizmo(load.LoaderPlugin): representations = {"*"} extensions = {"nk"} + settings_category = "nuke" + label = "Load Gizmo" order = 0 icon = "dropbox" @@ -71,7 +73,6 @@ class LoadGizmo(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] @@ -139,7 +140,6 @@ class LoadGizmo(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo_ip.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo_ip.py index 3112e27811..36e878fdf1 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_gizmo_ip.py @@ -6,14 +6,14 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( maintained_selection, create_backdrop, get_avalon_knob_data, set_avalon_knob_data, swap_node_with_dependency, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -27,6 +27,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin): representations = {"*"} extensions = {"nk"} + settings_category = "nuke" + label = "Load Gizmo - Input Process" order = 0 icon = "eye" @@ -73,7 +75,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] @@ -145,7 +146,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_image.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_image.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/load/load_image.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_image.py index d825b621fc..0c43f5a5ca 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_image.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_image.py @@ -7,10 +7,10 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( get_imageio_input_colorspace ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -33,9 +33,9 @@ class LoadImage(load.LoaderPlugin): "image", } representations = {"*"} - extensions = set( - ext.lstrip(".") for ext in IMAGE_EXTENSIONS - ) + extensions = set(ext.lstrip(".") for ext in IMAGE_EXTENSIONS) + + settings_category = "nuke" label = "Load Image" order = -10 @@ -133,7 +133,7 @@ class LoadImage(load.LoaderPlugin): "version": version_entity["version"], "colorspace": colorspace, } - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes.get(k, str(None)) r["tile_color"].setValue(int("0x4ecd25ff", 16)) @@ -207,7 +207,6 @@ class LoadImage(load.LoaderPlugin): "colorspace": version_attributes.get("colorSpace"), "source": version_attributes.get("source"), "fps": str(version_attributes.get("fps")), - "author": version_attributes.get("author") } # change color of node diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_matchmove.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_matchmove.py similarity index 95% rename from client/ayon_core/hosts/nuke/plugins/load/load_matchmove.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_matchmove.py index beebd0458f..c1b5a24504 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_matchmove.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_matchmove.py @@ -11,6 +11,8 @@ class MatchmoveLoader(load.LoaderPlugin): representations = {"*"} extensions = {"py"} + settings_category = "nuke" + defaults = ["Camera", "Object"] label = "Run matchmove script" diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_model.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_model.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/load/load_model.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_model.py index 0326e0a4fc..551147be96 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_model.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_model.py @@ -5,8 +5,8 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import maintained_selection -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api.lib import maintained_selection +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -22,6 +22,8 @@ class AlembicModelLoader(load.LoaderPlugin): representations = {"*"} extensions = {"abc"} + settings_category = "nuke" + label = "Load Alembic" icon = "cube" color = "orange" @@ -47,7 +49,7 @@ class AlembicModelLoader(load.LoaderPlugin): "version": version_entity["version"] } # add attributes from the version to imprint to metadata knob - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # getting file path @@ -130,7 +132,7 @@ class AlembicModelLoader(load.LoaderPlugin): } # add additional metadata from the version to imprint to Avalon knob - for k in ["source", "author", "fps"]: + for k in ["source", "fps"]: data_imprint[k] = version_attributes[k] # getting file path diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_ociolook.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_ociolook.py index c369030b65..bdff8d7e28 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_ociolook.py @@ -10,7 +10,7 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( containerise, viewer_update_and_undo_stop, update_container, @@ -24,6 +24,8 @@ class LoadOcioLookNodes(load.LoaderPlugin): representations = {"*"} extensions = {"json"} + settings_category = "nuke" + label = "Load OcioLook [nodes]" order = 0 icon = "cc" diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py b/server_addon/nuke/client/ayon_nuke/plugins/load/load_script_precomp.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py rename to server_addon/nuke/client/ayon_nuke/plugins/load/load_script_precomp.py index 3e554f9d3b..cf543dabfd 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/load/load_script_precomp.py @@ -5,8 +5,8 @@ from ayon_core.pipeline import ( load, get_representation_path, ) -from ayon_core.hosts.nuke.api.lib import get_avalon_knob_data -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api.lib import get_avalon_knob_data +from ayon_nuke.api import ( containerise, update_container, viewer_update_and_undo_stop @@ -20,6 +20,8 @@ class LinkAsGroup(load.LoaderPlugin): representations = {"*"} extensions = {"nk"} + settings_category = "nuke" + label = "Load Precomp" order = 0 icon = "file" @@ -55,7 +57,6 @@ class LinkAsGroup(load.LoaderPlugin): "handleStart", "handleEnd", "source", - "author", "fps" ]: data_imprint[k] = version_attributes[k] @@ -131,7 +132,6 @@ class LinkAsGroup(load.LoaderPlugin): "colorspace": version_attributes.get("colorSpace"), "source": version_attributes.get("source"), "fps": version_attributes.get("fps"), - "author": version_attributes.get("author") } # Update the imprinted representation diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_backdrop.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_backdrop.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_backdrop.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_backdrop.py index fc17de95b4..1471159380 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_backdrop.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_backdrop.py @@ -1,6 +1,6 @@ from pprint import pformat import pyblish.api -from ayon_core.hosts.nuke.api import lib as pnlib +from ayon_nuke.api import lib as pnlib import nuke @@ -13,6 +13,8 @@ class CollectBackdrops(pyblish.api.InstancePlugin): hosts = ["nuke"] families = ["nukenodes"] + settings_category = "nuke" + def process(self, instance): self.log.debug(pformat(instance.data)) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_context_data.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_context_data.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_context_data.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_context_data.py index 0a032e5a2d..33c8e63e82 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_context_data.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_context_data.py @@ -2,7 +2,7 @@ import os import nuke import pyblish.api from ayon_core.lib import get_version_from_path -import ayon_core.hosts.nuke.api as napi +import ayon_nuke.api as napi from ayon_core.pipeline import KnownPublishError @@ -13,6 +13,8 @@ class CollectContextData(pyblish.api.ContextPlugin): label = "Collect context data" hosts = ['nuke'] + settings_category = "nuke" + def process(self, context): # sourcery skip: avoid-builtin-shadow root_node = nuke.root() diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_framerate.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_framerate.py similarity index 91% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_framerate.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_framerate.py index 88a449e745..cd77eab0f1 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_framerate.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_framerate.py @@ -13,5 +13,7 @@ class CollectFramerate(pyblish.api.ContextPlugin): "nukeassist" ] + settings_category = "nuke" + def process(self, context): context.data["fps"] = nuke.root()["fps"].getValue() diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_gizmo.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_gizmo.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_gizmo.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_gizmo.py index fda1c7ac31..ece9823b37 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_gizmo.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_gizmo.py @@ -11,6 +11,8 @@ class CollectGizmo(pyblish.api.InstancePlugin): hosts = ["nuke"] families = ["gizmo"] + settings_category = "nuke" + def process(self, instance): gizmo_node = instance.data["transientData"]["node"] diff --git a/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_headless_farm.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_headless_farm.py new file mode 100644 index 0000000000..c00b9a8f5d --- /dev/null +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_headless_farm.py @@ -0,0 +1,58 @@ +import pyblish.api + +from ayon_core.pipeline.publish import ( + AYONPyblishPluginMixin +) + + +class CollectRenderOnFarm(pyblish.api.ContextPlugin): + """Setup instances for render on farm submission.""" + + # Needs to be after CollectFromCreateContext + order = pyblish.api.CollectorOrder - 0.49 + label = "Collect Render On Farm" + hosts = ["nuke"] + + settings_category = "nuke" + + def process(self, context): + if not context.data.get("render_on_farm", False): + return + + for instance in context: + if instance.data["family"] == "workfile": + instance.data["active"] = False + continue + + # Filter out all other instances. + node = instance.data["transientData"]["node"] + if node.name() != instance.context.data["node_name"]: + instance.data["active"] = False + continue + + instance.data["families"].append("render_on_farm") + + # Enable for farm publishing. + instance.data["farm"] = True + + # Skip workfile version incremental save. + instance.context.data["increment_script_version"] = False + + +class SetupRenderOnFarm(pyblish.api.InstancePlugin, AYONPyblishPluginMixin): + """Setup instance for render on farm submission.""" + + order = pyblish.api.CollectorOrder + 0.4999 + label = "Setup Render On Farm" + hosts = ["nuke"] + families = ["render_on_farm"] + + def process(self, instance): + # Clear the families as we only want the main family, ei. no review + # etc. + instance.data["families"] = ["render_on_farm"] + + # Use the workfile instead of published. + publish_attributes = instance.data["publish_attributes"] + plugin_attributes = publish_attributes["NukeSubmitDeadline"] + plugin_attributes["use_published_workfile"] = False diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_model.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_model.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_model.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_model.py index 1a2bc9c019..f4266bbbcb 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_model.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_model.py @@ -11,6 +11,8 @@ class CollectModel(pyblish.api.InstancePlugin): hosts = ["nuke"] families = ["model"] + settings_category = "nuke" + def process(self, instance): geo_node = instance.data["transientData"]["node"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_nuke_instance_data.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_nuke_instance_data.py index 951072ff3f..d1392a8460 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_nuke_instance_data.py @@ -11,6 +11,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): label = "Collect Nuke Instance Data" hosts = ["nuke", "nukeassist"] + settings_category = "nuke" + # presets sync_workfile_version_on_families = [] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_reads.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_reads.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_reads.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_reads.py index af17933eb1..439374e825 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_reads.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_reads.py @@ -12,6 +12,8 @@ class CollectNukeReads(pyblish.api.InstancePlugin): hosts = ["nuke", "nukeassist"] families = ["source"] + settings_category = "nuke" + def process(self, instance): self.log.debug("checking instance: {}".format(instance)) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_slate_node.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_slate_node.py index ac30bd6051..bb3b0083ab 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_slate_node.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_slate_node.py @@ -10,6 +10,8 @@ class CollectSlate(pyblish.api.InstancePlugin): hosts = ["nuke"] families = ["render"] + settings_category = "nuke" + def process(self, instance): node = instance.data["transientData"]["node"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_workfile.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_workfile.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_workfile.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_workfile.py index 0f03572f8b..e4bd5ed129 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_workfile.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_workfile.py @@ -11,6 +11,8 @@ class CollectWorkfile(pyblish.api.InstancePlugin): hosts = ['nuke'] families = ["workfile"] + settings_category = "nuke" + def process(self, instance): # sourcery skip: avoid-builtin-shadow script_data = instance.context.data["scriptData"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_writes.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/collect_writes.py index 745351dc49..816f493d72 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/collect_writes.py @@ -1,7 +1,7 @@ import os import nuke import pyblish.api -from ayon_core.hosts.nuke import api as napi +from ayon_nuke import api as napi from ayon_core.pipeline import publish @@ -14,6 +14,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, hosts = ["nuke", "nukeassist"] families = ["render", "prerender", "image"] + settings_category = "nuke" + # cache _write_nodes = {} _frame_ranges = {} @@ -153,6 +155,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, # Determine defined file type ext = write_node["file_type"].value() + # determine defined channel type + color_channels = write_node["channels"].value() + # get frame range data handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] @@ -172,7 +177,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, "path": write_file_path, "outputDir": output_dir, "ext": ext, - "colorspace": colorspace + "colorspace": colorspace, + "color_channels": color_channels }) if product_type == "render": diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_backdrop.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_backdrop.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_backdrop.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_backdrop.py index e53ce9015a..8c42920979 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_backdrop.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_backdrop.py @@ -5,7 +5,7 @@ import nuke import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( maintained_selection, reset_selection, select_nodes @@ -25,6 +25,8 @@ class ExtractBackdropNode(publish.Extractor): hosts = ["nuke"] families = ["nukenodes"] + settings_category = "nuke" + def process(self, instance): tmp_nodes = [] child_nodes = instance.data["transientData"]["childNodes"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_camera.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_camera.py index a1a5acb63b..83914087e3 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_camera.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_camera.py @@ -6,7 +6,7 @@ import nuke import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api.lib import maintained_selection +from ayon_nuke.api.lib import maintained_selection class ExtractCamera(publish.Extractor): @@ -17,6 +17,8 @@ class ExtractCamera(publish.Extractor): families = ["camera"] hosts = ["nuke"] + settings_category = "nuke" + # presets write_geo_knobs = [ ("file_type", "abc"), diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_gizmo.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_gizmo.py similarity index 95% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_gizmo.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_gizmo.py index 2a2e2255fd..05e3164163 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_gizmo.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_gizmo.py @@ -4,8 +4,8 @@ import nuke import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api import utils as pnutils -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api import utils as pnutils +from ayon_nuke.api.lib import ( maintained_selection, reset_selection, select_nodes @@ -23,6 +23,8 @@ class ExtractGizmo(publish.Extractor): hosts = ["nuke"] families = ["gizmo"] + settings_category = "nuke" + def process(self, instance): tmp_nodes = [] orig_grpn = instance.data["transientData"]["node"] diff --git a/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_headless_farm.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_headless_farm.py new file mode 100644 index 0000000000..4721fe4462 --- /dev/null +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_headless_farm.py @@ -0,0 +1,38 @@ +import os +from datetime import datetime +import shutil + +import pyblish.api + +from ayon_core.pipeline import registered_host + + +class ExtractRenderOnFarm(pyblish.api.InstancePlugin): + """Copy the workfile to a timestamped copy.""" + + order = pyblish.api.ExtractorOrder + 0.499 + label = "Extract Render On Farm" + hosts = ["nuke"] + families = ["render_on_farm"] + + settings_category = "nuke" + + def process(self, instance): + if not instance.context.data.get("render_on_farm", False): + return + + host = registered_host() + current_datetime = datetime.now() + formatted_timestamp = current_datetime.strftime("%Y%m%d%H%M%S") + base, ext = os.path.splitext(host.current_file()) + + directory = os.path.join(os.path.dirname(base), "farm_submissions") + if not os.path.exists(directory): + os.makedirs(directory) + + filename = "{}_{}{}".format( + os.path.basename(base), formatted_timestamp, ext + ) + path = os.path.join(directory, filename).replace("\\", "/") + instance.context.data["currentFile"] = path + shutil.copy(host.current_file(), path) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_model.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_model.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_model.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_model.py index 36896fe595..58b9d4179b 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_model.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_model.py @@ -4,7 +4,7 @@ import nuke import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( maintained_selection, select_nodes ) @@ -18,6 +18,8 @@ class ExtractModel(publish.Extractor): families = ["model"] hosts = ["nuke"] + settings_category = "nuke" + # presets write_geo_knobs = [ ("file_type", "abc"), diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_ouput_node.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_ouput_node.py similarity index 92% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_ouput_node.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_ouput_node.py index b8e038a4f5..52072cddc5 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_ouput_node.py @@ -1,6 +1,6 @@ import nuke import pyblish.api -from ayon_core.hosts.nuke.api.lib import maintained_selection +from ayon_nuke.api.lib import maintained_selection class CreateOutputNode(pyblish.api.ContextPlugin): @@ -11,7 +11,9 @@ class CreateOutputNode(pyblish.api.ContextPlugin): label = 'Output Node Create' order = pyblish.api.ExtractorOrder + 0.4 families = ["workfile"] - hosts = ['nuke'] + hosts = ["nuke"] + + settings_category = "nuke" def process(self, context): # capture selection state diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_output_directory.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_output_directory.py similarity index 94% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_output_directory.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_output_directory.py index d999d200de..45156ca9ae 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_output_directory.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_output_directory.py @@ -10,7 +10,7 @@ class ExtractOutputDirectory(pyblish.api.InstancePlugin): label = "Output Directory" optional = True - # targets = ["process"] + settings_category = "nuke" def process(self, instance): diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_render_local.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_render_local.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_render_local.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_render_local.py index c8be2a5564..c865684e7a 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_render_local.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_render_local.py @@ -4,7 +4,7 @@ import shutil import pyblish.api import clique import nuke -from ayon_core.hosts.nuke import api as napi +from ayon_nuke import api as napi from ayon_core.pipeline import publish from ayon_core.lib import collect_frames @@ -25,6 +25,8 @@ class NukeRenderLocal(publish.Extractor, hosts = ["nuke"] families = ["render.local", "prerender.local", "image.local"] + settings_category = "nuke" + def process(self, instance): child_nodes = ( instance.data.get("transientData", {}).get("childNodes") diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_data.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_review_data.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data.py index 258a019319..856616898b 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_data.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data.py @@ -16,6 +16,8 @@ class ExtractReviewData(publish.Extractor): families = ["review"] hosts = ["nuke"] + settings_category = "nuke" + def process(self, instance): fpath = instance.data["path"] ext = os.path.splitext(fpath)[-1][1:] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_data_lut.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data_lut.py similarity index 94% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_review_data_lut.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data_lut.py index 0674a2dd55..d3377807ea 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_data_lut.py @@ -2,8 +2,8 @@ import os import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api import plugin -from ayon_core.hosts.nuke.api.lib import maintained_selection +from ayon_nuke.api import plugin +from ayon_nuke.api.lib import maintained_selection class ExtractReviewDataLut(publish.Extractor): @@ -19,6 +19,8 @@ class ExtractReviewDataLut(publish.Extractor): families = ["review"] hosts = ["nuke"] + settings_category = "nuke" + def process(self, instance): self.log.debug("Creating staging dir...") if "representations" in instance.data: diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py similarity index 94% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py index 8d7a3ec311..b7bb911347 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_review_intermediates.py @@ -4,8 +4,8 @@ from pprint import pformat import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api import plugin -from ayon_core.hosts.nuke.api.lib import maintained_selection +from ayon_nuke.api import plugin +from ayon_nuke.api.lib import maintained_selection class ExtractReviewIntermediates(publish.Extractor): @@ -22,6 +22,8 @@ class ExtractReviewIntermediates(publish.Extractor): families = ["review"] hosts = ["nuke"] + settings_category = "nuke" + # presets viewer_lut_raw = None outputs = {} @@ -136,11 +138,15 @@ class ExtractReviewIntermediates(publish.Extractor): self, instance, o_name, o_data["extension"], multiple_presets) + delete = not o_data.get("publish", False) + if instance.data.get("farm"): if "review" in instance.data["families"]: instance.data["families"].remove("review") - data = exporter.generate_mov(farm=True, **o_data) + data = exporter.generate_mov( + farm=True, delete=delete, **o_data + ) self.log.debug( "_ data: {}".format(data)) @@ -154,7 +160,7 @@ class ExtractReviewIntermediates(publish.Extractor): "bakeWriteNodeName": data.get("bakeWriteNodeName") }) else: - data = exporter.generate_mov(**o_data) + data = exporter.generate_mov(delete=delete, **o_data) # add representation generated by exporter generated_repres.extend(data["representations"]) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_script_save.py similarity index 85% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_script_save.py index d325684a7c..ea584b6529 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_script_save.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_script_save.py @@ -6,7 +6,9 @@ class ExtractScriptSave(pyblish.api.InstancePlugin): """Save current Nuke workfile script""" label = 'Script Save' order = pyblish.api.ExtractorOrder - 0.1 - hosts = ['nuke'] + hosts = ["nuke"] + + settings_category = "nuke" def process(self, instance): diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_slate_frame.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/extract_slate_frame.py index 627888ac92..47750ea637 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/extract_slate_frame.py @@ -7,7 +7,7 @@ import pyblish.api import six from ayon_core.pipeline import publish -from ayon_core.hosts.nuke.api import ( +from ayon_nuke.api import ( maintained_selection, duplicate_node, get_view_process_node @@ -27,6 +27,8 @@ class ExtractSlateFrame(publish.Extractor): families = ["slate"] hosts = ["nuke"] + settings_category = "nuke" + # Settings values key_value_mapping = { "f_submission_note": { diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_asset_context.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_asset_context.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_backdrop.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_backdrop.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_backdrop.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_backdrop.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_gizmo.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_gizmo.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_gizmo.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_gizmo.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_knobs.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_knobs.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_knobs.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_knobs.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_output_resolution.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_output_resolution.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_output_resolution.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_output_resolution.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_proxy_mode.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_proxy_mode.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_proxy_mode.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_rendered_frames.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_rendered_frames.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_rendered_frames.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_script_attributes.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_script_attributes.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_script_attributes.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_script_attributes.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml b/server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_write_nodes.xml similarity index 100% rename from client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml rename to server_addon/nuke/client/ayon_nuke/plugins/publish/help/validate_write_nodes.xml diff --git a/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/increment_script_version.py similarity index 82% rename from client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/increment_script_version.py index 6b0be42ba1..36659aa2d2 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/increment_script_version.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/increment_script_version.py @@ -1,4 +1,3 @@ - import nuke import pyblish.api @@ -10,9 +9,13 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): label = "Increment Script Version" optional = True families = ["workfile"] - hosts = ['nuke'] + hosts = ["nuke"] + + settings_category = "nuke" def process(self, context): + if not context.data.get("increment_script_version", True): + return assert all(result["success"] for result in context.data["results"]), ( "Publishing not successful so version is not increased.") diff --git a/client/ayon_core/hosts/nuke/plugins/publish/remove_ouput_node.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/remove_ouput_node.py similarity index 91% rename from client/ayon_core/hosts/nuke/plugins/publish/remove_ouput_node.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/remove_ouput_node.py index fb77e8638c..4c17cb5f56 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/remove_ouput_node.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/remove_ouput_node.py @@ -9,7 +9,9 @@ class RemoveOutputNode(pyblish.api.ContextPlugin): label = 'Output Node Remove' order = pyblish.api.IntegratorOrder + 0.4 families = ["workfile"] - hosts = ['nuke'] + hosts = ["nuke"] + + settings_category = "nuke" def process(self, context): try: diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_asset_context.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_asset_context.py index 93a30aa438..903648fd1b 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_asset_context.py @@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) -from ayon_core.hosts.nuke.api import SelectInstanceNodeAction +from ayon_nuke.api import SelectInstanceNodeAction class ValidateCorrectAssetContext( @@ -34,6 +34,8 @@ class ValidateCorrectAssetContext( ] optional = True + settings_category = "nuke" + @classmethod def apply_settings(cls, project_settings): """Apply deprecated settings from project settings. diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_backdrop.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_backdrop.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_backdrop.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_backdrop.py index 22344c661e..f7b94e0c82 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_backdrop.py @@ -1,6 +1,6 @@ import nuke import pyblish -from ayon_core.hosts.nuke import api as napi +from ayon_nuke import api as napi from ayon_core.pipeline.publish import ( ValidateContentsOrder, @@ -65,6 +65,8 @@ class ValidateBackdrop( hosts = ["nuke"] actions = [SelectCenterInNodeGraph] + settings_category = "nuke" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_exposed_knobs.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_exposed_knobs.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_exposed_knobs.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_exposed_knobs.py index 217fe6fb85..d1b7c146fb 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_exposed_knobs.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_exposed_knobs.py @@ -1,7 +1,7 @@ import pyblish.api from ayon_core.pipeline.publish import get_errored_instances_from_context -from ayon_core.hosts.nuke.api.lib import link_knobs +from ayon_nuke.api.lib import link_knobs from ayon_core.pipeline.publish import ( OptionalPyblishPluginMixin, PublishValidationError @@ -52,6 +52,9 @@ class ValidateExposedKnobs( label = "Validate Exposed Knobs" actions = [RepairExposedKnobs] hosts = ["nuke"] + + settings_category = "nuke" + product_types_mapping = { "render": "CreateWriteRender", "prerender": "CreateWritePrerender", diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_gizmo.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_gizmo.py similarity index 97% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_gizmo.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_gizmo.py index 2cdcb90d70..55249ae931 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_gizmo.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_gizmo.py @@ -1,6 +1,6 @@ import pyblish from ayon_core.pipeline import PublishXmlValidationError -from ayon_core.hosts.nuke import api as napi +from ayon_nuke import api as napi import nuke @@ -43,6 +43,8 @@ class ValidateGizmo(pyblish.api.InstancePlugin): hosts = ["nuke"] actions = [OpenFailedGroupNode] + settings_category = "nuke" + def process(self, instance): grpn = instance.data["transientData"]["node"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_knobs.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_knobs.py index 8bcde9609d..ea03bd94b2 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_knobs.py @@ -32,6 +32,8 @@ class ValidateKnobs(pyblish.api.ContextPlugin): actions = [RepairContextAction] optional = True + settings_category = "nuke" + knobs = "{}" def process(self, context): diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_output_resolution.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_output_resolution.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_output_resolution.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_output_resolution.py index e8a00d2294..440cb8b758 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_output_resolution.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_output_resolution.py @@ -1,6 +1,6 @@ import pyblish.api -from ayon_core.hosts.nuke import api as napi +from ayon_nuke import api as napi from ayon_core.pipeline.publish import RepairAction from ayon_core.pipeline import ( PublishXmlValidationError, @@ -27,6 +27,8 @@ class ValidateOutputResolution( hosts = ["nuke"] actions = [RepairAction] + settings_category = "nuke" + missing_msg = "Missing Reformat node in render group node" resolution_msg = "Reformat is set to wrong format" diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_proxy_mode.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_proxy_mode.py similarity index 96% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_proxy_mode.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_proxy_mode.py index 26e54295c9..1eb858b17e 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_proxy_mode.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_proxy_mode.py @@ -25,6 +25,8 @@ class ValidateProxyMode(pyblish.api.ContextPlugin): hosts = ["nuke"] actions = [FixProxyMode] + settings_category = "nuke" + def process(self, context): rootNode = nuke.root() diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_rendered_frames.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_rendered_frames.py index 76ac7e97ad..20b7f6a6ac 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_rendered_frames.py @@ -54,6 +54,8 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): hosts = ["nuke", "nukestudio"] actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm] + settings_category = "nuke" + def process(self, instance): node = instance.data["transientData"]["node"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_script_attributes.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_script_attributes.py index 2bd2034079..617d8d835b 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_script_attributes.py @@ -5,7 +5,7 @@ from ayon_core.pipeline import ( OptionalPyblishPluginMixin ) from ayon_core.pipeline.publish import RepairAction -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( WorkfileSettings ) @@ -23,6 +23,8 @@ class ValidateScriptAttributes( optional = True actions = [RepairAction] + settings_category = "nuke" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_write_nodes.py similarity index 98% rename from client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py rename to server_addon/nuke/client/ayon_nuke/plugins/publish/validate_write_nodes.py index 0244c1d504..d642a4314c 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/publish/validate_write_nodes.py @@ -2,7 +2,7 @@ from collections import defaultdict import pyblish.api from ayon_core.pipeline.publish import get_errored_instances_from_context -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( get_write_node_template_attr, set_node_knobs_from_settings, color_gui_to_int @@ -59,6 +59,8 @@ class ValidateNukeWriteNode( actions = [RepairNukeWriteNodeAction] hosts = ["nuke"] + settings_category = "nuke" + def process(self, instance): if not self.is_active(instance.data): return diff --git a/client/ayon_core/hosts/nuke/plugins/workfile_build/create_placeholder.py b/server_addon/nuke/client/ayon_nuke/plugins/workfile_build/create_placeholder.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/workfile_build/create_placeholder.py rename to server_addon/nuke/client/ayon_nuke/plugins/workfile_build/create_placeholder.py index a5490021e4..4d43d59bad 100644 --- a/client/ayon_core/hosts/nuke/plugins/workfile_build/create_placeholder.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/workfile_build/create_placeholder.py @@ -4,7 +4,7 @@ from ayon_core.pipeline.workfile.workfile_template_builder import ( CreatePlaceholderItem, PlaceholderCreateMixin, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( find_free_space_to_paste_nodes, get_extreme_positions, get_group_io_nodes, @@ -18,7 +18,7 @@ from ayon_core.hosts.nuke.api.lib import ( duplicate_node, node_tempfile, ) -from ayon_core.hosts.nuke.api.workfile_template_builder import ( +from ayon_nuke.api.workfile_template_builder import ( NukePlaceholderPlugin ) diff --git a/client/ayon_core/hosts/nuke/plugins/workfile_build/load_placeholder.py b/server_addon/nuke/client/ayon_nuke/plugins/workfile_build/load_placeholder.py similarity index 99% rename from client/ayon_core/hosts/nuke/plugins/workfile_build/load_placeholder.py rename to server_addon/nuke/client/ayon_nuke/plugins/workfile_build/load_placeholder.py index 258f48c9d3..68bc10e41b 100644 --- a/client/ayon_core/hosts/nuke/plugins/workfile_build/load_placeholder.py +++ b/server_addon/nuke/client/ayon_nuke/plugins/workfile_build/load_placeholder.py @@ -4,7 +4,7 @@ from ayon_core.pipeline.workfile.workfile_template_builder import ( LoadPlaceholderItem, PlaceholderLoadMixin, ) -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( find_free_space_to_paste_nodes, get_extreme_positions, get_group_io_nodes, @@ -18,7 +18,7 @@ from ayon_core.hosts.nuke.api.lib import ( duplicate_node, node_tempfile, ) -from ayon_core.hosts.nuke.api.workfile_template_builder import ( +from ayon_nuke.api.workfile_template_builder import ( NukePlaceholderPlugin ) diff --git a/client/ayon_core/hosts/nuke/startup/__init__.py b/server_addon/nuke/client/ayon_nuke/startup/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/startup/__init__.py rename to server_addon/nuke/client/ayon_nuke/startup/__init__.py diff --git a/client/ayon_core/hosts/nuke/startup/clear_rendered.py b/server_addon/nuke/client/ayon_nuke/startup/clear_rendered.py similarity index 100% rename from client/ayon_core/hosts/nuke/startup/clear_rendered.py rename to server_addon/nuke/client/ayon_nuke/startup/clear_rendered.py diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/server_addon/nuke/client/ayon_nuke/startup/custom_write_node.py similarity index 99% rename from client/ayon_core/hosts/nuke/startup/custom_write_node.py rename to server_addon/nuke/client/ayon_nuke/startup/custom_write_node.py index f119e69919..5b0f240a49 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/server_addon/nuke/client/ayon_nuke/startup/custom_write_node.py @@ -3,7 +3,7 @@ import os import nuke import nukescripts from ayon_core.pipeline import Anatomy, get_current_project_name -from ayon_core.hosts.nuke.api.lib import ( +from ayon_nuke.api.lib import ( set_node_knobs_from_settings, get_nuke_imageio_settings ) diff --git a/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py b/server_addon/nuke/client/ayon_nuke/startup/frame_setting_for_read_nodes.py similarity index 100% rename from client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py rename to server_addon/nuke/client/ayon_nuke/startup/frame_setting_for_read_nodes.py diff --git a/client/ayon_core/hosts/nuke/startup/menu.py b/server_addon/nuke/client/ayon_nuke/startup/menu.py similarity index 64% rename from client/ayon_core/hosts/nuke/startup/menu.py rename to server_addon/nuke/client/ayon_nuke/startup/menu.py index 2559e2142a..c3dd8cda8f 100644 --- a/client/ayon_core/hosts/nuke/startup/menu.py +++ b/server_addon/nuke/client/ayon_nuke/startup/menu.py @@ -1,5 +1,5 @@ from ayon_core.pipeline import install_host -from ayon_core.hosts.nuke.api import NukeHost +from ayon_nuke.api import NukeHost host = NukeHost() install_host(host) diff --git a/client/ayon_core/hosts/nuke/startup/write_to_read.py b/server_addon/nuke/client/ayon_nuke/startup/write_to_read.py similarity index 100% rename from client/ayon_core/hosts/nuke/startup/write_to_read.py rename to server_addon/nuke/client/ayon_nuke/startup/write_to_read.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/__init__.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/__init__.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/__init__.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/any_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/any_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/any_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/any_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/api_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/api_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/api_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/api_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/compiler/__init__.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/compiler/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/compiler/__init__.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/compiler/__init__.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/compiler/plugin_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/compiler/plugin_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/compiler/plugin_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_database.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_database.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_database.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_database.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_pool.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_pool.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/descriptor_pool.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/descriptor_pool.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/duration_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/duration_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/duration_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/duration_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/empty_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/empty_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/empty_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/empty_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/field_mask_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/field_mask_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/field_mask_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/__init__.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/__init__.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/__init__.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/_parameterized.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/_parameterized.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/_parameterized.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/api_implementation.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/api_implementation.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/api_implementation.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/builder.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/builder.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/builder.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/builder.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/containers.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/containers.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/containers.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/containers.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/decoder.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/decoder.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/decoder.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/decoder.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/encoder.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/encoder.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/encoder.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/encoder.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/enum_type_wrapper.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/enum_type_wrapper.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/enum_type_wrapper.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/enum_type_wrapper.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/extension_dict.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/extension_dict.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/extension_dict.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/message_listener.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/message_listener.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/message_listener.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/message_listener.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/message_set_extensions_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/missing_enum_values_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_extensions_dynamic_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_extensions_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_extensions_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_extensions_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_messages_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/more_messages_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/more_messages_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/no_package_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/no_package_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/no_package_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/python_message.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/python_message.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/python_message.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/python_message.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/type_checkers.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/type_checkers.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/type_checkers.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/well_known_types.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/well_known_types.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/well_known_types.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/wire_format.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/wire_format.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/internal/wire_format.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/internal/wire_format.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/json_format.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/json_format.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/json_format.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/json_format.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/message.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/message.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/message.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/message.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/message_factory.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/message_factory.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/message_factory.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/message_factory.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/proto_builder.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/proto_builder.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/proto_builder.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/proto_builder.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/__init__.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/__init__.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/__init__.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/cpp_message.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/cpp_message.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/cpp_message.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/python_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/pyext/python_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/pyext/python_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/reflection.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/reflection.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/reflection.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/reflection.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/service.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/service.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/service.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/service.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/service_reflection.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/service_reflection.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/service_reflection.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/service_reflection.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/source_context_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/source_context_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/source_context_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/source_context_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/struct_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/struct_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/struct_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/struct_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/symbol_database.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/symbol_database.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/symbol_database.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/symbol_database.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/text_encoding.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_encoding.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/text_encoding.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_encoding.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/text_format.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/text_format.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/timestamp_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/timestamp_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/timestamp_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/type_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/type_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/type_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/type_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/util/__init__.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/__init__.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/util/__init__.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/__init__.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/json_format_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/util/json_format_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/json_format_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/util/json_format_proto3_pb2.py diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/wrappers_pb2.py similarity index 100% rename from client/ayon_core/hosts/nuke/vendor/google/protobuf/wrappers_pb2.py rename to server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/wrappers_pb2.py diff --git a/server_addon/nuke/package.py b/server_addon/nuke/package.py index bf03c4e7e7..9e1f9362cb 100644 --- a/server_addon/nuke/package.py +++ b/server_addon/nuke/package.py @@ -1,3 +1,10 @@ name = "nuke" title = "Nuke" -version = "0.1.11" +version = "0.2.1" + +client_dir = "ayon_nuke" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {} diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index 6bdc5ee5ad..e4a0f9c938 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -12,7 +12,11 @@ def instance_attributes_enum(): return [ {"value": "reviewable", "label": "Reviewable"}, {"value": "farm_rendering", "label": "Farm rendering"}, - {"value": "use_range_limit", "label": "Use range limit"} + {"value": "use_range_limit", "label": "Use range limit"}, + { + "value": "render_on_farm", + "label": "Render On Farm" + } ] diff --git a/server_addon/nuke/server/settings/loader_plugins.py b/server_addon/nuke/server/settings/loader_plugins.py index 531ea8d986..22cb469e8d 100644 --- a/server_addon/nuke/server/settings/loader_plugins.py +++ b/server_addon/nuke/server/settings/loader_plugins.py @@ -22,7 +22,9 @@ class LoadClipOptionsModel(BaseSettingsModel): add_retime: bool = SettingsField( title="Add retime" ) - + deep_exr: bool = SettingsField( + title="Deep Exr Read Node" + ) class LoadClipModel(BaseSettingsModel): enabled: bool = SettingsField( @@ -65,7 +67,8 @@ DEFAULT_LOADER_PLUGINS_SETTINGS = { "node_name_template": "{class_name}_{ext}", "options_defaults": { "start_at_workfile": True, - "add_retime": True + "add_retime": True, + "deep_exr": False } } } diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index d5b05d8715..6c37ecd37a 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -125,6 +125,7 @@ class ReformatNodesConfigModel(BaseSettingsModel): class IntermediateOutputModel(BaseSettingsModel): name: str = SettingsField(title="Output name") + publish: bool = SettingsField(title="Publish") filter: BakingStreamFilterModel = SettingsField( title="Filter", default_factory=BakingStreamFilterModel) read_raw: bool = SettingsField( @@ -230,10 +231,6 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=OptionalPluginModel, section="Validators" ) - ValidateContainers: OptionalPluginModel = SettingsField( - title="Validate Containers", - default_factory=OptionalPluginModel - ) ValidateKnobs: ValidateKnobsModel = SettingsField( title="Validate Knobs", default_factory=ValidateKnobsModel @@ -299,11 +296,6 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "optional": True, "active": True }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, "ValidateKnobs": { "enabled": False, "knobs": "\n".join([ @@ -346,6 +338,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "outputs": [ { "name": "baking", + "publish": False, "filter": { "task_types": [], "product_types": [], @@ -401,6 +394,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "outputs": [ { "name": "baking", + "publish": False, "filter": { "task_types": [], "product_types": [], diff --git a/server_addon/photoshop/package.py b/server_addon/photoshop/package.py index 25615529d1..22043f951c 100644 --- a/server_addon/photoshop/package.py +++ b/server_addon/photoshop/package.py @@ -1,3 +1,3 @@ name = "photoshop" title = "Photoshop" -version = "0.1.2" +version = "0.1.3" diff --git a/server_addon/photoshop/server/settings/publish_plugins.py b/server_addon/photoshop/server/settings/publish_plugins.py index d04faaf53a..149b08beb4 100644 --- a/server_addon/photoshop/server/settings/publish_plugins.py +++ b/server_addon/photoshop/server/settings/publish_plugins.py @@ -83,14 +83,6 @@ class CollectVersionPlugin(BaseSettingsModel): enabled: bool = SettingsField(True, title="Enabled") -class ValidateContainersPlugin(BaseSettingsModel): - """Check that workfile contains latest version of loaded items""" # noqa - _isGroup = True - enabled: bool = True - optional: bool = SettingsField(False, title="Optional") - active: bool = SettingsField(True, title="Active") - - class ValidateNamingPlugin(BaseSettingsModel): """Validate naming of products and layers""" # noqa invalid_chars: str = SettingsField( @@ -154,11 +146,6 @@ class PhotoshopPublishPlugins(BaseSettingsModel): default_factory=CollectVersionPlugin, ) - ValidateContainers: ValidateContainersPlugin = SettingsField( - title="Validate Containers", - default_factory=ValidateContainersPlugin, - ) - ValidateNaming: ValidateNamingPlugin = SettingsField( title="Validate naming of products and layers", default_factory=ValidateNamingPlugin, @@ -187,11 +174,6 @@ DEFAULT_PUBLISH_SETTINGS = { "CollectVersion": { "enabled": False }, - "ValidateContainers": { - "enabled": True, - "optional": True, - "active": True - }, "ValidateNaming": { "invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,;]", "replace_char": "_" diff --git a/client/ayon_core/hosts/traypublisher/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/__init__.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/__init__.py rename to server_addon/traypublisher/client/ayon_traypublisher/__init__.py diff --git a/client/ayon_core/hosts/traypublisher/addon.py b/server_addon/traypublisher/client/ayon_traypublisher/addon.py similarity index 93% rename from client/ayon_core/hosts/traypublisher/addon.py rename to server_addon/traypublisher/client/ayon_traypublisher/addon.py index 3dd275f223..5432cb1a92 100644 --- a/client/ayon_core/hosts/traypublisher/addon.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/addon.py @@ -29,8 +29,8 @@ class TrayPublishAddon(AYONAddon, IHostAddon, ITrayAction): def on_action_trigger(self): self.run_traypublisher() - def connect_with_addons(self, enabled_modules): - """Collect publish paths from other modules.""" + def connect_with_addons(self, enabled_addons): + """Collect publish paths from other addons.""" publish_paths = self.manager.collect_plugin_paths()["publish"] self.publish_paths.extend(publish_paths) @@ -55,9 +55,9 @@ def cli_main(): def launch(): """Launch TrayPublish tool UI.""" - from ayon_core.tools import traypublisher + from ayon_traypublisher import ui - traypublisher.main() + ui.main() @cli_main.command() diff --git a/client/ayon_core/hosts/traypublisher/api/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/api/__init__.py rename to server_addon/traypublisher/client/ayon_traypublisher/api/__init__.py diff --git a/client/ayon_core/hosts/traypublisher/api/editorial.py b/server_addon/traypublisher/client/ayon_traypublisher/api/editorial.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/api/editorial.py rename to server_addon/traypublisher/client/ayon_traypublisher/api/editorial.py diff --git a/client/ayon_core/hosts/traypublisher/api/pipeline.py b/server_addon/traypublisher/client/ayon_traypublisher/api/pipeline.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/api/pipeline.py rename to server_addon/traypublisher/client/ayon_traypublisher/api/pipeline.py diff --git a/client/ayon_core/hosts/traypublisher/api/plugin.py b/server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py similarity index 99% rename from client/ayon_core/hosts/traypublisher/api/plugin.py rename to server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py index 257d01eb50..973eb65b11 100644 --- a/client/ayon_core/hosts/traypublisher/api/plugin.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/api/plugin.py @@ -22,7 +22,7 @@ from .pipeline import ( ) REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) -SHARED_DATA_KEY = "openpype.traypublisher.instances" +SHARED_DATA_KEY = "ayon.traypublisher.instances" class HiddenTrayPublishCreator(HiddenCreator): diff --git a/client/ayon_core/hosts/traypublisher/batch_parsing.py b/server_addon/traypublisher/client/ayon_traypublisher/batch_parsing.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/batch_parsing.py rename to server_addon/traypublisher/client/ayon_traypublisher/batch_parsing.py diff --git a/client/ayon_core/hosts/traypublisher/csv_publish.py b/server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py similarity index 96% rename from client/ayon_core/hosts/traypublisher/csv_publish.py rename to server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py index b43792a357..b7906c5706 100644 --- a/client/ayon_core/hosts/traypublisher/csv_publish.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/csv_publish.py @@ -1,5 +1,3 @@ -import os - import pyblish.api import pyblish.util @@ -8,7 +6,7 @@ from ayon_core.lib.attribute_definitions import FileDefItem from ayon_core.pipeline import install_host from ayon_core.pipeline.create import CreateContext -from ayon_core.hosts.traypublisher.api import TrayPublisherHost +from ayon_traypublisher.api import TrayPublisherHost def csvpublish( diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py similarity index 93% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py index 4d865c1c5c..901bd758ba 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_colorspace_look.py @@ -15,13 +15,13 @@ from ayon_core.pipeline import ( CreatorError ) from ayon_core.pipeline import colorspace -from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator +from ayon_traypublisher.api.plugin import TrayPublishCreator class CreateColorspaceLook(TrayPublishCreator): """Creates colorspace look files.""" - identifier = "io.openpype.creators.traypublisher.colorspace_look" + identifier = "io.ayon.creators.traypublisher.colorspace_look" label = "Colorspace Look" product_type = "ociolook" description = "Publishes color space look file." @@ -156,14 +156,9 @@ This creator publishes color space look file (LUT). ] def apply_settings(self, project_settings): - host = self.create_context.host - host_name = host.name - project_name = host.get_current_project_name() - config_data = colorspace.get_imageio_config( - project_name, host_name, + config_data = colorspace.get_current_context_imageio_config_preset( project_settings=project_settings ) - if not config_data: self.enabled = False return diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py similarity index 99% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 8143e8b45b..5a5deeada8 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -13,9 +13,7 @@ from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS ) from ayon_core.pipeline.create import CreatorError -from ayon_core.hosts.traypublisher.api.plugin import ( - TrayPublishCreator -) +from ayon_traypublisher.api.plugin import TrayPublishCreator class IngestCSV(TrayPublishCreator): diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py similarity index 99% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py index 4057aee9a6..a2f6f211f5 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial.py @@ -4,11 +4,11 @@ from copy import deepcopy import ayon_api import opentimelineio as otio -from ayon_core.hosts.traypublisher.api.plugin import ( +from ayon_traypublisher.api.plugin import ( TrayPublishCreator, HiddenTrayPublishCreator ) -from ayon_core.hosts.traypublisher.api.editorial import ( +from ayon_traypublisher.api.editorial import ( ShotMetadataSolver ) from ayon_core.pipeline import CreatedInstance diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py new file mode 100644 index 0000000000..5f0a84be4a --- /dev/null +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_editorial_package.py @@ -0,0 +1,96 @@ +from pathlib import Path + +from ayon_core.pipeline import ( + CreatedInstance, +) + +from ayon_core.lib.attribute_definitions import ( + FileDef, + BoolDef, + TextDef, +) +from ayon_traypublisher.api.plugin import TrayPublishCreator + + +class EditorialPackageCreator(TrayPublishCreator): + """Creates instance for OTIO file from published folder. + + Folder contains OTIO file and exported .mov files. Process should publish + whole folder as single `editorial_pckg` product type and (possibly) convert + .mov files into different format and copy them into `publish` `resources` + subfolder. + """ + identifier = "editorial_pckg" + label = "Editorial package" + product_type = "editorial_pckg" + description = "Publish folder with OTIO file and resources" + + # Position batch creator after simple creators + order = 120 + + conversion_enabled = False + + def apply_settings(self, project_settings): + self.conversion_enabled = ( + project_settings["traypublisher"] + ["publish"] + ["ExtractEditorialPckgConversion"] + ["conversion_enabled"] + ) + + def get_icon(self): + return "fa.folder" + + def create(self, product_name, instance_data, pre_create_data): + folder_path = pre_create_data.get("folder_path") + if not folder_path: + return + + instance_data["creator_attributes"] = { + "folder_path": (Path(folder_path["directory"]) / + Path(folder_path["filenames"][0])).as_posix(), + "conversion_enabled": pre_create_data["conversion_enabled"] + } + + # Create new instance + new_instance = CreatedInstance(self.product_type, product_name, + instance_data, self) + self._store_new_instance(new_instance) + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return [ + FileDef( + "folder_path", + folders=True, + single_item=True, + extensions=[], + allow_sequences=False, + label="Folder path" + ), + BoolDef("conversion_enabled", + tooltip="Convert to output defined in Settings.", + default=self.conversion_enabled, + label="Convert resources"), + ] + + def get_instance_attr_defs(self): + return [ + TextDef( + "folder_path", + label="Folder path", + disabled=True + ), + BoolDef("conversion_enabled", + tooltip="Convert to output defined in Settings.", + label="Convert resources"), + ] + + def get_detail_description(self): + return """# Publish folder with OTIO file and video clips + + Folder contains OTIO file and exported .mov files. Process should + publish whole folder as single `editorial_pckg` product type and + (possibly) convert .mov files into different format and copy them into + `publish` `resources` subfolder. + """ diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_from_settings.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py similarity index 88% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_from_settings.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py index fe7ba4c4a4..13cf92ab10 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_from_settings.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_from_settings.py @@ -6,7 +6,7 @@ log = Logger.get_logger(__name__) def initialize(): - from ayon_core.hosts.traypublisher.api.plugin import SettingsCreator + from ayon_traypublisher.api.plugin import SettingsCreator project_name = os.environ["AYON_PROJECT_NAME"] project_settings = get_project_settings(project_name) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py similarity index 97% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py index 546408b4d6..77b9b0df0a 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_movie_batch.py @@ -17,8 +17,8 @@ from ayon_core.pipeline.create import ( TaskNotSetError, ) -from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator -from ayon_core.hosts.traypublisher.batch_parsing import ( +from ayon_traypublisher.api.plugin import TrayPublishCreator +from ayon_traypublisher.batch_parsing import ( get_folder_entity_from_filename ) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py similarity index 96% rename from client/ayon_core/hosts/traypublisher/plugins/create/create_online.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py index f48037701e..135a11c0c6 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_online.py @@ -14,13 +14,13 @@ from ayon_core.pipeline import ( CreatedInstance, CreatorError ) -from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator +from ayon_traypublisher.api.plugin import TrayPublishCreator class OnlineCreator(TrayPublishCreator): """Creates instance from file and retains its original name.""" - identifier = "io.openpype.creators.traypublisher.online" + identifier = "io.ayon.creators.traypublisher.online" label = "Online" product_type = "online" description = "Publish file retaining its original file name" diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_app_name.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_app_name.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_app_name.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_app_name.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_clip_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_clip_instances.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_clip_instances.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_clip_instances.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_colorspace_look.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_colorspace_look.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_colorspace_look.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_csv_ingest_instance_data.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_csv_ingest_instance_data.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_csv_ingest_instance_data.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_editorial_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_instances.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_editorial_instances.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_instances.py diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py new file mode 100644 index 0000000000..cb1277546c --- /dev/null +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_package.py @@ -0,0 +1,58 @@ +"""Produces instance.data["editorial_pckg"] data used during integration. + +Requires: + instance.data["creator_attributes"]["path"] - from creator + +Provides: + instance -> editorial_pckg (dict): + folder_path (str) + otio_path (str) - from dragged folder + resource_paths (list) + +""" +import os + +import pyblish.api + +from ayon_core.lib.transcoding import VIDEO_EXTENSIONS + + +class CollectEditorialPackage(pyblish.api.InstancePlugin): + """Collects path to OTIO file and resources""" + + label = "Collect Editorial Package" + order = pyblish.api.CollectorOrder - 0.1 + + hosts = ["traypublisher"] + families = ["editorial_pckg"] + + def process(self, instance): + folder_path = instance.data["creator_attributes"]["folder_path"] + if not folder_path or not os.path.exists(folder_path): + self.log.info(( + "Instance doesn't contain collected existing folder path." + )) + return + + instance.data["editorial_pckg"] = {} + instance.data["editorial_pckg"]["folder_path"] = folder_path + + otio_path, resource_paths = ( + self._get_otio_and_resource_paths(folder_path)) + + instance.data["editorial_pckg"]["otio_path"] = otio_path + instance.data["editorial_pckg"]["resource_paths"] = resource_paths + + def _get_otio_and_resource_paths(self, folder_path): + otio_path = None + resource_paths = [] + + file_names = os.listdir(folder_path) + for filename in file_names: + _, ext = os.path.splitext(filename) + file_path = os.path.join(folder_path, filename) + if ext == ".otio": + otio_path = file_path + elif ext in VIDEO_EXTENSIONS: + resource_paths.append(file_path) + return otio_path, resource_paths diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_reviewable.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_editorial_reviewable.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_editorial_reviewable.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py similarity index 85% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py index 8e29a0048d..5fbb9a6f4c 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -1,10 +1,7 @@ import pyblish.api -from ayon_core.pipeline import ( - publish, - registered_host -) from ayon_core.lib import EnumDef from ayon_core.pipeline import colorspace +from ayon_core.pipeline import publish from ayon_core.pipeline.publish import KnownPublishError @@ -19,9 +16,10 @@ class CollectColorspace(pyblish.api.InstancePlugin, families = ["render", "plate", "reference", "image", "online"] enabled = False - colorspace_items = [ + default_colorspace_items = [ (None, "Don't override") ] + colorspace_items = list(default_colorspace_items) colorspace_attr_show = False config_items = None @@ -69,14 +67,13 @@ class CollectColorspace(pyblish.api.InstancePlugin, @classmethod def apply_settings(cls, project_settings): - host = registered_host() - host_name = host.name - project_name = host.get_current_project_name() - config_data = colorspace.get_imageio_config( - project_name, host_name, + config_data = colorspace.get_current_context_imageio_config_preset( project_settings=project_settings ) + enabled = False + colorspace_items = list(cls.default_colorspace_items) + config_items = None if config_data: filepath = config_data["path"] config_items = colorspace.get_ocio_config_colorspaces(filepath) @@ -85,9 +82,11 @@ class CollectColorspace(pyblish.api.InstancePlugin, include_aliases=True, include_roles=True ) - cls.config_items = config_items - cls.colorspace_items.extend(labeled_colorspaces) - cls.enabled = True + colorspace_items.extend(labeled_colorspaces) + + cls.config_items = config_items + cls.colorspace_items = colorspace_items + cls.enabled = enabled @classmethod def get_attribute_defs(cls): diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py similarity index 64% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py index 4d203649c7..2e564a2e4e 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_frame_data_from_folder_entity.py @@ -10,9 +10,13 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.491 label = "Collect Missing Frame Data From Folder" - families = ["plate", "pointcache", - "vdbcache", "online", - "render"] + families = [ + "plate", + "pointcache", + "vdbcache", + "online", + "render", + ] hosts = ["traypublisher"] def process(self, instance): @@ -22,16 +26,26 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): "frameStart", "frameEnd", "handleStart", - "handleEnd" + "handleEnd", ): if key not in instance.data: missing_keys.append(key) + + # Skip the logic if all keys are already collected. + # NOTE: In editorial is not 'folderEntity' filled, so it would crash + # even if we don't need it. + if not missing_keys: + return + keys_set = [] folder_attributes = instance.data["folderEntity"]["attrib"] for key in missing_keys: if key in folder_attributes: instance.data[key] = folder_attributes[key] keys_set.append(key) + if keys_set: - self.log.debug(f"Frame range data {keys_set} " - "has been collected from folder entity.") + self.log.debug( + f"Frame range data {keys_set} " + "has been collected from folder entity." + ) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_movie_batch.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_movie_batch.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_movie_batch.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_movie_batch.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_online_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_online_file.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_online_file.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_online_file.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_review_frames.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_review_frames.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_sequence_frame_data.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_sequence_frame_data.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_shot_instances.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_shot_instances.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_simple_instances.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_simple_instances.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_simple_instances.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_source.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_source.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/collect_source.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/collect_source.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/extract_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_colorspace_look.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/extract_colorspace_look.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_colorspace_look.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_csv_file.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/extract_csv_file.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_csv_file.py diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py new file mode 100644 index 0000000000..6dd4e84704 --- /dev/null +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_editorial_pckg.py @@ -0,0 +1,232 @@ +import copy +import os.path +import subprocess + +import opentimelineio + +import pyblish.api + +from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess +from ayon_core.pipeline import publish + + +class ExtractEditorialPckgConversion(publish.Extractor): + """Replaces movie paths in otio file with publish rootless + + Prepares movie resources for integration (adds them to `transfers`). + Converts .mov files according to output definition. + """ + + label = "Extract Editorial Package" + order = pyblish.api.ExtractorOrder - 0.45 + hosts = ["traypublisher"] + families = ["editorial_pckg"] + + def process(self, instance): + editorial_pckg_data = instance.data.get("editorial_pckg") + + otio_path = editorial_pckg_data["otio_path"] + otio_basename = os.path.basename(otio_path) + staging_dir = self.staging_dir(instance) + + editorial_pckg_repre = { + 'name': "editorial_pckg", + 'ext': "otio", + 'files': otio_basename, + "stagingDir": staging_dir, + } + otio_staging_path = os.path.join(staging_dir, otio_basename) + + instance.data["representations"].append(editorial_pckg_repre) + + publish_resource_folder = self._get_publish_resource_folder(instance) + resource_paths = editorial_pckg_data["resource_paths"] + transfers = self._get_transfers(resource_paths, + publish_resource_folder) + + project_settings = instance.context.data["project_settings"] + output_def = (project_settings["traypublisher"] + ["publish"] + ["ExtractEditorialPckgConversion"] + ["output"]) + + conversion_enabled = (instance.data["creator_attributes"] + ["conversion_enabled"]) + + if conversion_enabled and output_def["ext"]: + transfers = self._convert_resources(output_def, transfers) + + instance.data["transfers"] = transfers + + source_to_rootless = self._get_resource_path_mapping(instance, + transfers) + + otio_data = editorial_pckg_data["otio_data"] + otio_data = self._replace_target_urls(otio_data, source_to_rootless) + + opentimelineio.adapters.write_to_file(otio_data, otio_staging_path) + + self.log.info("Added Editorial Package representation: {}".format( + editorial_pckg_repre)) + + def _get_publish_resource_folder(self, instance): + """Calculates publish folder and create it.""" + publish_path = self._get_published_path(instance) + publish_folder = os.path.dirname(publish_path) + publish_resource_folder = os.path.join(publish_folder, "resources") + + if not os.path.exists(publish_resource_folder): + os.makedirs(publish_resource_folder, exist_ok=True) + return publish_resource_folder + + def _get_resource_path_mapping(self, instance, transfers): + """Returns dict of {source_mov_path: rootless_published_path}.""" + replace_paths = {} + anatomy = instance.context.data["anatomy"] + for source, destination in transfers: + rootless_path = self._get_rootless(anatomy, destination) + source_file_name = os.path.basename(source) + replace_paths[source_file_name] = rootless_path + return replace_paths + + def _get_transfers(self, resource_paths, publish_resource_folder): + """Returns list of tuples (source, destination) with movie paths.""" + transfers = [] + for res_path in resource_paths: + res_basename = os.path.basename(res_path) + pub_res_path = os.path.join(publish_resource_folder, res_basename) + transfers.append((res_path, pub_res_path)) + return transfers + + def _replace_target_urls(self, otio_data, replace_paths): + """Replace original movie paths with published rootless ones.""" + for track in otio_data.tracks: + for clip in track: + # Check if the clip has a media reference + if clip.media_reference is not None: + # Access the target_url from the media reference + target_url = clip.media_reference.target_url + if not target_url: + continue + file_name = os.path.basename(target_url) + replace_path = replace_paths.get(file_name) + if replace_path: + clip.media_reference.target_url = replace_path + if clip.name == file_name: + clip.name = os.path.basename(replace_path) + + return otio_data + + def _get_rootless(self, anatomy, path): + """Try to find rootless {root[work]} path from `path`""" + success, rootless_path = anatomy.find_root_template_from_path( + path) + if not success: + # `rootless_path` is not set to `output_dir` if none of roots match + self.log.warning( + f"Could not find root path for remapping '{path}'." + ) + rootless_path = path + + return rootless_path + + def _get_published_path(self, instance): + """Calculates expected `publish` folder""" + # determine published path from Anatomy. + template_data = instance.data.get("anatomyData") + rep = instance.data["representations"][0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data["anatomy"] + template_data["root"] = anatomy.roots + template = anatomy.get_template_item("publish", "default", "path") + template_filled = template.format_strict(template_data) + return os.path.normpath(template_filled) + + def _convert_resources(self, output_def, transfers): + """Converts all resource files to configured format.""" + out_extension = output_def["ext"] + if not out_extension: + self.log.warning("No output extension configured in " + "ayon+settings://traypublisher/publish/ExtractEditorialPckgConversion") # noqa + return transfers + + final_transfers = [] + out_def_ffmpeg_args = output_def["ffmpeg_args"] + ffmpeg_input_args = [ + value.strip() + for value in out_def_ffmpeg_args["input"] + if value.strip() + ] + ffmpeg_video_filters = [ + value.strip() + for value in out_def_ffmpeg_args["video_filters"] + if value.strip() + ] + ffmpeg_audio_filters = [ + value.strip() + for value in out_def_ffmpeg_args["audio_filters"] + if value.strip() + ] + ffmpeg_output_args = [ + value.strip() + for value in out_def_ffmpeg_args["output"] + if value.strip() + ] + ffmpeg_input_args = self._split_ffmpeg_args(ffmpeg_input_args) + + generic_args = [ + subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) + ] + generic_args.extend(ffmpeg_input_args) + if ffmpeg_video_filters: + generic_args.append("-filter:v") + generic_args.append( + "\"{}\"".format(",".join(ffmpeg_video_filters))) + + if ffmpeg_audio_filters: + generic_args.append("-filter:a") + generic_args.append( + "\"{}\"".format(",".join(ffmpeg_audio_filters))) + + for source, destination in transfers: + base_name = os.path.basename(destination) + file_name, ext = os.path.splitext(base_name) + dest_path = os.path.join(os.path.dirname(destination), + f"{file_name}.{out_extension}") + final_transfers.append((source, dest_path)) + + all_args = copy.deepcopy(generic_args) + all_args.append(f"-i \"{source}\"") + all_args.extend(ffmpeg_output_args) # order matters + all_args.append(f"\"{dest_path}\"") + subprcs_cmd = " ".join(all_args) + + # run subprocess + self.log.debug("Executing: {}".format(subprcs_cmd)) + run_subprocess(subprcs_cmd, shell=True, logger=self.log) + return final_transfers + + def _split_ffmpeg_args(self, in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args diff --git a/client/ayon_core/plugins/publish/extract_trim_video_audio.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_trim_video_audio.py similarity index 100% rename from client/ayon_core/plugins/publish/extract_trim_video_audio.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/extract_trim_video_audio.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_existing_version.xml similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_existing_version.xml diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_frame_ranges.xml b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_frame_ranges.xml similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_frame_ranges.xml rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/help/validate_frame_ranges.xml diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_colorspace.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_colorspace.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_colorspace_look.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace_look.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_colorspace_look.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_colorspace_look.py diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py new file mode 100644 index 0000000000..c63c4a6a73 --- /dev/null +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_editorial_package.py @@ -0,0 +1,68 @@ +import os +import opentimelineio + +import pyblish.api +from ayon_core.pipeline import PublishValidationError + + +class ValidateEditorialPackage(pyblish.api.InstancePlugin): + """Checks that published folder contains all resources from otio + + Currently checks only by file names and expects flat structure. + It ignores path to resources in otio file as folder might be dragged in and + published from different location than it was created. + """ + + label = "Validate Editorial Package" + order = pyblish.api.ValidatorOrder - 0.49 + + hosts = ["traypublisher"] + families = ["editorial_pckg"] + + def process(self, instance): + editorial_pckg_data = instance.data.get("editorial_pckg") + if not editorial_pckg_data: + raise PublishValidationError("Editorial package not collected") + + folder_path = editorial_pckg_data["folder_path"] + + otio_path = editorial_pckg_data["otio_path"] + if not otio_path: + raise PublishValidationError( + f"Folder {folder_path} missing otio file") + + resource_paths = editorial_pckg_data["resource_paths"] + + resource_file_names = {os.path.basename(path) + for path in resource_paths} + + otio_data = opentimelineio.adapters.read_from_file(otio_path) + + target_urls = self._get_all_target_urls(otio_data) + missing_files = set() + for target_url in target_urls: + target_basename = os.path.basename(target_url) + if target_basename not in resource_file_names: + missing_files.add(target_basename) + + if missing_files: + raise PublishValidationError( + f"Otio file contains missing files `{missing_files}`.\n\n" + f"Please add them to `{folder_path}` and republish.") + + instance.data["editorial_pckg"]["otio_data"] = otio_data + + def _get_all_target_urls(self, otio_data): + target_urls = [] + + # Iterate through tracks, clips, or other elements + for track in otio_data.tracks: + for clip in track: + # Check if the clip has a media reference + if clip.media_reference is not None: + # Access the target_url from the media reference + target_url = clip.media_reference.target_url + if target_url: + target_urls.append(target_url) + + return target_urls diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_existing_version.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_existing_version.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_filepaths.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_filepaths.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_filepaths.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_filepaths.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_frame_ranges.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_frame_ranges.py diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_online_file.py similarity index 100% rename from client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py rename to server_addon/traypublisher/client/ayon_traypublisher/plugins/publish/validate_online_file.py diff --git a/client/ayon_core/tools/traypublisher/__init__.py b/server_addon/traypublisher/client/ayon_traypublisher/ui/__init__.py similarity index 100% rename from client/ayon_core/tools/traypublisher/__init__.py rename to server_addon/traypublisher/client/ayon_traypublisher/ui/__init__.py diff --git a/client/ayon_core/tools/traypublisher/window.py b/server_addon/traypublisher/client/ayon_traypublisher/ui/window.py similarity index 99% rename from client/ayon_core/tools/traypublisher/window.py rename to server_addon/traypublisher/client/ayon_traypublisher/ui/window.py index 4700e20531..288dac8529 100644 --- a/client/ayon_core/tools/traypublisher/window.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/ui/window.py @@ -13,7 +13,6 @@ import qtawesome from ayon_core.lib import AYONSettingsRegistry, is_running_from_build from ayon_core.pipeline import install_host -from ayon_core.hosts.traypublisher.api import TrayPublisherHost from ayon_core.tools.publisher.control_qt import QtPublisherController from ayon_core.tools.publisher.window import PublisherWindow from ayon_core.tools.common_models import ProjectsModel @@ -24,6 +23,7 @@ from ayon_core.tools.utils import ( ProjectSortFilterProxy, PROJECT_NAME_ROLE, ) +from ayon_traypublisher.api import TrayPublisherHost class TrayPublisherRegistry(AYONSettingsRegistry): diff --git a/server_addon/traypublisher/package.py b/server_addon/traypublisher/package.py index 4ca8ae9fd3..ea04835b45 100644 --- a/server_addon/traypublisher/package.py +++ b/server_addon/traypublisher/package.py @@ -1,3 +1,10 @@ name = "traypublisher" title = "TrayPublisher" -version = "0.1.4" +version = "0.2.0" + +client_dir = "ayon_traypublisher" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {} diff --git a/server_addon/traypublisher/server/settings/publish_plugins.py b/server_addon/traypublisher/server/settings/publish_plugins.py index f413c86227..99a0bbf107 100644 --- a/server_addon/traypublisher/server/settings/publish_plugins.py +++ b/server_addon/traypublisher/server/settings/publish_plugins.py @@ -1,4 +1,7 @@ -from ayon_server.settings import BaseSettingsModel, SettingsField +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField, +) class ValidatePluginModel(BaseSettingsModel): @@ -14,6 +17,45 @@ class ValidateFrameRangeModel(ValidatePluginModel): 'my_asset_to_publish.mov')""" +class ExtractEditorialPckgFFmpegModel(BaseSettingsModel): + video_filters: list[str] = SettingsField( + default_factory=list, + title="Video filters" + ) + audio_filters: list[str] = SettingsField( + default_factory=list, + title="Audio filters" + ) + input: list[str] = SettingsField( + default_factory=list, + title="Input arguments" + ) + output: list[str] = SettingsField( + default_factory=list, + title="Output arguments" + ) + + +class ExtractEditorialPckgOutputDefModel(BaseSettingsModel): + _layout = "expanded" + ext: str = SettingsField("", title="Output extension") + + ffmpeg_args: ExtractEditorialPckgFFmpegModel = SettingsField( + default_factory=ExtractEditorialPckgFFmpegModel, + title="FFmpeg arguments" + ) + + +class ExtractEditorialPckgConversionModel(BaseSettingsModel): + """Set output definition if resource files should be converted.""" + conversion_enabled: bool = SettingsField(True, + title="Conversion enabled") + output: ExtractEditorialPckgOutputDefModel = SettingsField( + default_factory=ExtractEditorialPckgOutputDefModel, + title="Output Definitions", + ) + + class TrayPublisherPublishPlugins(BaseSettingsModel): CollectFrameDataFromAssetEntity: ValidatePluginModel = SettingsField( default_factory=ValidatePluginModel, @@ -28,6 +70,13 @@ class TrayPublisherPublishPlugins(BaseSettingsModel): default_factory=ValidatePluginModel, ) + ExtractEditorialPckgConversion: ExtractEditorialPckgConversionModel = ( + SettingsField( + default_factory=ExtractEditorialPckgConversionModel, + title="Extract Editorial Package Conversion" + ) + ) + DEFAULT_PUBLISH_PLUGINS = { "CollectFrameDataFromAssetEntity": { @@ -44,5 +93,24 @@ DEFAULT_PUBLISH_PLUGINS = { "enabled": True, "optional": True, "active": True + }, + "ExtractEditorialPckgConversion": { + "optional": False, + "conversion_enabled": True, + "output": { + "ext": "", + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-apply_trc gamma22" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-intra" + ] + } + } } } diff --git a/client/ayon_core/hosts/tvpaint/__init__.py b/server_addon/tvpaint/client/ayon_tvpaint/__init__.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/__init__.py rename to server_addon/tvpaint/client/ayon_tvpaint/__init__.py diff --git a/client/ayon_core/hosts/tvpaint/addon.py b/server_addon/tvpaint/client/ayon_tvpaint/addon.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/addon.py rename to server_addon/tvpaint/client/ayon_tvpaint/addon.py diff --git a/client/ayon_core/hosts/tvpaint/api/__init__.py b/server_addon/tvpaint/client/ayon_tvpaint/api/__init__.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/api/__init__.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/__init__.py diff --git a/client/ayon_core/hosts/tvpaint/api/communication_server.py b/server_addon/tvpaint/client/ayon_tvpaint/api/communication_server.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/api/communication_server.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/communication_server.py index d185bdf685..7ccb49f07e 100644 --- a/client/ayon_core/hosts/tvpaint/api/communication_server.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/api/communication_server.py @@ -22,7 +22,7 @@ from aiohttp_json_rpc.protocol import ( from aiohttp_json_rpc.exceptions import RpcError from ayon_core.lib import emit_event -from ayon_core.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path +from ayon_tvpaint.tvpaint_plugin import get_plugin_files_path log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) diff --git a/client/ayon_core/hosts/tvpaint/api/launch_script.py b/server_addon/tvpaint/client/ayon_tvpaint/api/launch_script.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/api/launch_script.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/launch_script.py index bcc92d8b6d..1e23e95572 100644 --- a/client/ayon_core/hosts/tvpaint/api/launch_script.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/api/launch_script.py @@ -10,7 +10,7 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core import style from ayon_core.pipeline import install_host -from ayon_core.hosts.tvpaint.api import ( +from ayon_tvpaint.api import ( TVPaintHost, CommunicationWrapper, ) diff --git a/client/ayon_core/hosts/tvpaint/api/lib.py b/server_addon/tvpaint/client/ayon_tvpaint/api/lib.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/api/lib.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/lib.py diff --git a/client/ayon_core/hosts/tvpaint/api/pipeline.py b/server_addon/tvpaint/client/ayon_tvpaint/api/pipeline.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/api/pipeline.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/pipeline.py index 6f5c4d49d4..5ec6355138 100644 --- a/client/ayon_core/hosts/tvpaint/api/pipeline.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/api/pipeline.py @@ -7,8 +7,9 @@ import requests import ayon_api import pyblish.api +from ayon_tvpaint import TVPAINT_ROOT_DIR + from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from ayon_core.hosts.tvpaint import TVPAINT_ROOT_DIR from ayon_core.settings import get_current_project_settings from ayon_core.lib import register_event_callback from ayon_core.pipeline import ( diff --git a/client/ayon_core/hosts/tvpaint/api/plugin.py b/server_addon/tvpaint/client/ayon_tvpaint/api/plugin.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/api/plugin.py rename to server_addon/tvpaint/client/ayon_tvpaint/api/plugin.py index e715b959f4..9dd6ae530a 100644 --- a/client/ayon_core/hosts/tvpaint/api/plugin.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/api/plugin.py @@ -12,7 +12,7 @@ from ayon_core.pipeline.create.creator_plugins import cache_and_get_instances from .lib import get_layers_data -SHARED_DATA_KEY = "openpype.tvpaint.instances" +SHARED_DATA_KEY = "ayon.tvpaint.instances" class TVPaintCreatorCommon: @@ -89,6 +89,8 @@ class TVPaintCreatorCommon: class TVPaintCreator(Creator, TVPaintCreatorCommon): + settings_category = "tvpaint" + def collect_instances(self): self._collect_create_instances() @@ -140,6 +142,8 @@ class TVPaintCreator(Creator, TVPaintCreatorCommon): class TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon): + settings_category = "tvpaint" + def collect_instances(self): self._collect_create_instances() @@ -152,6 +156,7 @@ class TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon): class Loader(LoaderPlugin): hosts = ["tvpaint"] + settings_category = "tvpaint" @staticmethod def get_members_from_container(container): diff --git a/client/ayon_core/hosts/tvpaint/hooks/pre_launch_args.py b/server_addon/tvpaint/client/ayon_tvpaint/hooks/pre_launch_args.py similarity index 95% rename from client/ayon_core/hosts/tvpaint/hooks/pre_launch_args.py rename to server_addon/tvpaint/client/ayon_tvpaint/hooks/pre_launch_args.py index 691b81e089..8ee91aa0e7 100644 --- a/client/ayon_core/hosts/tvpaint/hooks/pre_launch_args.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/hooks/pre_launch_args.py @@ -37,6 +37,6 @@ class TvpaintPrelaunchHook(PreLaunchHook): self.launch_context.launch_args.extend(remainders) def launch_script_path(self): - from ayon_core.hosts.tvpaint import get_launch_script_path + from ayon_tvpaint import get_launch_script_path return get_launch_script_path() diff --git a/client/ayon_core/hosts/tvpaint/lib.py b/server_addon/tvpaint/client/ayon_tvpaint/lib.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/lib.py rename to server_addon/tvpaint/client/ayon_tvpaint/lib.py diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/convert_legacy.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/create/convert_legacy.py index 34fe0ce8f4..e79a6565e8 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/convert_legacy.py @@ -4,8 +4,8 @@ from ayon_core.pipeline.create.creator_plugins import ( ProductConvertorPlugin, cache_and_get_instances, ) -from ayon_core.hosts.tvpaint.api.plugin import SHARED_DATA_KEY -from ayon_core.hosts.tvpaint.api.lib import get_groups_data +from ayon_tvpaint.api.plugin import SHARED_DATA_KEY +from ayon_tvpaint.api.lib import get_groups_data class TVPaintLegacyConverted(ProductConvertorPlugin): diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_render.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/plugins/create/create_render.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_render.py index dc9c2466e0..2286a4417a 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_render.py @@ -52,11 +52,11 @@ from ayon_core.pipeline.create import ( CreatedInstance, CreatorError, ) -from ayon_core.hosts.tvpaint.api.plugin import ( +from ayon_tvpaint.api.plugin import ( TVPaintCreator, TVPaintAutoCreator, ) -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api.lib import ( get_layers_data, get_groups_data, execute_george_through_file, diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_review.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/create/create_review.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_review.py index acb4f0f8d6..6068ffa1d8 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_review.py @@ -1,7 +1,7 @@ import ayon_api from ayon_core.pipeline import CreatedInstance -from ayon_core.hosts.tvpaint.api.plugin import TVPaintAutoCreator +from ayon_tvpaint.api.plugin import TVPaintAutoCreator class TVPaintReviewCreator(TVPaintAutoCreator): diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_workfile.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_workfile.py index f21f41439e..b08f731869 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/create/create_workfile.py @@ -1,7 +1,7 @@ import ayon_api from ayon_core.pipeline import CreatedInstance -from ayon_core.hosts.tvpaint.api.plugin import TVPaintAutoCreator +from ayon_tvpaint.api.plugin import TVPaintAutoCreator class TVPaintWorkfileCreator(TVPaintAutoCreator): diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_image.py similarity index 95% rename from client/ayon_core/hosts/tvpaint/plugins/load/load_image.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_image.py index aad8f92d26..de61992d3f 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_image.py @@ -1,6 +1,6 @@ from ayon_core.lib.attribute_definitions import BoolDef -from ayon_core.hosts.tvpaint.api import plugin -from ayon_core.hosts.tvpaint.api.lib import execute_george_through_file +from ayon_tvpaint.api import plugin +from ayon_tvpaint.api.lib import execute_george_through_file class ImportImage(plugin.Loader): diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_reference_image.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_reference_image.py index a7fcb9f4a4..ce08aa9cd9 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_reference_image.py @@ -2,12 +2,12 @@ import collections from ayon_core.lib.attribute_definitions import BoolDef from ayon_core.pipeline import registered_host -from ayon_core.hosts.tvpaint.api import plugin -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api import plugin +from ayon_tvpaint.api.lib import ( get_layers_data, execute_george_through_file, ) -from ayon_core.hosts.tvpaint.api.pipeline import ( +from ayon_tvpaint.api.pipeline import ( write_workfile_metadata, SECTION_NAME_CONTAINERS, containerise, diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_sound.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_sound.py index 7e8c8022d6..086afba079 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_sound.py @@ -1,7 +1,7 @@ import os import tempfile -from ayon_core.hosts.tvpaint.api import plugin -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api import plugin +from ayon_tvpaint.api.lib import ( execute_george_through_file, ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_workfile.py similarity index 96% rename from client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_workfile.py index 07c2d91533..045e22f188 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/load/load_workfile.py @@ -10,11 +10,11 @@ from ayon_core.pipeline.workfile import ( get_last_workfile_with_version, ) from ayon_core.pipeline.template_data import get_template_data_with_names -from ayon_core.hosts.tvpaint.api import plugin -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api import plugin +from ayon_tvpaint.api.lib import ( execute_george_through_file, ) -from ayon_core.hosts.tvpaint.api.pipeline import ( +from ayon_tvpaint.api.pipeline import ( get_current_workfile_context, ) from ayon_core.pipeline.version_start import get_versioning_start diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_instance_frames.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_instance_frames.py index 5f134a0cd0..a9e69166d7 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_instance_frames.py @@ -14,6 +14,8 @@ class CollectOutputFrameRange(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["review", "render"] + settings_category = "tvpaint" + def process(self, instance): folder_entity = instance.data.get("folderEntity") if not folder_entity: diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_render_instances.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_render_instances.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/plugins/publish/collect_render_instances.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_render_instances.py index 596d257f22..00af624700 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_render_instances.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_render_instances.py @@ -9,6 +9,7 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["render", "review"] + settings_category = "tvpaint" ignore_render_pass_transparency = False def process(self, instance): diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile.py similarity index 96% rename from client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile.py index a9e9db3872..27de086a46 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile.py @@ -9,6 +9,8 @@ class CollectWorkfile(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["workfile"] + settings_category = "tvpaint" + def process(self, instance): context = instance.context current_file = context.data["currentFile"] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile_data.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile_data.py index 3155773bda..a34a718ff5 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/collect_workfile_data.py @@ -4,13 +4,13 @@ import tempfile import pyblish.api -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api.lib import ( execute_george, execute_george_through_file, get_layers_data, get_groups_data, ) -from ayon_core.hosts.tvpaint.api.pipeline import ( +from ayon_tvpaint.api.pipeline import ( SECTION_NAME_CONTEXT, SECTION_NAME_INSTANCES, SECTION_NAME_CONTAINERS, @@ -58,6 +58,8 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): hosts = ["tvpaint"] actions = [ResetTVPaintWorkfileMetadata] + settings_category = "tvpaint" + def process(self, context): current_project_id = execute_george("tv_projectcurrentid") execute_george("tv_projectselect {}".format(current_project_id)) diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_convert_to_exr.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_convert_to_exr.py index d1bc68ef35..020ebc1a89 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_convert_to_exr.py @@ -23,6 +23,8 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["render"] + settings_category = "tvpaint" + enabled = False # Replace source PNG files or just add diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_sequence.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_sequence.py index fe5e148b7b..86c20c6528 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/extract_sequence.py @@ -10,13 +10,13 @@ from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_instance_families, ) -from ayon_core.hosts.tvpaint.api.lib import ( +from ayon_tvpaint.api.lib import ( execute_george, execute_george_through_file, get_layers_pre_post_behavior, get_layers_exposure_frames, ) -from ayon_core.hosts.tvpaint.lib import ( +from ayon_tvpaint.lib import ( calculate_layers_extraction_data, get_frame_filename_template, fill_reference_frames, @@ -31,6 +31,8 @@ class ExtractSequence(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["review", "render"] + settings_category = "tvpaint" + # Modifiable with settings review_bg = [255, 255, 255, 1.0] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_asset_name.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_asset_name.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_layers_visibility.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_layers_visibility.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_marks.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_marks.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_marks.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_marks.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_missing_layer_names.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_missing_layer_names.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_render_layer_group.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_render_layer_group.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_render_pass_group.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_render_pass_group.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_scene_settings.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_scene_settings.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_start_frame.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_start_frame.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_workfile_metadata.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_workfile_metadata.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_workfile_project_name.xml similarity index 100% rename from client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/help/validate_workfile_project_name.xml diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/increment_workfile_version.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/increment_workfile_version.py similarity index 95% rename from client/ayon_core/hosts/tvpaint/plugins/publish/increment_workfile_version.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/increment_workfile_version.py index 5dd6110bc7..601d276b97 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/increment_workfile_version.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/increment_workfile_version.py @@ -12,6 +12,8 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): optional = True hosts = ["tvpaint"] + settings_category = "tvpaint" + def process(self, context): assert all(result["success"] for result in context.data["results"]), ( diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_asset_name.py similarity index 96% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_asset_name.py index 764c090720..8763c005dc 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_asset_name.py @@ -3,7 +3,7 @@ from ayon_core.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin, ) -from ayon_core.hosts.tvpaint.api.pipeline import ( +from ayon_tvpaint.api.pipeline import ( list_instances, write_instances, ) @@ -48,6 +48,8 @@ class ValidateAssetName( hosts = ["tvpaint"] actions = [FixFolderPaths] + settings_category = "tvpaint" + def process(self, context): if not self.is_active(context.data): return diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_duplicated_layer_names.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_duplicated_layer_names.py index aab0557bdd..be4dc0f123 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_duplicated_layer_names.py @@ -9,6 +9,8 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["renderPass"] + settings_category = "tvpaint" + def process(self, instance): # Prepare layers layers_by_name = instance.context.data["layersByName"] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_layers_visibility.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_layers_visibility.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_layers_visibility.py index 1bcdf7baa1..f58b8a6973 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_layers_visibility.py @@ -10,6 +10,8 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["review", "render"] + settings_category = "tvpaint" + def process(self, instance): layers = instance.data.get("layers") # Instance have empty layers diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_marks.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_marks.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_marks.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_marks.py index 6bfbe840bb..0911beb4e8 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_marks.py @@ -5,7 +5,7 @@ from ayon_core.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin, ) -from ayon_core.hosts.tvpaint.api.lib import execute_george +from ayon_tvpaint.api.lib import execute_george class ValidateMarksRepair(pyblish.api.Action): @@ -41,6 +41,8 @@ class ValidateMarks( optional = True actions = [ValidateMarksRepair] + settings_category = "tvpaint" + @staticmethod def get_expected_data(context): scene_mark_in = context.data["sceneMarkIn"] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_missing_layer_names.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_missing_layer_names.py index 3fc80f6e78..f340d3c10d 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_missing_layer_names.py @@ -9,6 +9,8 @@ class ValidateMissingLayers(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["renderPass"] + settings_category = "tvpaint" + def process(self, instance): # Prepare layers layers_by_name = instance.context.data["layersByName"] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_layer_group.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_layer_group.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_layer_group.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_layer_group.py index 0e97a01de2..b20ea3cac6 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_layer_group.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_layer_group.py @@ -12,6 +12,8 @@ class ValidateRenderLayerGroups(pyblish.api.ContextPlugin): label = "Validate Render Layers Group" order = pyblish.api.ValidatorOrder + 0.1 + settings_category = "tvpaint" + def process(self, context): # Prepare layers render_layers_by_group_id = collections.defaultdict(list) diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_pass_group.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_pass_group.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_pass_group.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_pass_group.py index 874af38dd4..3d00fd031f 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_render_pass_group.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_render_pass_group.py @@ -13,6 +13,8 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder + 0.1 families = ["renderPass"] + settings_category = "tvpaint" + def process(self, instance): # Prepare layers layers_data = instance.context.data["layersData"] diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_scene_settings.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_scene_settings.py index 5e42b5ab2f..8bad5c43c8 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_scene_settings.py @@ -16,6 +16,8 @@ class ValidateProjectSettings( label = "Validate Scene Settings" order = pyblish.api.ValidatorOrder + + settings_category = "tvpaint" optional = True def process(self, context): diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_start_frame.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_start_frame.py similarity index 92% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_start_frame.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_start_frame.py index fea64bd6a8..9669acf1b5 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_start_frame.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_start_frame.py @@ -3,7 +3,7 @@ from ayon_core.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin, ) -from ayon_core.hosts.tvpaint.api.lib import execute_george +from ayon_tvpaint.api.lib import execute_george class RepairStartFrame(pyblish.api.Action): @@ -27,6 +27,8 @@ class ValidateStartFrame( order = pyblish.api.ValidatorOrder hosts = ["tvpaint"] actions = [RepairStartFrame] + + settings_category = "tvpaint" optional = True def process(self, context): diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_metadata.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_metadata.py index 1d9954d051..34c02c78ed 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_metadata.py @@ -31,6 +31,8 @@ class ValidateWorkfileMetadata(pyblish.api.ContextPlugin): actions = [ValidateWorkfileMetadataRepair] + settings_category = "tvpaint" + required_keys = {"project_name", "folder_path", "task_name"} def process(self, context): diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_project_name.py similarity index 98% rename from client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py rename to server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_project_name.py index 5b42842717..868c7d44fc 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/plugins/publish/validate_workfile_project_name.py @@ -12,6 +12,8 @@ class ValidateWorkfileProjectName(pyblish.api.ContextPlugin): label = "Validate Workfile Project Name" order = pyblish.api.ValidatorOrder + settings_category = "tvpaint" + def process(self, context): workfile_context = context.data.get("workfile_context") # If workfile context is missing than project is matching to diff --git a/client/ayon_core/hosts/tvpaint/resources/template.tvpp b/server_addon/tvpaint/client/ayon_tvpaint/resources/template.tvpp similarity index 100% rename from client/ayon_core/hosts/tvpaint/resources/template.tvpp rename to server_addon/tvpaint/client/ayon_tvpaint/resources/template.tvpp diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/__init__.py b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/__init__.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/__init__.py rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/__init__.py diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/CMakeLists.txt b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/CMakeLists.txt similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/CMakeLists.txt rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/CMakeLists.txt diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/README.md similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/README.md diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/library.cpp similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/library.cpp diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/library.def b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/library.def similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_code/library.def rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_code/library.def diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll diff --git a/client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll b/server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll similarity index 100% rename from client/ayon_core/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll rename to server_addon/tvpaint/client/ayon_tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll diff --git a/client/ayon_core/hosts/tvpaint/worker/__init__.py b/server_addon/tvpaint/client/ayon_tvpaint/worker/__init__.py similarity index 100% rename from client/ayon_core/hosts/tvpaint/worker/__init__.py rename to server_addon/tvpaint/client/ayon_tvpaint/worker/__init__.py diff --git a/client/ayon_core/hosts/tvpaint/worker/init_file.tvpp b/server_addon/tvpaint/client/ayon_tvpaint/worker/init_file.tvpp similarity index 100% rename from client/ayon_core/hosts/tvpaint/worker/init_file.tvpp rename to server_addon/tvpaint/client/ayon_tvpaint/worker/init_file.tvpp diff --git a/client/ayon_core/hosts/tvpaint/worker/worker.py b/server_addon/tvpaint/client/ayon_tvpaint/worker/worker.py similarity index 97% rename from client/ayon_core/hosts/tvpaint/worker/worker.py rename to server_addon/tvpaint/client/ayon_tvpaint/worker/worker.py index 3d9b1ef2b8..aa79fd442c 100644 --- a/client/ayon_core/hosts/tvpaint/worker/worker.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/worker/worker.py @@ -5,11 +5,11 @@ import tempfile import shutil import asyncio -from ayon_core.hosts.tvpaint.api.communication_server import ( +from ayon_tvpaint.api.communication_server import ( BaseCommunicator, CommunicationWrapper ) -from openpype_modules.job_queue.job_workers import WorkerJobsConnection +from ayon_core.modules.job_queue.job_workers import WorkerJobsConnection from .worker_job import ProcessTVPaintCommands diff --git a/client/ayon_core/hosts/tvpaint/worker/worker_job.py b/server_addon/tvpaint/client/ayon_tvpaint/worker/worker_job.py similarity index 99% rename from client/ayon_core/hosts/tvpaint/worker/worker_job.py rename to server_addon/tvpaint/client/ayon_tvpaint/worker/worker_job.py index f111ed369a..db91010c47 100644 --- a/client/ayon_core/hosts/tvpaint/worker/worker_job.py +++ b/server_addon/tvpaint/client/ayon_tvpaint/worker/worker_job.py @@ -256,7 +256,7 @@ class CollectSceneData(BaseCommand): name = "collect_scene_data" def execute(self): - from ayon_core.hosts.tvpaint.api.lib import ( + from ayon_tvpaint.api.lib import ( get_layers_data, get_groups_data, get_layers_pre_post_behavior, diff --git a/server_addon/tvpaint/package.py b/server_addon/tvpaint/package.py index 2be3164f4a..3ab35f727e 100644 --- a/server_addon/tvpaint/package.py +++ b/server_addon/tvpaint/package.py @@ -1,3 +1,9 @@ name = "tvpaint" title = "TVPaint" -version = "0.1.2" +version = "0.2.0" +client_dir = "ayon_tvpaint" + +ayon_required_addons = { + "core": ">0.3.2", +} +ayon_compatible_addons = {}