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/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/publish/collect_local_render_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_local_render_instances.py
index 5a446fa0d3..474002e1ee 100644
--- 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
@@ -132,6 +132,6 @@ class CollectLocalRenderInstances(pyblish.api.InstancePlugin):
]
})
- # Remove original render instance
- # I can't remove it here as I still need it to trigger the render.
- # context.remove(instance)
+ # 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/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 @@
+
+
+
+
+
+
diff --git a/client/ayon_core/hosts/max/api/lib.py b/client/ayon_core/hosts/max/api/lib.py
index 0e3abe25ec..f20f754248 100644
--- a/client/ayon_core/hosts/max/api/lib.py
+++ b/client/ayon_core/hosts/max/api/lib.py
@@ -369,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
@@ -389,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/load/load_image.py b/client/ayon_core/hosts/maya/plugins/load/load_image.py
index 5b0858ce70..171920f747 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/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py
index e3505a16f2..0a4755c166 100644
--- a/client/ayon_core/hosts/nuke/api/lib.py
+++ b/client/ayon_core/hosts/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
@@ -1552,10 +1554,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"]
diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/client/ayon_core/hosts/nuke/api/plugin.py
index fb56dec833..ec13104d4d 100644
--- a/client/ayon_core/hosts/nuke/api/plugin.py
+++ b/client/ayon_core/hosts/nuke/api/plugin.py
@@ -778,6 +778,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,7 +835,7 @@ 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
# get colorspace settings
@@ -947,6 +948,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,8 +990,13 @@ 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
diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py b/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py
index 7d823919dc..50af8a4eb9 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py
@@ -62,7 +62,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 +206,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/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py
index 14c54c3adc..3c7d4f3bb2 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py
@@ -48,7 +48,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 +123,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/client/ayon_core/hosts/nuke/plugins/load/load_clip.py
index df8f2ab018..7fa90da86f 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py
@@ -9,7 +9,8 @@ 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 (
get_imageio_input_colorspace,
@@ -197,7 +198,6 @@ class LoadClip(plugin.NukeLoader):
"frameStart",
"frameEnd",
"source",
- "author",
"fps",
"handleStart",
"handleEnd",
@@ -347,8 +347,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 +546,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/client/ayon_core/hosts/nuke/plugins/load/load_effects.py
index a87c81295a..be7420fcf0 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_effects.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_effects.py
@@ -69,7 +69,6 @@ class LoadEffects(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
- "author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@@ -189,7 +188,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/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py
index 8fa1347598..9bb430b37b 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py
@@ -69,7 +69,6 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
- "author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@@ -192,7 +191,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/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py
index 95f85bacfc..57d00795ae 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py
@@ -71,7 +71,6 @@ class LoadGizmo(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
- "author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@@ -139,7 +138,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/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py
index 3112e27811..ed2b1ec458 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py
@@ -73,7 +73,6 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
- "author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@@ -145,7 +144,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/client/ayon_core/hosts/nuke/plugins/load/load_image.py
index d825b621fc..b5fccd8a0d 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_image.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_image.py
@@ -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_model.py b/client/ayon_core/hosts/nuke/plugins/load/load_model.py
index 0326e0a4fc..40862cd1e0 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_model.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_model.py
@@ -47,7 +47,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 +130,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_script_precomp.py b/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py
index 3e554f9d3b..d6699be164 100644
--- a/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py
+++ b/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py
@@ -55,7 +55,6 @@ class LinkAsGroup(load.LoaderPlugin):
"handleStart",
"handleEnd",
"source",
- "author",
"fps"
]:
data_imprint[k] = version_attributes[k]
@@ -131,7 +130,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_writes.py b/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py
index 745351dc49..27525bcad1 100644
--- a/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py
+++ b/client/ayon_core/hosts/nuke/plugins/publish/collect_writes.py
@@ -153,6 +153,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 +175,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_review_intermediates.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py
index 8d7a3ec311..82c7b6e4c5 100644
--- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py
+++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py
@@ -136,11 +136,16 @@ class ExtractReviewIntermediates(publish.Extractor):
self, instance, o_name, o_data["extension"],
multiple_presets)
+ o_data["add_custom_tags"].append("intermediate")
+ 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 +159,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/traypublisher/plugins/create/create_colorspace_look.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py
index 4d865c1c5c..da05afe86b 100644
--- a/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py
+++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py
@@ -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_editorial_package.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial_package.py
new file mode 100644
index 0000000000..82b109be28
--- /dev/null
+++ b/client/ayon_core/hosts/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_core.hosts.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/publish/collect_editorial_package.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_editorial_package.py
new file mode 100644
index 0000000000..cb1277546c
--- /dev/null
+++ b/client/ayon_core/hosts/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_explicit_colorspace.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py
index 8e29a0048d..5fbb9a6f4c 100644
--- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py
+++ b/client/ayon_core/hosts/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/extract_editorial_pckg.py b/client/ayon_core/hosts/traypublisher/plugins/publish/extract_editorial_pckg.py
new file mode 100644
index 0000000000..6dd4e84704
--- /dev/null
+++ b/client/ayon_core/hosts/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/hosts/traypublisher/plugins/publish/validate_editorial_package.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_editorial_package.py
new file mode 100644
index 0000000000..c63c4a6a73
--- /dev/null
+++ b/client/ayon_core/hosts/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/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/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_user_credentials.py b/client/ayon_core/modules/deadline/plugins/publish/collect_user_credentials.py
index 5d03523c89..99d75ecb9e 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
@@ -76,6 +76,9 @@ class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin):
)
instance.data["deadline"]["auth"] = None
+ 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
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_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 0f505dce78..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)
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/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py
index a29866976c..865b566e6e 100644
--- a/client/ayon_core/plugins/publish/integrate.py
+++ b/client/ayon_core/plugins/publish/integrate.py
@@ -108,70 +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",
- "render.local.hou"
- ]
default_template_name = "publish"
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/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/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/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/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/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/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/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py
index 9e8e796aff..4a0c022f23 100644
--- a/server_addon/houdini/server/settings/publish.py
+++ b/server_addon/houdini/server/settings/publish.py
@@ -77,10 +77,6 @@ class PublishPluginsModel(BaseSettingsModel):
default_factory=CollectLocalRenderInstancesModel,
title="Collect Local Render Instances."
)
- ValidateContainers: BasicValidateModel = SettingsField(
- default_factory=BasicValidateModel,
- title="Validate Latest Containers.",
- section="Validators")
ValidateInstanceInContextHoudini: BasicValidateModel = SettingsField(
default_factory=BasicValidateModel,
title="Validate Instance is in same Context.")
@@ -119,11 +115,6 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = {
]
}
},
- "ValidateContainers": {
- "enabled": True,
- "optional": True,
- "active": True
- },
"ValidateInstanceInContextHoudini": {
"enabled": True,
"optional": 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/server_addon/nuke/package.py b/server_addon/nuke/package.py
index bf03c4e7e7..bc166bd14e 100644
--- a/server_addon/nuke/package.py
+++ b/server_addon/nuke/package.py
@@ -1,3 +1,3 @@
name = "nuke"
title = "Nuke"
-version = "0.1.11"
+version = "0.1.13"
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/server_addon/traypublisher/package.py b/server_addon/traypublisher/package.py
index 4ca8ae9fd3..c138a2296d 100644
--- a/server_addon/traypublisher/package.py
+++ b/server_addon/traypublisher/package.py
@@ -1,3 +1,3 @@
name = "traypublisher"
title = "TrayPublisher"
-version = "0.1.4"
+version = "0.1.5"
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"
+ ]
+ }
+ }
}
}