diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 387b5574ab..b6a243bcfe 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,7 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.16.3-nightly.3
- 3.16.3-nightly.2
- 3.16.3-nightly.1
- 3.16.2
@@ -134,7 +135,6 @@ body:
- 3.14.7-nightly.4
- 3.14.7-nightly.3
- 3.14.7-nightly.2
- - 3.14.7-nightly.1
validations:
required: true
- type: dropdown
diff --git a/openpype/client/mongo/entity_links.py b/openpype/client/mongo/entity_links.py
index c97a828118..fd13a2d83b 100644
--- a/openpype/client/mongo/entity_links.py
+++ b/openpype/client/mongo/entity_links.py
@@ -212,16 +212,12 @@ def _process_referenced_pipeline_result(result, link_type):
continue
for output in sorted(outputs_recursive, key=lambda o: o["depth"]):
- output_links = output.get("data", {}).get("inputLinks")
- if not output_links and output["type"] != "hero_version":
- continue
-
# Leaf
if output["_id"] not in correctly_linked_ids:
continue
_filter_input_links(
- output_links,
+ output.get("data", {}).get("inputLinks"),
link_type,
correctly_linked_ids
)
diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py
index c54acbc203..1418bc210b 100644
--- a/openpype/hooks/pre_add_last_workfile_arg.py
+++ b/openpype/hooks/pre_add_last_workfile_arg.py
@@ -1,6 +1,6 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class AddLastWorkfileToLaunchArgs(PreLaunchHook):
@@ -13,8 +13,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
# Execute after workfile template copy
order = 10
- app_groups = [
- "3dsmax",
+ app_groups = {
+ "3dsmax", "adsk_3dsmax",
"maya",
"nuke",
"nukex",
@@ -26,8 +26,9 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"photoshop",
"tvpaint",
"substancepainter",
- "aftereffects"
- ]
+ "aftereffects",
+ }
+ launch_types = {LaunchTypes.local}
def execute(self):
if not self.data.get("start_last_workfile"):
diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py
index 70c549919f..2203ff4396 100644
--- a/openpype/hooks/pre_copy_template_workfile.py
+++ b/openpype/hooks/pre_copy_template_workfile.py
@@ -1,7 +1,7 @@
import os
import shutil
-from openpype.lib import PreLaunchHook
from openpype.settings import get_project_settings
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.pipeline.workfile import (
get_custom_workfile_template,
get_custom_workfile_template_by_string_context
@@ -19,7 +19,8 @@ class CopyTemplateWorkfile(PreLaunchHook):
# Before `AddLastWorkfileToLaunchArgs`
order = 0
- app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"]
+ app_groups = {"blender", "photoshop", "tvpaint", "aftereffects"}
+ launch_types = {LaunchTypes.local}
def execute(self):
"""Check if can copy template for context and do it if possible.
diff --git a/openpype/hooks/pre_create_extra_workdir_folders.py b/openpype/hooks/pre_create_extra_workdir_folders.py
index 8856281120..4c9d08b375 100644
--- a/openpype/hooks/pre_create_extra_workdir_folders.py
+++ b/openpype/hooks/pre_create_extra_workdir_folders.py
@@ -1,5 +1,5 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.pipeline.workfile import create_workdir_extra_folders
@@ -14,6 +14,7 @@ class CreateWorkdirExtraFolders(PreLaunchHook):
# Execute after workfile template copy
order = 15
+ launch_types = {LaunchTypes.local}
def execute(self):
if not self.application.is_host:
diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py
index 21ec8e7881..7536df4c16 100644
--- a/openpype/hooks/pre_foundry_apps.py
+++ b/openpype/hooks/pre_foundry_apps.py
@@ -1,5 +1,5 @@
import subprocess
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class LaunchFoundryAppsWindows(PreLaunchHook):
@@ -13,8 +13,9 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
# Should be as last hook because must change launch arguments to string
order = 1000
- app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"]
- platforms = ["windows"]
+ app_groups = {"nuke", "nukeassist", "nukex", "hiero", "nukestudio"}
+ platforms = {"windows"}
+ launch_types = {LaunchTypes.local}
def execute(self):
# Change `creationflags` to CREATE_NEW_CONSOLE
diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py
index 260e28a18b..813df24af0 100644
--- a/openpype/hooks/pre_global_host_data.py
+++ b/openpype/hooks/pre_global_host_data.py
@@ -1,5 +1,5 @@
from openpype.client import get_project, get_asset_by_name
-from openpype.lib import (
+from openpype.lib.applications import (
PreLaunchHook,
EnvironmentPrepData,
prepare_app_environments,
@@ -10,6 +10,7 @@ from openpype.pipeline import Anatomy
class GlobalHostDataHook(PreLaunchHook):
order = -100
+ launch_types = set()
def execute(self):
"""Prepare global objects to `data` that will be used for sure."""
diff --git a/openpype/hooks/pre_mac_launch.py b/openpype/hooks/pre_mac_launch.py
index f85557a4f0..402e9a5517 100644
--- a/openpype/hooks/pre_mac_launch.py
+++ b/openpype/hooks/pre_mac_launch.py
@@ -1,5 +1,5 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class LaunchWithTerminal(PreLaunchHook):
@@ -12,7 +12,8 @@ class LaunchWithTerminal(PreLaunchHook):
"""
order = 1000
- platforms = ["darwin"]
+ platforms = {"darwin"}
+ launch_types = {LaunchTypes.local}
def execute(self):
executable = str(self.launch_context.executable)
diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py
index 043cb3c7f6..d9e912c826 100644
--- a/openpype/hooks/pre_non_python_host_launch.py
+++ b/openpype/hooks/pre_non_python_host_launch.py
@@ -1,10 +1,11 @@
import os
-from openpype.lib import (
+from openpype.lib import get_openpype_execute_args
+from openpype.lib.applications import (
+ get_non_python_host_kwargs,
PreLaunchHook,
- get_openpype_execute_args
+ LaunchTypes,
)
-from openpype.lib.applications import get_non_python_host_kwargs
from openpype import PACKAGE_DIR as OPENPYPE_DIR
@@ -16,9 +17,10 @@ class NonPythonHostHook(PreLaunchHook):
python script which launch the host. For these cases it is necessary to
prepend python (or openpype) executable and script path before application's.
"""
- app_groups = ["harmony", "photoshop", "aftereffects"]
+ app_groups = {"harmony", "photoshop", "aftereffects"}
order = 20
+ launch_types = {LaunchTypes.local}
def execute(self):
# Pop executable
@@ -54,4 +56,3 @@ class NonPythonHostHook(PreLaunchHook):
self.launch_context.kwargs = \
get_non_python_host_kwargs(self.launch_context.kwargs)
-
diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py
index 8f462665bc..1ac305b635 100644
--- a/openpype/hooks/pre_ocio_hook.py
+++ b/openpype/hooks/pre_ocio_hook.py
@@ -1,8 +1,6 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook
-from openpype.pipeline.colorspace import (
- get_imageio_config
-)
+from openpype.pipeline.colorspace import get_imageio_config
from openpype.pipeline.template_data import get_template_data_with_names
@@ -10,7 +8,7 @@ class OCIOEnvHook(PreLaunchHook):
"""Set OCIO environment variable for hosts that use OpenColorIO."""
order = 0
- hosts = [
+ hosts = {
"substancepainter",
"fusion",
"blender",
@@ -20,8 +18,9 @@ class OCIOEnvHook(PreLaunchHook):
"maya",
"nuke",
"hiero",
- "resolve"
- ]
+ "resolve",
+ }
+ launch_types = set()
def execute(self):
"""Hook entry method."""
diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py
index c70aa41dbe..bdb48e11f8 100644
--- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py
+++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py
@@ -1,11 +1,5 @@
import os
-import sys
-import six
-from openpype.lib import (
- get_ffmpeg_tool_path,
- run_subprocess,
-)
from openpype.pipeline import publish
from openpype.hosts.aftereffects.api import get_stub
diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py
index 559e9ae0ce..68c9bfdd57 100644
--- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py
+++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py
@@ -1,6 +1,6 @@
from pathlib import Path
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class AddPythonScriptToLaunchArgs(PreLaunchHook):
@@ -8,9 +8,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook):
# Append after file argument
order = 15
- app_groups = [
- "blender",
- ]
+ app_groups = {"blender"}
+ launch_types = {LaunchTypes.local}
def execute(self):
if not self.launch_context.data.get("python_scripts"):
diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py
index e5f66d2a26..777e383215 100644
--- a/openpype/hosts/blender/hooks/pre_pyside_install.py
+++ b/openpype/hosts/blender/hooks/pre_pyside_install.py
@@ -2,7 +2,7 @@ import os
import re
import subprocess
from platform import system
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class InstallPySideToBlender(PreLaunchHook):
@@ -16,7 +16,8 @@ class InstallPySideToBlender(PreLaunchHook):
blender's python packages.
"""
- app_groups = ["blender"]
+ app_groups = {"blender"}
+ launch_types = {LaunchTypes.local}
def execute(self):
# Prelaunch hook is not crucial
diff --git a/openpype/hosts/blender/hooks/pre_windows_console.py b/openpype/hosts/blender/hooks/pre_windows_console.py
index d6be45b225..2161b7a2f5 100644
--- a/openpype/hosts/blender/hooks/pre_windows_console.py
+++ b/openpype/hosts/blender/hooks/pre_windows_console.py
@@ -1,5 +1,5 @@
import subprocess
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class BlenderConsoleWindows(PreLaunchHook):
@@ -13,8 +13,9 @@ class BlenderConsoleWindows(PreLaunchHook):
# Should be as last hook because must change launch arguments to string
order = 1000
- app_groups = ["blender"]
- platforms = ["windows"]
+ app_groups = {"blender"}
+ platforms = {"windows"}
+ launch_types = {LaunchTypes.local}
def execute(self):
# Change `creationflags` to CREATE_NEW_CONSOLE
diff --git a/openpype/hosts/celaction/hooks/__init__.py b/openpype/hosts/celaction/hooks/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openpype/hosts/celaction/hooks/pre_celaction_setup.py b/openpype/hosts/celaction/hooks/pre_celaction_setup.py
index 96e784875c..83aeab7c58 100644
--- a/openpype/hosts/celaction/hooks/pre_celaction_setup.py
+++ b/openpype/hosts/celaction/hooks/pre_celaction_setup.py
@@ -2,20 +2,18 @@ import os
import shutil
import winreg
import subprocess
-from openpype.lib import PreLaunchHook, get_openpype_execute_args
-from openpype.hosts.celaction import scripts
-
-CELACTION_SCRIPTS_DIR = os.path.dirname(
- os.path.abspath(scripts.__file__)
-)
+from openpype.lib import get_openpype_execute_args
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
+from openpype.hosts.celaction import CELACTION_ROOT_DIR
class CelactionPrelaunchHook(PreLaunchHook):
"""
Bootstrap celacion with pype
"""
- app_groups = ["celaction"]
- platforms = ["windows"]
+ app_groups = {"celaction"}
+ platforms = {"windows"}
+ launch_types = {LaunchTypes.local}
def execute(self):
asset_doc = self.data["asset_doc"]
@@ -37,7 +35,9 @@ class CelactionPrelaunchHook(PreLaunchHook):
winreg.KEY_ALL_ACCESS
)
- path_to_cli = os.path.join(CELACTION_SCRIPTS_DIR, "publish_cli.py")
+ path_to_cli = os.path.join(
+ CELACTION_ROOT_DIR, "scripts", "publish_cli.py"
+ )
subprocess_args = get_openpype_execute_args("run", path_to_cli)
openpype_executable = subprocess_args.pop(0)
workfile_settings = self.get_workfile_settings()
@@ -122,9 +122,8 @@ class CelactionPrelaunchHook(PreLaunchHook):
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings
- openpype_celaction_dir = os.path.dirname(CELACTION_SCRIPTS_DIR)
template_path = os.path.join(
- openpype_celaction_dir,
+ CELACTION_ROOT_DIR,
"resources",
"celaction_template_scene.scn"
)
diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py
index 83110bb6b5..850569cfdd 100644
--- a/openpype/hosts/flame/hooks/pre_flame_setup.py
+++ b/openpype/hosts/flame/hooks/pre_flame_setup.py
@@ -6,13 +6,10 @@ import socket
from pprint import pformat
from openpype.lib import (
- PreLaunchHook,
get_openpype_username,
run_subprocess,
)
-from openpype.lib.applications import (
- ApplicationLaunchFailed
-)
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.hosts import flame as opflame
@@ -22,11 +19,12 @@ class FlamePrelaunch(PreLaunchHook):
Will make sure flame_script_dirs are copied to user's folder defined
in environment var FLAME_SCRIPT_DIR.
"""
- app_groups = ["flame"]
+ app_groups = {"flame"}
permissions = 0o777
wtc_script_path = os.path.join(
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
+ launch_types = {LaunchTypes.local}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py
index fd726ccda1..66b0f803aa 100644
--- a/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py
+++ b/openpype/hosts/fusion/hooks/pre_fusion_profile_hook.py
@@ -2,12 +2,16 @@ import os
import shutil
import platform
from pathlib import Path
-from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
from openpype.hosts.fusion import (
FUSION_HOST_DIR,
FUSION_VERSIONS_DICT,
get_fusion_version,
)
+from openpype.lib.applications import (
+ PreLaunchHook,
+ LaunchTypes,
+ ApplicationLaunchFailed,
+)
class FusionCopyPrefsPrelaunch(PreLaunchHook):
@@ -21,8 +25,9 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook):
Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs
"""
- app_groups = ["fusion"]
+ app_groups = {"fusion"}
order = 2
+ launch_types = {LaunchTypes.local}
def get_fusion_profile_name(self, profile_version) -> str:
# Returns 'Default', unless FUSION16_PROFILE is set
diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py
index f27cd1674b..576628e876 100644
--- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py
+++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py
@@ -1,5 +1,9 @@
import os
-from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
+from openpype.lib.applications import (
+ PreLaunchHook,
+ LaunchTypes,
+ ApplicationLaunchFailed,
+)
from openpype.hosts.fusion import (
FUSION_HOST_DIR,
FUSION_VERSIONS_DICT,
@@ -17,8 +21,9 @@ class FusionPrelaunch(PreLaunchHook):
Fusion 18 : Python 3.6 - 3.10
"""
- app_groups = ["fusion"]
+ app_groups = {"fusion"}
order = 1
+ launch_types = {LaunchTypes.local}
def execute(self):
# making sure python 3 is installed at provided path
diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py
index 05e52e2478..70c837205e 100644
--- a/openpype/hosts/houdini/api/plugin.py
+++ b/openpype/hosts/houdini/api/plugin.py
@@ -167,6 +167,7 @@ class HoudiniCreatorBase(object):
class HoudiniCreator(NewCreator, HoudiniCreatorBase):
"""Base class for most of the Houdini creator plugins."""
selected_nodes = []
+ settings_name = None
def create(self, subset_name, instance_data, pre_create_data):
try:
@@ -294,3 +295,21 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
"""
return [hou.ropNodeTypeCategory()]
+
+ def apply_settings(self, project_settings, system_settings):
+ """Method called on initialization of plugin to apply settings."""
+
+ settings_name = self.settings_name
+ if settings_name is None:
+ settings_name = self.__class__.__name__
+
+ settings = project_settings["houdini"]["create"]
+ settings = settings.get(settings_name)
+ if settings is None:
+ self.log.debug(
+ "No settings found for {}".format(self.__class__.__name__)
+ )
+ return
+
+ for key, value in settings.items():
+ setattr(self, key, value)
diff --git a/openpype/hosts/houdini/hooks/set_paths.py b/openpype/hosts/houdini/hooks/set_paths.py
index 04a33b1643..b23659e23b 100644
--- a/openpype/hosts/houdini/hooks/set_paths.py
+++ b/openpype/hosts/houdini/hooks/set_paths.py
@@ -1,4 +1,4 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class SetPath(PreLaunchHook):
@@ -6,7 +6,8 @@ class SetPath(PreLaunchHook):
Hook `GlobalHostDataHook` must be executed before this hook.
"""
- app_groups = ["houdini"]
+ app_groups = {"houdini"}
+ launch_types = {LaunchTypes.local}
def execute(self):
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py
index 8b310753d0..45ef9ea82f 100644
--- a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py
+++ b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py
@@ -13,6 +13,8 @@ class CreateArnoldAss(plugin.HoudiniCreator):
defaults = ["Main"]
# Default extension: `.ass` or `.ass.gz`
+ # however calling HoudiniCreator.create()
+ # will override it by the value in the project settings
ext = ".ass"
def create(self, subset_name, instance_data, pre_create_data):
diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py
index a1101fd045..a3f31e7e94 100644
--- a/openpype/hosts/houdini/plugins/create/create_bgeo.py
+++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py
@@ -8,7 +8,7 @@ from openpype.lib import EnumDef
class CreateBGEO(plugin.HoudiniCreator):
"""BGEO pointcache creator."""
identifier = "io.openpype.creators.houdini.bgeo"
- label = "BGEO PointCache"
+ label = "PointCache (Bgeo)"
family = "pointcache"
icon = "gears"
diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py
index 554d5f2016..7eaf2aff2b 100644
--- a/openpype/hosts/houdini/plugins/create/create_pointcache.py
+++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py
@@ -8,7 +8,7 @@ import hou
class CreatePointCache(plugin.HoudiniCreator):
"""Alembic ROP to pointcache"""
identifier = "io.openpype.creators.houdini.pointcache"
- label = "Point Cache"
+ label = "PointCache (Abc)"
family = "pointcache"
icon = "gears"
diff --git a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py b/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py
deleted file mode 100644
index 22746aabb0..0000000000
--- a/openpype/hosts/houdini/plugins/publish/validate_bgeo_file_sop_path.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Validator plugin for SOP Path in bgeo isntance."""
-import pyblish.api
-from openpype.pipeline import PublishValidationError
-
-
-class ValidateNoSOPPath(pyblish.api.InstancePlugin):
- """Validate if SOP Path in BGEO instance exists."""
-
- order = pyblish.api.ValidatorOrder
- families = ["bgeo"]
- label = "Validate BGEO SOP Path"
-
- def process(self, instance):
-
- import hou
-
- node = hou.node(instance.data.get("instance_node"))
- sop_path = node.evalParm("soppath")
- if not sop_path:
- raise PublishValidationError(
- ("Empty SOP Path ('soppath' parameter) found in "
- f"the BGEO instance Geometry - {node.path()}"))
- if not isinstance(hou.node(sop_path), hou.SopNode):
- raise PublishValidationError(
- "SOP path is not pointing to valid SOP node.")
diff --git a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py
index 543c8e1407..afe05e3173 100644
--- a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py
+++ b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py
@@ -7,8 +7,6 @@ from openpype.pipeline import (
)
from openpype.pipeline.publish import RepairAction
-from openpype.pipeline.publish import RepairAction
-
class ValidateWorkfilePaths(
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py
index 4fcf4fef21..5fb8334d4b 100644
--- a/openpype/hosts/max/hooks/force_startup_script.py
+++ b/openpype/hosts/max/hooks/force_startup_script.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
"""Pre-launch to force 3ds max startup script."""
-from openpype.lib import PreLaunchHook
import os
+from openpype.hosts.max import MAX_HOST_DIR
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class ForceStartupScript(PreLaunchHook):
@@ -13,12 +14,14 @@ class ForceStartupScript(PreLaunchHook):
Hook `GlobalHostDataHook` must be executed before this hook.
"""
- app_groups = ["3dsmax"]
+ app_groups = {"3dsmax", "adsk_3dsmax"}
order = 11
+ launch_types = {LaunchTypes.local}
def execute(self):
startup_args = [
"-U",
"MAXScript",
- f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa
+ os.path.join(MAX_HOST_DIR, "startup", "startup.ms"),
+ ]
self.launch_context.launch_args.append(startup_args)
diff --git a/openpype/hosts/max/hooks/inject_python.py b/openpype/hosts/max/hooks/inject_python.py
index d9753ccbd8..e9dddbf710 100644
--- a/openpype/hosts/max/hooks/inject_python.py
+++ b/openpype/hosts/max/hooks/inject_python.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Pre-launch hook to inject python environment."""
-from openpype.lib import PreLaunchHook
import os
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class InjectPythonPath(PreLaunchHook):
@@ -13,7 +13,8 @@ class InjectPythonPath(PreLaunchHook):
Hook `GlobalHostDataHook` must be executed before this hook.
"""
- app_groups = ["3dsmax"]
+ app_groups = {"3dsmax", "adsk_3dsmax"}
+ launch_types = {LaunchTypes.local}
def execute(self):
self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"]
diff --git a/openpype/hosts/max/hooks/set_paths.py b/openpype/hosts/max/hooks/set_paths.py
index 3db5306344..4b961fa91e 100644
--- a/openpype/hosts/max/hooks/set_paths.py
+++ b/openpype/hosts/max/hooks/set_paths.py
@@ -1,4 +1,4 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class SetPath(PreLaunchHook):
@@ -6,7 +6,8 @@ class SetPath(PreLaunchHook):
Hook `GlobalHostDataHook` must be executed before this hook.
"""
- app_groups = ["max"]
+ app_groups = {"max"}
+ launch_types = {LaunchTypes.local}
def execute(self):
workdir = self.launch_context.env.get("AVALON_WORKDIR", "")
diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py
index 689d7adb4f..4b1ea698a6 100644
--- a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py
+++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py
@@ -1,4 +1,4 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class MayaPreAutoLoadPlugins(PreLaunchHook):
@@ -6,7 +6,8 @@ class MayaPreAutoLoadPlugins(PreLaunchHook):
# Before AddLastWorkfileToLaunchArgs
order = 9
- app_groups = ["maya"]
+ app_groups = {"maya"}
+ launch_types = {LaunchTypes.local}
def execute(self):
diff --git a/openpype/hosts/maya/hooks/pre_copy_mel.py b/openpype/hosts/maya/hooks/pre_copy_mel.py
index 9cea829ad7..0fb5af149a 100644
--- a/openpype/hosts/maya/hooks/pre_copy_mel.py
+++ b/openpype/hosts/maya/hooks/pre_copy_mel.py
@@ -1,4 +1,4 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.hosts.maya.lib import create_workspace_mel
@@ -7,7 +7,8 @@ class PreCopyMel(PreLaunchHook):
Hook `GlobalHostDataHook` must be executed before this hook.
"""
- app_groups = ["maya"]
+ app_groups = {"maya"}
+ launch_types = {LaunchTypes.local}
def execute(self):
project_doc = self.data["project_doc"]
diff --git a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py
index 7582ce0591..1fe3c3ca2c 100644
--- a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py
+++ b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py
@@ -1,4 +1,4 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class MayaPreOpenWorkfilePostInitialization(PreLaunchHook):
@@ -6,7 +6,8 @@ class MayaPreOpenWorkfilePostInitialization(PreLaunchHook):
# Before AddLastWorkfileToLaunchArgs.
order = 9
- app_groups = ["maya"]
+ app_groups = {"maya"}
+ launch_types = {LaunchTypes.local}
def execute(self):
diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py
index 364c8eeff4..9942229155 100644
--- a/openpype/hosts/nuke/api/lib.py
+++ b/openpype/hosts/nuke/api/lib.py
@@ -424,10 +424,13 @@ def add_publish_knob(node):
return node
-@deprecated
+@deprecated("openpype.hosts.nuke.api.lib.set_node_data")
def set_avalon_knob_data(node, data=None, prefix="avalon:"):
"""[DEPRECATED] Sets data into nodes's avalon knob
+ This function is still used but soon will be deprecated.
+ Use `set_node_data` instead.
+
Arguments:
node (nuke.Node): Nuke node to imprint with data,
data (dict, optional): Data to be imprinted into AvalonTab
@@ -487,10 +490,13 @@ def set_avalon_knob_data(node, data=None, prefix="avalon:"):
return node
-@deprecated
+@deprecated("openpype.hosts.nuke.api.lib.get_node_data")
def get_avalon_knob_data(node, prefix="avalon:", create=True):
"""[DEPRECATED] Gets a data from nodes's avalon knob
+ This function is still used but soon will be deprecated.
+ Use `get_node_data` instead.
+
Arguments:
node (obj): Nuke node to search for data,
prefix (str, optional): filtering prefix
@@ -1699,7 +1705,7 @@ def create_write_node_legacy(
knob_value = float(knob_value)
if knob_type == "bool":
knob_value = bool(knob_value)
- if knob_type in ["2d_vector", "3d_vector"]:
+ if knob_type in ["2d_vector", "3d_vector", "color", "box"]:
knob_value = list(knob_value)
GN[knob_name].setValue(knob_value)
@@ -1715,7 +1721,7 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs):
Args:
node (nuke.Node): nuke node
knob_settings (list): list of dict. Keys are `type`, `name`, `value`
- kwargs (dict)[optional]: keys for formatable knob settings
+ kwargs (dict)[optional]: keys for formattable knob settings
"""
for knob in knob_settings:
log.debug("__ knob: {}".format(pformat(knob)))
@@ -1732,7 +1738,7 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs):
)
continue
- # first deal with formatable knob settings
+ # first deal with formattable knob settings
if knob_type == "formatable":
template = knob["template"]
to_type = knob["to_type"]
@@ -1741,8 +1747,8 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs):
**kwargs
)
except KeyError as msg:
- log.warning("__ msg: {}".format(msg))
- raise KeyError(msg)
+ raise KeyError(
+ "Not able to format expression: {}".format(msg))
# convert value to correct type
if to_type == "2d_vector":
@@ -1781,8 +1787,8 @@ def convert_knob_value_to_correct_type(knob_type, knob_value):
knob_value = knob_value
elif knob_type == "color_gui":
knob_value = color_gui_to_int(knob_value)
- elif knob_type in ["2d_vector", "3d_vector", "color"]:
- knob_value = [float(v) for v in knob_value]
+ elif knob_type in ["2d_vector", "3d_vector", "color", "box"]:
+ knob_value = [float(val_) for val_ in knob_value]
return knob_value
@@ -2204,7 +2210,6 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
continue
preset_clrsp = input["colorspace"]
- log.debug(preset_clrsp)
if preset_clrsp is not None:
current = n["colorspace"].value()
future = str(preset_clrsp)
@@ -2686,7 +2691,15 @@ def _launch_workfile_app():
host_tools.show_workfiles(parent=None, on_top=True)
+@deprecated("openpype.hosts.nuke.api.lib.start_workfile_template_builder")
def process_workfile_builder():
+ """ [DEPRECATED] Process workfile builder on nuke start
+
+ This function is deprecated and will be removed in future versions.
+ Use settings for `project_settings/nuke/templated_workfile_build` which are
+ supported by api `start_workfile_template_builder()`.
+ """
+
# to avoid looping of the callback, remove it!
nuke.removeOnCreate(process_workfile_builder, nodeClass="Root")
@@ -2695,11 +2708,6 @@ def process_workfile_builder():
workfile_builder = project_settings["nuke"].get(
"workfile_builder", {})
- # get all imortant settings
- openlv_on = env_value_to_bool(
- env_key="AVALON_OPEN_LAST_WORKFILE",
- default=None)
-
# get settings
createfv_on = workfile_builder.get("create_first_version") or None
builder_on = workfile_builder.get("builder_on_start") or None
@@ -2740,20 +2748,15 @@ def process_workfile_builder():
save_file(last_workfile_path)
return
- # skip opening of last version if it is not enabled
- if not openlv_on or not os.path.exists(last_workfile_path):
- return
-
- log.info("Opening last workfile...")
- # open workfile
- open_file(last_workfile_path)
-
def start_workfile_template_builder():
from .workfile_template_builder import (
build_workfile_template
)
+ # remove callback since it would be duplicating the workfile
+ nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root")
+
# to avoid looping of the callback, remove it!
log.info("Starting workfile template builder...")
try:
@@ -2761,8 +2764,6 @@ def start_workfile_template_builder():
except TemplateProfileNotFound:
log.warning("Template profile not found. Skipping...")
- # remove callback since it would be duplicating the workfile
- nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root")
@deprecated
def recreate_instance(origin_node, avalon_data=None):
diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py
index fcc3becd2d..65b4b91323 100644
--- a/openpype/hosts/nuke/api/pipeline.py
+++ b/openpype/hosts/nuke/api/pipeline.py
@@ -34,6 +34,7 @@ from .lib import (
get_main_window,
add_publish_knob,
WorkfileSettings,
+ # TODO: remove this once workfile builder will be removed
process_workfile_builder,
start_workfile_template_builder,
launch_workfiles_app,
@@ -155,11 +156,18 @@ def add_nuke_callbacks():
"""
nuke_settings = get_current_project_settings()["nuke"]
workfile_settings = WorkfileSettings()
+
# Set context settings.
nuke.addOnCreate(
workfile_settings.set_context_settings, nodeClass="Root")
+
+ # adding favorites to file browser
nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root")
+
+ # template builder callbacks
nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root")
+
+ # TODO: remove this callback once workfile builder will be removed
nuke.addOnCreate(process_workfile_builder, nodeClass="Root")
# fix ffmpeg settings on script
@@ -169,11 +177,12 @@ def add_nuke_callbacks():
nuke.addOnScriptLoad(check_inventory_versions)
nuke.addOnScriptSave(check_inventory_versions)
- # # set apply all workfile settings on script load and save
+ # set apply all workfile settings on script load and save
nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
+
if nuke_settings["nuke-dirmap"]["enabled"]:
- log.info("Added Nuke's dirmaping callback ...")
+ log.info("Added Nuke's dir-mapping callback ...")
# Add dirmap for file paths.
nuke.addFilenameFilter(dirmap_file_name_filter)
@@ -539,6 +548,8 @@ def list_instances(creator_id=None):
"""
instances_by_order = defaultdict(list)
subset_instances = []
+ instance_ids = set()
+
for node in nuke.allNodes(recurseGroups=True):
if node.Class() in ["Viewer", "Dot"]:
@@ -564,6 +575,14 @@ def list_instances(creator_id=None):
if creator_id and instance_data["creator_identifier"] != creator_id:
continue
+ if instance_data["instance_id"] in instance_ids:
+ instance_data.pop("instance_id")
+ else:
+ instance_ids.add(instance_data["instance_id"])
+
+ # node name could change, so update subset name data
+ _update_subset_name_data(instance_data, node)
+
if "render_order" not in node.knobs():
subset_instances.append((node, instance_data))
continue
@@ -572,23 +591,43 @@ def list_instances(creator_id=None):
instances_by_order[order].append((node, instance_data))
# Sort instances based on order attribute or subset name.
+ # TODO: remove in future Publisher enhanced with sorting
ordered_instances = []
for key in sorted(instances_by_order.keys()):
- instances_by_subset = {}
- for node, data in instances_by_order[key]:
- instances_by_subset[data["subset"]] = (node, data)
+ instances_by_subset = defaultdict(list)
+ for node, data_ in instances_by_order[key]:
+ instances_by_subset[data_["subset"]].append((node, data_))
for subkey in sorted(instances_by_subset.keys()):
- ordered_instances.append(instances_by_subset[subkey])
+ ordered_instances.extend(instances_by_subset[subkey])
- instances_by_subset = {}
- for node, data in subset_instances:
- instances_by_subset[data["subset"]] = (node, data)
+ instances_by_subset = defaultdict(list)
+ for node, data_ in subset_instances:
+ instances_by_subset[data_["subset"]].append((node, data_))
for key in sorted(instances_by_subset.keys()):
- ordered_instances.append(instances_by_subset[key])
+ ordered_instances.extend(instances_by_subset[key])
return ordered_instances
+def _update_subset_name_data(instance_data, node):
+ """Update subset name data in instance data.
+
+ Args:
+ instance_data (dict): instance creator data
+ node (nuke.Node): nuke node
+ """
+ # make sure node name is subset name
+ old_subset_name = instance_data["subset"]
+ old_variant = instance_data["variant"]
+ subset_name_root = old_subset_name.replace(old_variant, "")
+
+ new_subset_name = node.name()
+ new_variant = new_subset_name.replace(subset_name_root, "")
+
+ instance_data["subset"] = new_subset_name
+ instance_data["variant"] = new_variant
+
+
def remove_instance(instance):
"""Remove instance from current workfile metadata.
diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py
index cfdb407d26..3975f2e57c 100644
--- a/openpype/hosts/nuke/api/plugin.py
+++ b/openpype/hosts/nuke/api/plugin.py
@@ -212,9 +212,15 @@ class NukeCreator(NewCreator):
created_instance["creator_attributes"].pop(key)
def update_instances(self, update_list):
- for created_inst, _changes in update_list:
+ for created_inst, changes in update_list:
instance_node = created_inst.transient_data["node"]
+ # update instance node name if subset name changed
+ if "subset" in changes.changed_keys:
+ instance_node["name"].setValue(
+ changes["subset"].new_value
+ )
+
# in case node is not existing anymore (user erased it manually)
try:
instance_node.fullName()
@@ -256,6 +262,17 @@ class NukeWriteCreator(NukeCreator):
family = "write"
icon = "sign-out"
+ def get_linked_knobs(self):
+ linked_knobs = []
+ if "channels" in self.instance_attributes:
+ linked_knobs.append("channels")
+ if "ordered" in self.instance_attributes:
+ linked_knobs.append("render_order")
+ if "use_range_limit" in self.instance_attributes:
+ linked_knobs.extend(["___", "first", "last", "use_limit"])
+
+ return linked_knobs
+
def integrate_links(self, node, outputs=True):
# skip if no selection
if not self.selected_node:
@@ -921,7 +938,11 @@ class ExporterReviewMov(ExporterReview):
except Exception:
self.log.info("`mov64_codec` knob was not found")
- write_node["mov64_write_timecode"].setValue(1)
+ try:
+ write_node["mov64_write_timecode"].setValue(1)
+ except Exception:
+ self.log.info("`mov64_write_timecode` knob was not found")
+
write_node["raw"].setValue(1)
# connect
write_node.setInput(0, self.previous_node)
diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py
index 3948a665c6..657291ec51 100644
--- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py
+++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py
@@ -1,11 +1,12 @@
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook
class PrelaunchNukeAssistHook(PreLaunchHook):
"""
Adding flag when nukeassist
"""
- app_groups = ["nukeassist"]
+ app_groups = {"nukeassist"}
+ launch_types = set()
def execute(self):
self.launch_context.env["NUKEASSIST"] = "1"
diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py
index 0c8adfb75c..8c18739587 100644
--- a/openpype/hosts/nuke/plugins/create/create_write_image.py
+++ b/openpype/hosts/nuke/plugins/create/create_write_image.py
@@ -64,9 +64,6 @@ class CreateWriteImage(napi.NukeWriteCreator):
)
def create_instance_node(self, subset_name, instance_data):
- linked_knobs_ = []
- if "use_range_limit" in self.instance_attributes:
- linked_knobs_ = ["channels", "___", "first", "last", "use_limit"]
# add fpath_template
write_data = {
@@ -81,7 +78,7 @@ class CreateWriteImage(napi.NukeWriteCreator):
write_data,
input=self.selected_node,
prenodes=self.prenodes,
- linked_knobs=linked_knobs_,
+ linked_knobs=self.get_linked_knobs(),
**{
"frame": nuke.frame()
}
diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py
index c3bba5f477..395c3b002f 100644
--- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py
+++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py
@@ -45,12 +45,6 @@ class CreateWritePrerender(napi.NukeWriteCreator):
return attr_defs
def create_instance_node(self, subset_name, instance_data):
- linked_knobs_ = []
- if "use_range_limit" in self.instance_attributes:
- linked_knobs_ = ["channels", "___", "first", "last", "use_limit"]
-
- linked_knobs_.append("render_order")
-
# add fpath_template
write_data = {
"creator": self.__class__.__name__,
@@ -73,7 +67,7 @@ class CreateWritePrerender(napi.NukeWriteCreator):
write_data,
input=self.selected_node,
prenodes=self.prenodes,
- linked_knobs=linked_knobs_,
+ linked_knobs=self.get_linked_knobs(),
**{
"width": width,
"height": height
diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py
index aef4b06a2c..91acf4eabc 100644
--- a/openpype/hosts/nuke/plugins/create/create_write_render.py
+++ b/openpype/hosts/nuke/plugins/create/create_write_render.py
@@ -39,10 +39,6 @@ class CreateWriteRender(napi.NukeWriteCreator):
return attr_defs
def create_instance_node(self, subset_name, instance_data):
- linked_knobs_ = [
- "channels", "___", "first", "last", "use_limit", "render_order"
- ]
-
# add fpath_template
write_data = {
"creator": self.__class__.__name__,
@@ -60,12 +56,15 @@ class CreateWriteRender(napi.NukeWriteCreator):
actual_format = nuke.root().knob('format').value()
width, height = (actual_format.width(), actual_format.height())
+ self.log.debug(">>>>>>> : {}".format(self.instance_attributes))
+ self.log.debug(">>>>>>> : {}".format(self.get_linked_knobs()))
+
created_node = napi.create_write_node(
subset_name,
write_data,
input=self.selected_node,
prenodes=self.prenodes,
- linked_knobs=linked_knobs_,
+ linked_knobs=self.get_linked_knobs(),
**{
"width": width,
"height": height
diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py
index 5539324fb7..19038b168d 100644
--- a/openpype/hosts/nuke/plugins/load/load_clip.py
+++ b/openpype/hosts/nuke/plugins/load/load_clip.py
@@ -91,14 +91,14 @@ class LoadClip(plugin.NukeLoader):
# reset container id so it is always unique for each instance
self.reset_container_id()
- self.log.warning(self.extensions)
-
is_sequence = len(representation["files"]) > 1
if is_sequence:
- representation = self._representation_with_hash_in_frame(
- representation
+ context["representation"] = \
+ self._representation_with_hash_in_frame(
+ representation
)
+
filepath = self.filepath_from_context(context)
filepath = filepath.replace("\\", "/")
self.log.debug("_ filepath: {}".format(filepath))
@@ -260,6 +260,7 @@ class LoadClip(plugin.NukeLoader):
representation = self._representation_with_hash_in_frame(
representation
)
+
filepath = get_representation_path(representation).replace("\\", "/")
self.log.debug("_ filepath: {}".format(filepath))
diff --git a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py
index bc03baad8d..73f5ac75b1 100644
--- a/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py
+++ b/openpype/hosts/resolve/hooks/pre_resolve_last_workfile.py
@@ -1,5 +1,5 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class PreLaunchResolveLastWorkfile(PreLaunchHook):
@@ -9,7 +9,8 @@ class PreLaunchResolveLastWorkfile(PreLaunchHook):
workfile. This property is set explicitly in Launcher.
"""
order = 10
- app_groups = ["resolve"]
+ app_groups = {"resolve"}
+ launch_types = {LaunchTypes.local}
def execute(self):
if not self.data.get("start_last_workfile"):
diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py
index 3fd39d665c..326f37dffc 100644
--- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py
+++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py
@@ -1,7 +1,7 @@
import os
from pathlib import Path
import platform
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.hosts.resolve.utils import setup
@@ -30,7 +30,8 @@ class PreLaunchResolveSetup(PreLaunchHook):
"""
- app_groups = ["resolve"]
+ app_groups = {"resolve"}
+ launch_types = {LaunchTypes.local}
def execute(self):
current_platform = platform.system().lower()
diff --git a/openpype/hosts/resolve/hooks/pre_resolve_startup.py b/openpype/hosts/resolve/hooks/pre_resolve_startup.py
index 599e0c0008..6dbfd09a37 100644
--- a/openpype/hosts/resolve/hooks/pre_resolve_startup.py
+++ b/openpype/hosts/resolve/hooks/pre_resolve_startup.py
@@ -1,6 +1,6 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
import openpype.hosts.resolve
@@ -9,7 +9,8 @@ class PreLaunchResolveStartup(PreLaunchHook):
"""
order = 11
- app_groups = ["resolve"]
+ app_groups = {"resolve"}
+ launch_types = {LaunchTypes.local}
def execute(self):
# Set the openpype prelaunch startup script path for easy access
diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
index c31403437a..a1c946b60b 100644
--- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py
+++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
@@ -1,7 +1,5 @@
-from openpype.lib import (
- PreLaunchHook,
- get_openpype_execute_args
-)
+from openpype.lib import get_openpype_execute_args
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
class TvpaintPrelaunchHook(PreLaunchHook):
@@ -13,7 +11,8 @@ class TvpaintPrelaunchHook(PreLaunchHook):
Existence of last workfile is checked. If workfile does not exists tries
to copy templated workfile from predefined path.
"""
- app_groups = ["tvpaint"]
+ app_groups = {"tvpaint"}
+ launch_types = {LaunchTypes.local}
def execute(self):
# Pop tvpaint executable
diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py
index 3225d742a3..fcc5d98ab6 100644
--- a/openpype/hosts/unreal/addon.py
+++ b/openpype/hosts/unreal/addon.py
@@ -12,6 +12,11 @@ class UnrealAddon(OpenPypeModule, IHostAddon):
def initialize(self, module_settings):
self.enabled = True
+ def get_global_environments(self):
+ return {
+ "AYON_UNREAL_ROOT": UNREAL_ROOT_DIR,
+ }
+
def add_implementation_envs(self, env, app):
"""Modify environments to contain all required for implementation."""
# Set AYON_UNREAL_PLUGIN required for Unreal implementation
diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py
index e5010366b8..202d7854f6 100644
--- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py
+++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py
@@ -7,9 +7,10 @@ from pathlib import Path
from qtpy import QtCore
from openpype import resources
-from openpype.lib import (
+from openpype.lib.applications import (
PreLaunchHook,
ApplicationLaunchFailed,
+ LaunchTypes,
)
from openpype.pipeline.workfile import get_workfile_template_key
import openpype.hosts.unreal.lib as unreal_lib
@@ -29,6 +30,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
shell script.
"""
+ app_groups = {"unreal"}
+ launch_types = {LaunchTypes.local}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration
index ff15c70077..63266607ce 160000
--- a/openpype/hosts/unreal/integration
+++ b/openpype/hosts/unreal/integration
@@ -1 +1 @@
-Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986
+Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757
diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py
index 0c39773c19..6d544f65b2 100644
--- a/openpype/hosts/unreal/lib.py
+++ b/openpype/hosts/unreal/lib.py
@@ -369,11 +369,11 @@ def get_compatible_integration(
def get_path_to_cmdlet_project(ue_version: str) -> Path:
cmd_project = Path(
- os.path.abspath(os.getenv("OPENPYPE_ROOT")))
+ os.path.dirname(os.path.abspath(__file__)))
# For now, only tested on Windows (For Linux and Mac
# it has to be implemented)
- cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}"
+ cmd_project /= f"integration/UE_{ue_version}"
# if the integration doesn't exist for current engine version
# try to find the closest to it.
diff --git a/openpype/hosts/webpublisher/publish_functions.py b/openpype/hosts/webpublisher/publish_functions.py
index 83f53ced68..41aab68cce 100644
--- a/openpype/hosts/webpublisher/publish_functions.py
+++ b/openpype/hosts/webpublisher/publish_functions.py
@@ -6,7 +6,7 @@ import pyblish.util
from openpype.lib import Logger
from openpype.lib.applications import (
ApplicationManager,
- get_app_environments_for_context,
+ LaunchTypes,
)
from openpype.pipeline import install_host
from openpype.hosts.webpublisher.api import WebpublisherHost
@@ -156,22 +156,31 @@ def cli_publish_from_app(
found_variant_key = find_variant_key(application_manager, host_name)
app_name = "{}/{}".format(host_name, found_variant_key)
+ data = {
+ "last_workfile_path": workfile_path,
+ "start_last_workfile": True,
+ "project_name": project_name,
+ "asset_name": asset_name,
+ "task_name": task_name,
+ "launch_type": LaunchTypes.automated,
+ }
+ launch_context = application_manager.create_launch_context(
+ app_name, **data)
+ launch_context.run_prelaunch_hooks()
+
# must have for proper launch of app
- env = get_app_environments_for_context(
- project_name,
- asset_name,
- task_name,
- app_name
- )
+ env = launch_context.env
print("env:: {}".format(env))
+ env["OPENPYPE_PUBLISH_DATA"] = batch_path
+ # must pass identifier to update log lines for a batch
+ env["BATCH_LOG_ID"] = str(_id)
+ env["HEADLESS_PUBLISH"] = 'true' # to use in app lib
+ env["USER_EMAIL"] = user_email
+
os.environ.update(env)
- os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path
- # must pass identifier to update log lines for a batch
- os.environ["BATCH_LOG_ID"] = str(_id)
- os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
- os.environ["USER_EMAIL"] = user_email
-
+ # Why is this here? Registered host in this process does not affect
+ # regitered host in launched process.
pyblish.api.register_host(host_name)
if targets:
if isinstance(targets, str):
@@ -184,15 +193,7 @@ def cli_publish_from_app(
os.environ["PYBLISH_TARGETS"] = os.pathsep.join(
set(current_targets))
- data = {
- "last_workfile_path": workfile_path,
- "start_last_workfile": True,
- "project_name": project_name,
- "asset_name": asset_name,
- "task_name": task_name
- }
-
- launched_app = application_manager.launch(app_name, **data)
+ launched_app = application_manager.launch_with_context(launch_context)
timeout = get_timeout(project_name, host_name, task_type)
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index fac3e33f71..ff5e27c122 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -12,10 +12,6 @@ from abc import ABCMeta, abstractmethod
import six
from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR
-from openpype.client import (
- get_project,
- get_asset_by_name,
-)
from openpype.settings import (
get_system_settings,
get_project_settings,
@@ -47,6 +43,25 @@ CUSTOM_LAUNCH_APP_GROUPS = {
}
+class LaunchTypes:
+ """Launch types are filters for pre/post-launch hooks.
+
+ Please use these variables in case they'll change values.
+ """
+
+ # Local launch - application is launched on local machine
+ local = "local"
+ # Farm render job - application is on farm
+ farm_render = "farm-render"
+ # Farm publish job - integration post-render job
+ farm_publish = "farm-publish"
+ # Remote launch - application is launched on remote machine from which
+ # can be started publishing
+ remote = "remote"
+ # Automated launch - application is launched with automated publishing
+ automated = "automated"
+
+
def parse_environments(env_data, env_group=None, platform_name=None):
"""Parse environment values from settings byt group and platform.
@@ -483,6 +498,42 @@ class ApplicationManager:
break
return output
+ def create_launch_context(self, app_name, **data):
+ """Prepare launch context for application.
+
+ Args:
+ app_name (str): Name of application that should be launched.
+ **data (Any): Any additional data. Data may be used during
+
+ Returns:
+ ApplicationLaunchContext: Launch context for application.
+
+ Raises:
+ ApplicationNotFound: Application was not found by entered name.
+ """
+
+ app = self.applications.get(app_name)
+ if not app:
+ raise ApplicationNotFound(app_name)
+
+ executable = app.find_executable()
+
+ return ApplicationLaunchContext(
+ app, executable, **data
+ )
+
+ def launch_with_context(self, launch_context):
+ """Launch application using existing launch context.
+
+ Args:
+ launch_context (ApplicationLaunchContext): Prepared launch
+ context.
+ """
+
+ if not launch_context.executable:
+ raise ApplictionExecutableNotFound(launch_context.application)
+ return launch_context.launch()
+
def launch(self, app_name, **data):
"""Launch procedure.
@@ -503,18 +554,10 @@ class ApplicationManager:
failed. Exception should contain explanation message,
traceback should not be needed.
"""
- app = self.applications.get(app_name)
- if not app:
- raise ApplicationNotFound(app_name)
- executable = app.find_executable()
- if not executable:
- raise ApplictionExecutableNotFound(app)
+ context = self.create_launch_context(app_name, **data)
+ return self.launch_with_context(context)
- context = ApplicationLaunchContext(
- app, executable, **data
- )
- return context.launch()
class EnvironmentToolGroup:
@@ -736,13 +779,17 @@ class LaunchHook:
# Order of prelaunch hook, will be executed as last if set to None.
order = None
# List of host implementations, skipped if empty.
- hosts = []
- # List of application groups
- app_groups = []
- # List of specific application names
- app_names = []
- # List of platform availability, skipped if empty.
- platforms = []
+ hosts = set()
+ # Set of application groups
+ app_groups = set()
+ # Set of specific application names
+ app_names = set()
+ # Set of platform availability
+ platforms = set()
+ # Set of launch types for which is available
+ # - if empty then is available for all launch types
+ # - by default has 'local' which is most common reason for launc hooks
+ launch_types = {LaunchTypes.local}
def __init__(self, launch_context):
"""Constructor of launch hook.
@@ -790,6 +837,10 @@ class LaunchHook:
if launch_context.app_name not in cls.app_names:
return False
+ if cls.launch_types:
+ if launch_context.launch_type not in cls.launch_types:
+ return False
+
return True
@property
@@ -859,9 +910,9 @@ class PostLaunchHook(LaunchHook):
class ApplicationLaunchContext:
"""Context of launching application.
- Main purpose of context is to prepare launch arguments and keyword arguments
- for new process. Most important part of keyword arguments preparations
- are environment variables.
+ Main purpose of context is to prepare launch arguments and keyword
+ arguments for new process. Most important part of keyword arguments
+ preparations are environment variables.
During the whole process is possible to use `data` attribute to store
object usable in multiple places.
@@ -874,14 +925,30 @@ class ApplicationLaunchContext:
insert argument between `nuke.exe` and `--NukeX`. To keep them together
it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`.
+ Notes:
+ It is possible to use launch context only to prepare environment
+ variables. In that case `executable` may be None and can be used
+ 'run_prelaunch_hooks' method to run prelaunch hooks which prepare
+ them.
+
Args:
application (Application): Application definition.
executable (ApplicationExecutable): Object with path to executable.
+ env_group (Optional[str]): Environment variable group. If not set
+ 'DEFAULT_ENV_SUBGROUP' is used.
+ launch_type (Optional[str]): Launch type. If not set 'local' is used.
**data (dict): Any additional data. Data may be used during
preparation to store objects usable in multiple places.
"""
- def __init__(self, application, executable, env_group=None, **data):
+ def __init__(
+ self,
+ application,
+ executable,
+ env_group=None,
+ launch_type=None,
+ **data
+ ):
from openpype.modules import ModulesManager
# Application object
@@ -896,6 +963,10 @@ class ApplicationLaunchContext:
self.executable = executable
+ if launch_type is None:
+ launch_type = LaunchTypes.local
+ self.launch_type = launch_type
+
if env_group is None:
env_group = DEFAULT_ENV_SUBGROUP
@@ -903,8 +974,11 @@ class ApplicationLaunchContext:
self.data = dict(data)
+ launch_args = []
+ if executable is not None:
+ launch_args = executable.as_args()
# subprocess.Popen launch arguments (first argument in constructor)
- self.launch_args = executable.as_args()
+ self.launch_args = launch_args
self.launch_args.extend(application.arguments)
if self.data.get("app_args"):
self.launch_args.extend(self.data.pop("app_args"))
@@ -946,6 +1020,7 @@ class ApplicationLaunchContext:
self.postlaunch_hooks = None
self.process = None
+ self._prelaunch_hooks_executed = False
@property
def env(self):
@@ -1215,6 +1290,27 @@ class ApplicationLaunchContext:
# Return process which is already terminated
return process
+ def run_prelaunch_hooks(self):
+ """Run prelaunch hooks.
+
+ This method will be executed only once, any future calls will skip
+ the processing.
+ """
+
+ if self._prelaunch_hooks_executed:
+ self.log.warning("Prelaunch hooks were already executed.")
+ return
+ # Discover launch hooks
+ self.discover_launch_hooks()
+
+ # Execute prelaunch hooks
+ for prelaunch_hook in self.prelaunch_hooks:
+ self.log.debug("Executing prelaunch hook: {}".format(
+ str(prelaunch_hook.__class__.__name__)
+ ))
+ prelaunch_hook.execute()
+ self._prelaunch_hooks_executed = True
+
def launch(self):
"""Collect data for new process and then create it.
@@ -1227,15 +1323,8 @@ class ApplicationLaunchContext:
self.log.warning("Application was already launched.")
return
- # Discover launch hooks
- self.discover_launch_hooks()
-
- # Execute prelaunch hooks
- for prelaunch_hook in self.prelaunch_hooks:
- self.log.debug("Executing prelaunch hook: {}".format(
- str(prelaunch_hook.__class__.__name__)
- ))
- prelaunch_hook.execute()
+ if not self._prelaunch_hooks_executed:
+ self.run_prelaunch_hooks()
self.log.debug("All prelaunch hook executed. Starting new process.")
@@ -1353,6 +1442,7 @@ def get_app_environments_for_context(
task_name,
app_name,
env_group=None,
+ launch_type=None,
env=None,
modules_manager=None
):
@@ -1363,54 +1453,33 @@ def get_app_environments_for_context(
task_name (str): Name of task.
app_name (str): Name of application that is launched and can be found
by ApplicationManager.
- env (dict): Initial environment variables. `os.environ` is used when
- not passed.
- modules_manager (ModulesManager): Initialized modules manager.
+ env_group (Optional[str]): Name of environment group. If not passed
+ default group is used.
+ launch_type (Optional[str]): Type for which prelaunch hooks are
+ executed.
+ env (Optional[dict[str, str]]): Initial environment variables.
+ `os.environ` is used when not passed.
+ modules_manager (Optional[ModulesManager]): Initialized modules
+ manager.
Returns:
dict: Environments for passed context and application.
"""
- from openpype.modules import ModulesManager
- from openpype.pipeline import Anatomy
- from openpype.lib.openpype_version import is_running_staging
-
- # Project document
- project_doc = get_project(project_name)
- asset_doc = get_asset_by_name(project_name, asset_name)
-
- if modules_manager is None:
- modules_manager = ModulesManager()
-
- # Prepare app object which can be obtained only from ApplciationManager
+ # Prepare app object which can be obtained only from ApplicationManager
app_manager = ApplicationManager()
- app = app_manager.applications[app_name]
-
- # Project's anatomy
- anatomy = Anatomy(project_name)
-
- data = EnvironmentPrepData({
- "project_name": project_name,
- "asset_name": asset_name,
- "task_name": task_name,
-
- "app": app,
-
- "project_doc": project_doc,
- "asset_doc": asset_doc,
-
- "anatomy": anatomy,
-
- "env": env
- })
- data["env"].update(anatomy.root_environments())
- if is_running_staging():
- data["env"]["OPENPYPE_IS_STAGING"] = "1"
-
- prepare_app_environments(data, env_group, modules_manager)
- prepare_context_environments(data, env_group, modules_manager)
-
- return data["env"]
+ context = app_manager.create_launch_context(
+ app_name,
+ project_name=project_name,
+ asset_name=asset_name,
+ task_name=task_name,
+ env_group=env_group,
+ launch_type=launch_type,
+ env=env,
+ modules_manager=modules_manager,
+ )
+ context.run_prelaunch_hooks()
+ return context.env
def _merge_env(env, current_env):
diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py
index 3fa427204b..23e959d84c 100644
--- a/openpype/modules/deadline/abstract_submit_deadline.py
+++ b/openpype/modules/deadline/abstract_submit_deadline.py
@@ -25,6 +25,7 @@ from openpype.pipeline.publish import (
from openpype.pipeline.publish.lib import (
replace_with_published_scene_path
)
+from openpype import AYON_SERVER_ENABLED
JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError)
@@ -397,6 +398,15 @@ class DeadlineJobInfo(object):
for key, value in data.items():
setattr(self, key, value)
+ def add_render_job_env_var(self):
+ """Check if in OP or AYON mode and use appropriate env var."""
+ if AYON_SERVER_ENABLED:
+ self.EnvironmentKeyValue["AYON_RENDER_JOB"] = "1"
+ self.EnvironmentKeyValue["AYON_BUNDLE_NAME"] = (
+ os.environ["AYON_BUNDLE_NAME"])
+ else:
+ self.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+
@six.add_metaclass(AbstractMetaInstancePlugin)
class AbstractSubmitDeadline(pyblish.api.InstancePlugin,
diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
index 2de6073e29..eadfc3c83e 100644
--- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
+++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
@@ -21,6 +21,8 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
def process(self, instance):
instance.data["deadlineUrl"] = self._collect_deadline_url(instance)
+ instance.data["deadlineUrl"] = \
+ instance.data["deadlineUrl"].strip().rstrip("/")
self.log.info(
"Using {} for submission.".format(instance.data["deadlineUrl"]))
diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py
index 1a0d615dc3..58721efad3 100644
--- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py
+++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py
@@ -48,3 +48,6 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin):
context.data["defaultDeadline"] = deadline_webservice
self.log.debug("Overriding from project settings with {}".format( # noqa: E501
deadline_webservice))
+
+ context.data["defaultDeadline"] = \
+ context.data["defaultDeadline"].strip().rstrip("/")
diff --git a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
index 83dd5b49e2..009375e87e 100644
--- a/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
@@ -106,8 +106,8 @@ class AfterEffectsSubmitDeadline(
if value:
dln_job_info.EnvironmentKeyValue[key] = value
- # to recognize job from PYPE for turning Event On/Off
- dln_job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ dln_job_info.add_render_job_env_var()
return dln_job_info
diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
index 84fca11d9d..2c37268f04 100644
--- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
@@ -299,8 +299,8 @@ class HarmonySubmitDeadline(
if value:
job_info.EnvironmentKeyValue[key] = value
- # to recognize job from PYPE for turning Event On/Off
- job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ job_info.add_render_job_env_var()
return job_info
diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
index af341ca8e8..8c814bec95 100644
--- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
@@ -105,8 +105,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
if value:
job_info.EnvironmentKeyValue[key] = value
- # to recognize job from PYPE for turning Event On/Off
- job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ job_info.add_render_job_env_var(job_info)
for i, filepath in enumerate(instance.data["files"]):
dirname = os.path.dirname(filepath)
diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
index fff7a4ced5..2c1db1c880 100644
--- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py
@@ -131,8 +131,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
continue
job_info.EnvironmentKeyValue[key] = value
- # to recognize job from PYPE for turning Event On/Off
- job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ job_info.add_render_job_env_var(job_info)
job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1"
# Add list of expected files to job
diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
index 1dfb6e0e5c..75d24b28f0 100644
--- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
@@ -226,8 +226,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
continue
job_info.EnvironmentKeyValue[key] = value
- # to recognize job from PYPE for turning Event On/Off
- job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ job_info.add_render_job_env_var()
job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1"
# Adding file dependencies.
diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
index 39120f7c8a..0d23f44333 100644
--- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py
@@ -4,6 +4,7 @@ from datetime import datetime
from maya import cmds
+from openpype import AYON_SERVER_ENABLED
from openpype.pipeline import legacy_io, PublishXmlValidationError
from openpype.tests.lib import is_in_tests
from openpype.lib import is_running_from_build
@@ -114,11 +115,14 @@ class MayaSubmitRemotePublishDeadline(
environment["AVALON_TASK"] = instance.context.data["task"]
environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME")
environment["OPENPYPE_LOG_NO_COLORS"] = "1"
- environment["OPENPYPE_REMOTE_JOB"] = "1"
environment["OPENPYPE_USERNAME"] = instance.context.data["user"]
environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"]
environment["OPENPYPE_REMOTE_PUBLISH"] = "1"
+ if AYON_SERVER_ENABLED:
+ environment["AYON_REMOTE_PUBLISH"] = "1"
+ else:
+ environment["OPENPYPE_REMOTE_PUBLISH"] = "1"
for key, value in environment.items():
job_info.EnvironmentKeyValue[key] = value
diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
index 4900231783..93c6ad8139 100644
--- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
@@ -8,6 +8,8 @@ import requests
import pyblish.api
import nuke
+
+from openpype import AYON_SERVER_ENABLED
from openpype.pipeline import legacy_io
from openpype.pipeline.publish import (
OpenPypePyblishPluginMixin
@@ -337,8 +339,14 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
if _path.lower().startswith('openpype_'):
environment[_path] = os.environ[_path]
- # to recognize job from PYPE for turning Event On/Off
- environment["OPENPYPE_RENDER_JOB"] = "1"
+ # to recognize render jobs
+ if AYON_SERVER_ENABLED:
+ environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"]
+ render_job_label = "AYON_RENDER_JOB"
+ else:
+ render_job_label = "OPENPYPE_RENDER_JOB"
+
+ environment[render_job_label] = "1"
# finally search replace in values of any key
if self.env_search_replace_values:
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index 2ed21c0621..ec182fcd66 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -9,16 +9,13 @@ import clique
import pyblish.api
+from openpype import AYON_SERVER_ENABLED
from openpype.client import (
get_last_version_by_subset_name,
)
-from openpype.pipeline import (
- legacy_io,
-)
-from openpype.pipeline import publish
-from openpype.lib import EnumDef
+from openpype.pipeline import publish, legacy_io
+from openpype.lib import EnumDef, is_running_from_build
from openpype.tests.lib import is_in_tests
-from openpype.lib import is_running_from_build
from openpype.pipeline.farm.pyblish_functions import (
create_skeleton_instance,
@@ -94,7 +91,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
label = "Submit image sequence jobs to Deadline or Muster"
order = pyblish.api.IntegratorOrder + 0.2
icon = "tractor"
- deadline_plugin = "OpenPype"
+
targets = ["local"]
hosts = ["fusion", "max", "maya", "nuke", "houdini",
@@ -126,10 +123,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"OPENPYPE_SG_USER"
]
- # Add OpenPype version if we are running from build.
- if is_running_from_build():
- environ_keys.append("OPENPYPE_VERSION")
-
# custom deadline attributes
deadline_department = ""
deadline_pool = ""
@@ -189,7 +182,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
instance.data.get("asset"),
instances[0]["subset"],
instance.context,
- 'render',
+ instances[0]["family"],
override_version
)
@@ -203,13 +196,25 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"AVALON_ASSET": instance.context.data["asset"],
"AVALON_TASK": instance.context.data["task"],
"OPENPYPE_USERNAME": instance.context.data["user"],
- "OPENPYPE_PUBLISH_JOB": "1",
- "OPENPYPE_RENDER_JOB": "0",
- "OPENPYPE_REMOTE_JOB": "0",
"OPENPYPE_LOG_NO_COLORS": "1",
"IS_TEST": str(int(is_in_tests()))
}
+ if AYON_SERVER_ENABLED:
+ environment["AYON_PUBLISH_JOB"] = "1"
+ environment["AYON_RENDER_JOB"] = "0"
+ environment["AYON_REMOTE_PUBLISH"] = "0"
+ environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"]
+ deadline_plugin = "Ayon"
+ else:
+ environment["OPENPYPE_PUBLISH_JOB"] = "1"
+ environment["OPENPYPE_RENDER_JOB"] = "0"
+ environment["OPENPYPE_REMOTE_PUBLISH"] = "0"
+ deadline_plugin = "Openpype"
+ # Add OpenPype version if we are running from build.
+ if is_running_from_build():
+ self.environ_keys.append("OPENPYPE_VERSION")
+
# add environments from self.environ_keys
for env_key in self.environ_keys:
if os.getenv(env_key):
@@ -252,7 +257,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
)
payload = {
"JobInfo": {
- "Plugin": self.deadline_plugin,
+ "Plugin": deadline_plugin,
"BatchName": job["Props"]["Batch"],
"Name": job_name,
"UserName": job["Props"]["User"],
@@ -563,11 +568,22 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
else:
version = 1
+ host_name = context.data["hostName"]
+ task_info = template_data.get("task") or {}
+
+ template_name = publish.get_publish_template_name(
+ project_name,
+ host_name,
+ family,
+ task_info.get("name"),
+ task_info.get("type"),
+ )
+
template_data["subset"] = subset
template_data["family"] = family
template_data["version"] = version
- render_templates = anatomy.templates_obj["render"]
+ render_templates = anatomy.templates_obj[template_name]
if "folder" in render_templates:
publish_folder = render_templates["folder"].format_strict(
template_data
diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico
new file mode 100644
index 0000000000..aea977a125
Binary files /dev/null and b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.ico differ
diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.options b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.options
new file mode 100644
index 0000000000..1fbe1ef299
--- /dev/null
+++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.options
@@ -0,0 +1,9 @@
+[Arguments]
+Type=string
+Label=Arguments
+Category=Python Options
+CategoryOrder=0
+Index=1
+Description=The arguments to pass to the script. If no arguments are required, leave this blank.
+Required=false
+DisableIfBlank=true
diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param
new file mode 100644
index 0000000000..8ba044ff81
--- /dev/null
+++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.param
@@ -0,0 +1,35 @@
+[About]
+Type=label
+Label=About
+Category=About Plugin
+CategoryOrder=-1
+Index=0
+Default=Ayon Plugin for Deadline
+Description=Not configurable
+
+[AyonExecutable]
+Type=multilinemultifilename
+Label=Ayon Executable
+Category=Ayon Executables
+CategoryOrder=1
+Index=0
+Default=
+Description=The path to the Ayon executable. Enter alternative paths on separate lines.
+
+[AyonServerUrl]
+Type=string
+Label=Ayon Server Url
+Category=Ayon Credentials
+CategoryOrder=2
+Index=0
+Default=
+Description=Url to Ayon server
+
+[AyonApiKey]
+Type=password
+Label=Ayon API key
+Category=Ayon Credentials
+CategoryOrder=2
+Index=0
+Default=
+Description=API key for service account on Ayon Server
diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py
new file mode 100644
index 0000000000..16149d7e20
--- /dev/null
+++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+
+from System.IO import Path
+from System.Text.RegularExpressions import Regex
+
+from Deadline.Plugins import PluginType, DeadlinePlugin
+from Deadline.Scripting import (
+ StringUtils,
+ FileUtils,
+ DirectoryUtils,
+ RepositoryUtils
+)
+
+import re
+import os
+import platform
+
+
+######################################################################
+# This is the function that Deadline calls to get an instance of the
+# main DeadlinePlugin class.
+######################################################################
+def GetDeadlinePlugin():
+ return AyonDeadlinePlugin()
+
+
+def CleanupDeadlinePlugin(deadlinePlugin):
+ deadlinePlugin.Cleanup()
+
+
+class AyonDeadlinePlugin(DeadlinePlugin):
+ """
+ Standalone plugin for publishing from Ayon
+
+ Calls Ayonexecutable 'ayon_console' from first correctly found
+ file based on plugin configuration. Uses 'publish' command and passes
+ path to metadata json file, which contains all needed information
+ for publish process.
+ """
+ def __init__(self):
+ self.InitializeProcessCallback += self.InitializeProcess
+ self.RenderExecutableCallback += self.RenderExecutable
+ self.RenderArgumentCallback += self.RenderArgument
+
+ def Cleanup(self):
+ for stdoutHandler in self.StdoutHandlers:
+ del stdoutHandler.HandleCallback
+
+ del self.InitializeProcessCallback
+ del self.RenderExecutableCallback
+ del self.RenderArgumentCallback
+
+ def InitializeProcess(self):
+ self.PluginType = PluginType.Simple
+ self.StdoutHandling = True
+
+ self.SingleFramesOnly = self.GetBooleanPluginInfoEntryWithDefault(
+ "SingleFramesOnly", False)
+ self.LogInfo("Single Frames Only: %s" % self.SingleFramesOnly)
+
+ self.AddStdoutHandlerCallback(
+ ".*Progress: (\d+)%.*").HandleCallback += self.HandleProgress
+
+ def RenderExecutable(self):
+ job = self.GetJob()
+
+ # set required env vars for Ayon
+ # cannot be in InitializeProcess as it is too soon
+ config = RepositoryUtils.GetPluginConfig("Ayon")
+ ayon_server_url = (
+ job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or
+ config.GetConfigEntryWithDefault("AyonServerUrl", "")
+ )
+ ayon_api_key = (
+ job.GetJobEnvironmentKeyValue("AYON_API_KEY") or
+ config.GetConfigEntryWithDefault("AyonApiKey", "")
+ )
+ ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME")
+
+ environment = {
+ "AYON_SERVER_URL": ayon_server_url,
+ "AYON_API_KEY": ayon_api_key,
+ "AYON_BUNDLE_NAME": ayon_bundle_name,
+ }
+
+ for env, val in environment.items():
+ self.SetProcessEnvironmentVariable(env, val)
+
+ exe_list = self.GetConfigEntry("AyonExecutable")
+ # clean '\ ' for MacOS pasting
+ if platform.system().lower() == "darwin":
+ exe_list = exe_list.replace("\\ ", " ")
+ exe = FileUtils.SearchFileList(exe_list)
+
+ if exe == "":
+ self.FailRender(
+ "Ayon executable was not found " +
+ "in the semicolon separated list " +
+ "\"" + ";".join(exe_list) + "\". " +
+ "The path to the render executable can be configured " +
+ "from the Plugin Configuration in the Deadline Monitor.")
+ return exe
+
+ def RenderArgument(self):
+ arguments = str(self.GetPluginInfoEntryWithDefault("Arguments", ""))
+ arguments = RepositoryUtils.CheckPathMapping(arguments)
+
+ arguments = re.sub(r"<(?i)STARTFRAME>", str(self.GetStartFrame()),
+ arguments)
+ arguments = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()),
+ arguments)
+ arguments = re.sub(r"<(?i)QUOTE>", "\"", arguments)
+
+ arguments = self.ReplacePaddedFrame(arguments,
+ "<(?i)STARTFRAME%([0-9]+)>",
+ self.GetStartFrame())
+ arguments = self.ReplacePaddedFrame(arguments,
+ "<(?i)ENDFRAME%([0-9]+)>",
+ self.GetEndFrame())
+
+ count = 0
+ for filename in self.GetAuxiliaryFilenames():
+ localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename)
+ arguments = re.sub(r"<(?i)AUXFILE" + str(count) + r">",
+ localAuxFile.replace("\\", "/"), arguments)
+ count += 1
+
+ return arguments
+
+ def ReplacePaddedFrame(self, arguments, pattern, frame):
+ frameRegex = Regex(pattern)
+ while True:
+ frameMatch = frameRegex.Match(arguments)
+ if not frameMatch.Success:
+ break
+ paddingSize = int(frameMatch.Groups[1].Value)
+ if paddingSize > 0:
+ padding = StringUtils.ToZeroPaddedString(
+ frame, paddingSize, False)
+ else:
+ padding = str(frame)
+ arguments = arguments.replace(
+ frameMatch.Groups[0].Value, padding)
+
+ return arguments
+
+ def HandleProgress(self):
+ progress = float(self.GetRegexMatch(1))
+ self.SetProgress(progress)
diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py
index 15226bb773..5f7e1f1032 100644
--- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py
+++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py
@@ -355,6 +355,13 @@ def inject_openpype_environment(deadlinePlugin):
" AVALON_TASK, AVALON_APP_NAME"
))
+ openpype_mongo = job.GetJobEnvironmentKeyValue("OPENPYPE_MONGO")
+ if openpype_mongo:
+ # inject env var for OP extractenvironments
+ # SetEnvironmentVariable is important, not SetProcessEnv...
+ deadlinePlugin.SetEnvironmentVariable("OPENPYPE_MONGO",
+ openpype_mongo)
+
if not os.environ.get("OPENPYPE_MONGO"):
print(">>> Missing OPENPYPE_MONGO env var, process won't work")
@@ -398,6 +405,151 @@ def inject_openpype_environment(deadlinePlugin):
raise
+def inject_ayon_environment(deadlinePlugin):
+ """ Pull env vars from Ayon and push them to rendering process.
+
+ Used for correct paths, configuration from OpenPype etc.
+ """
+ job = deadlinePlugin.GetJob()
+
+ print(">>> Injecting Ayon environments ...")
+ try:
+ exe_list = get_ayon_executable()
+ exe = FileUtils.SearchFileList(exe_list)
+
+ if not exe:
+ raise RuntimeError((
+ "Ayon executable was not found in the semicolon "
+ "separated list \"{}\"."
+ "The path to the render executable can be configured"
+ " from the Plugin Configuration in the Deadline Monitor."
+ ).format(";".join(exe_list)))
+
+ print("--- Ayon executable: {}".format(exe))
+
+ ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME")
+ if not ayon_bundle_name:
+ raise RuntimeError("Missing env var in job properties "
+ "AYON_BUNDLE_NAME")
+
+ config = RepositoryUtils.GetPluginConfig("Ayon")
+ ayon_server_url = (
+ job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or
+ config.GetConfigEntryWithDefault("AyonServerUrl", "")
+ )
+ ayon_api_key = (
+ job.GetJobEnvironmentKeyValue("AYON_API_KEY") or
+ config.GetConfigEntryWithDefault("AyonApiKey", "")
+ )
+
+ if not all([ayon_server_url, ayon_api_key]):
+ raise RuntimeError((
+ "Missing required values for server url and api key. "
+ "Please fill in Ayon Deadline plugin or provide by "
+ "AYON_SERVER_URL and AYON_API_KEY"
+ ))
+
+ # tempfile.TemporaryFile cannot be used because of locking
+ temp_file_name = "{}_{}.json".format(
+ datetime.utcnow().strftime('%Y%m%d%H%M%S%f'),
+ str(uuid.uuid1())
+ )
+ export_url = os.path.join(tempfile.gettempdir(), temp_file_name)
+ print(">>> Temporary path: {}".format(export_url))
+
+ args = [
+ "--headless",
+ "extractenvironments",
+ export_url
+ ]
+
+ add_kwargs = {
+ "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"),
+ "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"),
+ "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"),
+ "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"),
+ "envgroup": "farm",
+ }
+
+ if job.GetJobEnvironmentKeyValue('IS_TEST'):
+ args.append("--automatic-tests")
+
+ if all(add_kwargs.values()):
+ for key, value in add_kwargs.items():
+ args.extend(["--{}".format(key), value])
+ else:
+ raise RuntimeError((
+ "Missing required env vars: AVALON_PROJECT, AVALON_ASSET,"
+ " AVALON_TASK, AVALON_APP_NAME"
+ ))
+
+ environment = {
+ "AYON_SERVER_URL": ayon_server_url,
+ "AYON_API_KEY": ayon_api_key,
+ "AYON_BUNDLE_NAME": ayon_bundle_name,
+ }
+ for env, val in environment.items():
+ deadlinePlugin.SetEnvironmentVariable(env, val)
+
+ args_str = subprocess.list2cmdline(args)
+ print(">>> Executing: {} {}".format(exe, args_str))
+ process_exitcode = deadlinePlugin.RunProcess(
+ exe, args_str, os.path.dirname(exe), -1
+ )
+
+ if process_exitcode != 0:
+ raise RuntimeError(
+ "Failed to run Ayon process to extract environments."
+ )
+
+ print(">>> Loading file ...")
+ with open(export_url) as fp:
+ contents = json.load(fp)
+
+ for key, value in contents.items():
+ deadlinePlugin.SetProcessEnvironmentVariable(key, value)
+
+ script_url = job.GetJobPluginInfoKeyValue("ScriptFilename")
+ if script_url:
+ script_url = script_url.format(**contents).replace("\\", "/")
+ print(">>> Setting script path {}".format(script_url))
+ job.SetJobPluginInfoKeyValue("ScriptFilename", script_url)
+
+ print(">>> Removing temporary file")
+ os.remove(export_url)
+
+ print(">> Injection end.")
+ except Exception as e:
+ if hasattr(e, "output"):
+ print(">>> Exception {}".format(e.output))
+ import traceback
+ print(traceback.format_exc())
+ print("!!! Injection failed.")
+ RepositoryUtils.FailJob(job)
+ raise
+
+
+def get_ayon_executable():
+ """Return OpenPype Executable from Event Plug-in Settings
+
+ Returns:
+ (list) of paths
+ Raises:
+ (RuntimeError) if no path configured at all
+ """
+ config = RepositoryUtils.GetPluginConfig("Ayon")
+ exe_list = config.GetConfigEntryWithDefault("AyonExecutable", "")
+
+ if not exe_list:
+ raise RuntimeError("Path to Ayon executable not configured."
+ "Please set it in Ayon Deadline Plugin.")
+
+ # clean '\ ' for MacOS pasting
+ if platform.system().lower() == "darwin":
+ exe_list = exe_list.replace("\\ ", " ")
+ return exe_list
+
+
def inject_render_job_id(deadlinePlugin):
"""Inject dependency ids to publish process as env var for validation."""
print(">>> Injecting render job id ...")
@@ -422,16 +574,29 @@ def __main__(deadlinePlugin):
openpype_publish_job = \
job.GetJobEnvironmentKeyValue('OPENPYPE_PUBLISH_JOB') or '0'
openpype_remote_job = \
- job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_JOB') or '0'
+ job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_PUBLISH') or '0'
- print("--- Job type - render {}".format(openpype_render_job))
- print("--- Job type - publish {}".format(openpype_publish_job))
- print("--- Job type - remote {}".format(openpype_remote_job))
if openpype_publish_job == '1' and openpype_render_job == '1':
raise RuntimeError("Misconfiguration. Job couldn't be both " +
"render and publish.")
if openpype_publish_job == '1':
inject_render_job_id(deadlinePlugin)
- elif openpype_render_job == '1' or openpype_remote_job == '1':
+ if openpype_render_job == '1' or openpype_remote_job == '1':
inject_openpype_environment(deadlinePlugin)
+
+ ayon_render_job = \
+ job.GetJobEnvironmentKeyValue('AYON_RENDER_JOB') or '0'
+ ayon_publish_job = \
+ job.GetJobEnvironmentKeyValue('AYON_PUBLISH_JOB') or '0'
+ ayon_remote_job = \
+ job.GetJobEnvironmentKeyValue('AYON_REMOTE_PUBLISH') or '0'
+
+ if ayon_publish_job == '1' and ayon_render_job == '1':
+ raise RuntimeError("Misconfiguration. Job couldn't be both " +
+ "render and publish.")
+
+ if ayon_publish_job == '1':
+ inject_render_job_id(deadlinePlugin)
+ if ayon_render_job == '1' or ayon_remote_job == '1':
+ inject_ayon_environment(deadlinePlugin)
diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
index 86ecffd5b8..ac4e499e41 100644
--- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
+++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
@@ -2,11 +2,12 @@ import os
import ftrack_api
from openpype.settings import get_project_settings
-from openpype.lib import PostLaunchHook
+from openpype.lib.applications import PostLaunchHook, LaunchTypes
class PostFtrackHook(PostLaunchHook):
order = None
+ launch_types = {LaunchTypes.local}
def execute(self):
project_name = self.data.get("project_name")
diff --git a/openpype/modules/slack/launch_hooks/pre_python2_vendor.py b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py
index 0f4bc22a34..891c92bb7a 100644
--- a/openpype/modules/slack/launch_hooks/pre_python2_vendor.py
+++ b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py
@@ -1,5 +1,5 @@
import os
-from openpype.lib import PreLaunchHook
+from openpype.lib.applications import PreLaunchHook
from openpype_modules.slack import SLACK_MODULE_DIR
@@ -8,6 +8,7 @@ class PrePython2Support(PreLaunchHook):
Path to vendor modules is added to the beginning of PYTHONPATH.
"""
+ launch_types = set()
def execute(self):
if not self.application.use_python_2:
diff --git a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py
index bbc220945c..77f6933756 100644
--- a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py
+++ b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py
@@ -1,12 +1,8 @@
import os
import shutil
-from openpype.client.entities import (
- get_representations,
- get_project
-)
-
-from openpype.lib import PreLaunchHook
+from openpype.client.entities import get_representations
+from openpype.lib.applications import PreLaunchHook, LaunchTypes
from openpype.lib.profiles_filtering import filter_profiles
from openpype.modules.sync_server.sync_server import (
download_last_published_workfile,
@@ -32,6 +28,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook):
"nuke", "nukeassist", "nukex", "hiero", "nukestudio",
"maya", "harmony", "celaction", "flame", "fusion",
"houdini", "tvpaint"]
+ launch_types = {LaunchTypes.local}
def execute(self):
"""Check if local workfile doesn't exist, else copy it.
diff --git a/openpype/modules/timers_manager/launch_hooks/post_start_timer.py b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py
index d6ae013403..76c3cca33e 100644
--- a/openpype/modules/timers_manager/launch_hooks/post_start_timer.py
+++ b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py
@@ -1,4 +1,4 @@
-from openpype.lib import PostLaunchHook
+from openpype.lib.applications import PostLaunchHook, LaunchTypes
class PostStartTimerHook(PostLaunchHook):
@@ -7,6 +7,7 @@ class PostStartTimerHook(PostLaunchHook):
This module requires enabled TimerManager module.
"""
order = None
+ launch_types = {LaunchTypes.local}
def execute(self):
project_name = self.data.get("project_name")
diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py
index 8a3f25a026..4cb4b97707 100644
--- a/openpype/pype_commands.py
+++ b/openpype/pype_commands.py
@@ -88,7 +88,10 @@ class PypeCommands:
"""
from openpype.lib import Logger
- from openpype.lib.applications import get_app_environments_for_context
+ from openpype.lib.applications import (
+ get_app_environments_for_context,
+ LaunchTypes,
+ )
from openpype.modules import ModulesManager
from openpype.pipeline import (
install_openpype_plugins,
@@ -122,7 +125,8 @@ class PypeCommands:
context["project_name"],
context["asset_name"],
context["task_name"],
- app_full_name
+ app_full_name,
+ launch_type=LaunchTypes.farm_publish,
)
os.environ.update(env)
@@ -237,11 +241,19 @@ class PypeCommands:
Called by Deadline plugin to propagate environment into render jobs.
"""
- from openpype.lib.applications import get_app_environments_for_context
+ from openpype.lib.applications import (
+ get_app_environments_for_context,
+ LaunchTypes,
+ )
if all((project, asset, task, app)):
env = get_app_environments_for_context(
- project, asset, task, app, env_group
+ project,
+ asset,
+ task,
+ app,
+ env_group=env_group,
+ launch_type=LaunchTypes.farm_render,
)
else:
env = os.environ.copy()
diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py
index cd12a8f757..904751e653 100644
--- a/openpype/settings/ayon_settings.py
+++ b/openpype/settings/ayon_settings.py
@@ -124,8 +124,6 @@ def _convert_applications_system_settings(
# Applications settings
ayon_apps = addon_settings["applications"]
- if "adsk_3dsmax" in ayon_apps:
- ayon_apps["3dsmax"] = ayon_apps.pop("adsk_3dsmax")
additional_apps = ayon_apps.pop("additional_apps")
applications = _convert_applications_groups(
diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json
index a53f1ff202..a5256aad8b 100644
--- a/openpype/settings/defaults/project_settings/houdini.json
+++ b/openpype/settings/defaults/project_settings/houdini.json
@@ -14,7 +14,7 @@
"create": {
"CreateArnoldAss": {
"enabled": true,
- "defaults": [],
+ "default_variants": [],
"ext": ".ass"
},
"CreateAlembicCamera": {
diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json
index 8e1022f877..342d2bfb2a 100644
--- a/openpype/settings/defaults/project_settings/maya.json
+++ b/openpype/settings/defaults/project_settings/maya.json
@@ -521,7 +521,7 @@
"enabled": true,
"make_tx": true,
"rs_tex": false,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -533,7 +533,7 @@
},
"CreateUnrealStaticMesh": {
"enabled": true,
- "defaults": [
+ "default_variants": [
"",
"_Main"
],
@@ -547,7 +547,7 @@
},
"CreateUnrealSkeletalMesh": {
"enabled": true,
- "defaults": [],
+ "default_variants": [],
"joint_hints": "jnt_org"
},
"CreateMultiverseLook": {
@@ -559,7 +559,7 @@
"write_face_sets": false,
"include_parent_hierarchy": false,
"include_user_defined_attributes": false,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -567,7 +567,7 @@
"enabled": true,
"write_color_sets": false,
"write_face_sets": false,
- "defaults": [
+ "default_variants": [
"Main",
"Proxy",
"Sculpt"
@@ -578,7 +578,7 @@
"write_color_sets": false,
"write_face_sets": false,
"include_user_defined_attributes": false,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -586,20 +586,20 @@
"enabled": true,
"write_color_sets": false,
"write_face_sets": false,
- "defaults": [
+ "default_variants": [
"Main"
]
},
"CreateReview": {
"enabled": true,
- "defaults": [
+ "default_variants": [
"Main"
],
"useMayaTimeline": true
},
"CreateAss": {
"enabled": true,
- "defaults": [
+ "default_variants": [
"Main"
],
"expandProcedurals": false,
@@ -621,7 +621,7 @@
"enabled": true,
"vrmesh": true,
"alembic": true,
- "defaults": [
+ "default_variants": [
"Main"
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json
index 26c64e6219..6b516ddf4a 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json
@@ -284,6 +284,10 @@
"type": "schema_template",
"name": "template_workfile_options"
},
+ {
+ "type": "label",
+ "label": "^ Settings and for Workfile Builder is deprecated and will be soon removed.
Please use Template Workfile Build Settings instead."
+ },
{
"type": "schema",
"name": "schema_templated_workfile_build"
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json
index 83e0cf789a..64d157d281 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json
@@ -18,7 +18,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
},
@@ -86,4 +86,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
index d28d42c10c..8dec0a8817 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
@@ -28,7 +28,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
@@ -52,7 +52,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
},
@@ -84,7 +84,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
},
@@ -147,7 +147,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
@@ -177,7 +177,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
@@ -212,7 +212,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
@@ -242,7 +242,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
@@ -262,7 +262,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
},
@@ -287,7 +287,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
},
@@ -389,7 +389,7 @@
},
{
"type": "list",
- "key": "defaults",
+ "key": "default_variants",
"label": "Default Subsets",
"object_type": "text"
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json
index c9dee8681a..51c78ce8f0 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_knob_inputs.json
@@ -213,7 +213,7 @@
},
{
"type": "number",
- "key": "y",
+ "key": "z",
"default": 1,
"decimal": 4,
"maximum": 99999999
@@ -238,29 +238,75 @@
"object_types": [
{
"type": "number",
- "key": "x",
+ "key": "r",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
- "key": "x",
+ "key": "g",
"default": 1,
"decimal": 4,
"maximum": 99999999
},
+ {
+ "type": "number",
+ "key": "b",
+ "default": 1,
+ "decimal": 4,
+ "maximum": 99999999
+ },
+ {
+ "type": "number",
+ "key": "a",
+ "default": 1,
+ "decimal": 4,
+ "maximum": 99999999
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "box",
+ "label": "Box",
+ "children": [
+ {
+ "type": "text",
+ "key": "name",
+ "label": "Name"
+ },
+ {
+ "type": "list-strict",
+ "key": "value",
+ "label": "Value",
+ "object_types": [
+ {
+ "type": "number",
+ "key": "x",
+ "default": 0,
+ "decimal": 4,
+ "maximum": 99999999
+ },
{
"type": "number",
"key": "y",
- "default": 1,
+ "default": 0,
"decimal": 4,
"maximum": 99999999
},
{
"type": "number",
- "key": "y",
- "default": 1,
+ "key": "r",
+ "default": 1920,
+ "decimal": 4,
+ "maximum": 99999999
+ },
+ {
+ "type": "number",
+ "key": "t",
+ "default": 1080,
"decimal": 4,
"maximum": 99999999
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json
index 8be48e669d..3a34858f4e 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_nuke_write_attrs.json
@@ -13,6 +13,12 @@
},
{
"use_range_limit": "Use range limit"
+ },
+ {
+ "ordered": "Defined order"
+ },
+ {
+ "channels": "Channels override"
}
]
}
diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py
index 25fff73134..470645b9ee 100644
--- a/openpype/tools/publisher/widgets/overview_widget.py
+++ b/openpype/tools/publisher/widgets/overview_widget.py
@@ -28,12 +28,14 @@ class OverviewWidget(QtWidgets.QFrame):
self._refreshing_instances = False
self._controller = controller
- create_widget = CreateWidget(controller, self)
+ subset_content_widget = QtWidgets.QWidget(self)
+
+ create_widget = CreateWidget(controller, subset_content_widget)
# --- Created Subsets/Instances ---
# Common widget for creation and overview
subset_views_widget = BorderedLabelWidget(
- "Subsets to publish", self
+ "Subsets to publish", subset_content_widget
)
subset_view_cards = InstanceCardView(controller, subset_views_widget)
@@ -45,14 +47,14 @@ class OverviewWidget(QtWidgets.QFrame):
subset_views_layout.setCurrentWidget(subset_view_cards)
# Buttons at the bottom of subset view
- create_btn = CreateInstanceBtn(self)
- delete_btn = RemoveInstanceBtn(self)
- change_view_btn = ChangeViewBtn(self)
+ create_btn = CreateInstanceBtn(subset_views_widget)
+ delete_btn = RemoveInstanceBtn(subset_views_widget)
+ change_view_btn = ChangeViewBtn(subset_views_widget)
# --- Overview ---
# Subset details widget
subset_attributes_wrap = BorderedLabelWidget(
- "Publish options", self
+ "Publish options", subset_content_widget
)
subset_attributes_widget = SubsetAttributesWidget(
controller, subset_attributes_wrap
@@ -81,7 +83,6 @@ class OverviewWidget(QtWidgets.QFrame):
subset_views_widget.set_center_widget(subset_view_widget)
# Whole subset layout with attributes and details
- subset_content_widget = QtWidgets.QWidget(self)
subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget)
subset_content_layout.setContentsMargins(0, 0, 0, 0)
subset_content_layout.addWidget(create_widget, 7)
@@ -161,44 +162,62 @@ class OverviewWidget(QtWidgets.QFrame):
self._change_anim = change_anim
# Start in create mode
- self._create_widget_policy = create_widget.sizePolicy()
- self._subset_views_widget_policy = subset_views_widget.sizePolicy()
- self._subset_attributes_wrap_policy = (
- subset_attributes_wrap.sizePolicy()
- )
- self._max_widget_width = None
self._current_state = "create"
subset_attributes_wrap.setVisible(False)
+ def make_sure_animation_is_finished(self):
+ if self._change_anim.state() == QtCore.QAbstractAnimation.Running:
+ self._change_anim.stop()
+ self._on_change_anim_finished()
+
def set_state(self, new_state, animate):
if new_state == self._current_state:
return
self._current_state = new_state
- anim_is_running = (
- self._change_anim.state() == QtCore.QAbstractAnimation.Running
- )
if not animate:
- self._change_visibility_for_state()
- if anim_is_running:
- self._change_anim.stop()
+ self.make_sure_animation_is_finished()
return
- if self._max_widget_width is None:
- self._max_widget_width = self._subset_views_widget.maximumWidth()
-
if new_state == "create":
direction = QtCore.QAbstractAnimation.Backward
else:
direction = QtCore.QAbstractAnimation.Forward
self._change_anim.setDirection(direction)
- if not anim_is_running:
- view_width = self._subset_views_widget.width()
- self._subset_views_widget.setMinimumWidth(view_width)
- self._subset_views_widget.setMaximumWidth(view_width)
+ if (
+ self._change_anim.state() != QtCore.QAbstractAnimation.Running
+ ):
+ self._start_animation()
+
+ def _start_animation(self):
+ views_geo = self._subset_views_widget.geometry()
+ layout_spacing = self._subset_content_layout.spacing()
+ if self._create_widget.isVisible():
+ create_geo = self._create_widget.geometry()
+ subset_geo = QtCore.QRect(create_geo)
+ subset_geo.moveTop(views_geo.top())
+ subset_geo.moveLeft(views_geo.right() + layout_spacing)
+ self._subset_attributes_wrap.setVisible(True)
+
+ elif self._subset_attributes_wrap.isVisible():
+ subset_geo = self._subset_attributes_wrap.geometry()
+ create_geo = QtCore.QRect(subset_geo)
+ create_geo.moveTop(views_geo.top())
+ create_geo.moveRight(views_geo.left() - (layout_spacing + 1))
+ self._create_widget.setVisible(True)
+ else:
self._change_anim.start()
+ return
+
+ while self._subset_content_layout.count():
+ self._subset_content_layout.takeAt(0)
+ self._subset_views_widget.setGeometry(views_geo)
+ self._create_widget.setGeometry(create_geo)
+ self._subset_attributes_wrap.setGeometry(subset_geo)
+
+ self._change_anim.start()
def get_subset_views_geo(self):
parent = self._subset_views_widget.parent()
@@ -281,41 +300,39 @@ class OverviewWidget(QtWidgets.QFrame):
def _on_change_anim(self, value):
self._create_widget.setVisible(True)
self._subset_attributes_wrap.setVisible(True)
- width = (
- self._subset_content_widget.width()
- - (
- self._subset_views_widget.width()
- + (self._subset_content_layout.spacing() * 2)
- )
- )
- subset_attrs_width = int((float(width) / self.anim_end_value) * value)
- if subset_attrs_width > width:
- subset_attrs_width = width
+ layout_spacing = self._subset_content_layout.spacing()
+ content_width = (
+ self._subset_content_widget.width() - (layout_spacing * 2)
+ )
+ content_height = self._subset_content_widget.height()
+ views_width = max(
+ int(content_width * 0.3),
+ self._subset_views_widget.minimumWidth()
+ )
+ width = content_width - views_width
+ # Visible widths of other widgets
+ subset_attrs_width = int((float(width) / self.anim_end_value) * value)
create_width = width - subset_attrs_width
- self._create_widget.setMinimumWidth(create_width)
- self._create_widget.setMaximumWidth(create_width)
- self._subset_attributes_wrap.setMinimumWidth(subset_attrs_width)
- self._subset_attributes_wrap.setMaximumWidth(subset_attrs_width)
+ views_geo = QtCore.QRect(
+ create_width + layout_spacing, 0,
+ views_width, content_height
+ )
+ create_geo = QtCore.QRect(0, 0, width, content_height)
+ subset_attrs_geo = QtCore.QRect(create_geo)
+ create_geo.moveRight(views_geo.left() - (layout_spacing + 1))
+ subset_attrs_geo.moveLeft(views_geo.right() + layout_spacing)
+
+ self._subset_views_widget.setGeometry(views_geo)
+ self._create_widget.setGeometry(create_geo)
+ self._subset_attributes_wrap.setGeometry(subset_attrs_geo)
def _on_change_anim_finished(self):
self._change_visibility_for_state()
- self._create_widget.setMinimumWidth(0)
- self._create_widget.setMaximumWidth(self._max_widget_width)
- self._subset_attributes_wrap.setMinimumWidth(0)
- self._subset_attributes_wrap.setMaximumWidth(self._max_widget_width)
- self._subset_views_widget.setMinimumWidth(0)
- self._subset_views_widget.setMaximumWidth(self._max_widget_width)
- self._create_widget.setSizePolicy(
- self._create_widget_policy
- )
- self._subset_attributes_wrap.setSizePolicy(
- self._subset_attributes_wrap_policy
- )
- self._subset_views_widget.setSizePolicy(
- self._subset_views_widget_policy
- )
+ self._subset_content_layout.addWidget(self._create_widget, 7)
+ self._subset_content_layout.addWidget(self._subset_views_widget, 3)
+ self._subset_content_layout.addWidget(self._subset_attributes_wrap, 7)
def _change_visibility_for_state(self):
self._create_widget.setVisible(
diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py
index 2bda0c1cfe..39e78c01bb 100644
--- a/openpype/tools/publisher/window.py
+++ b/openpype/tools/publisher/window.py
@@ -634,16 +634,7 @@ class PublisherWindow(QtWidgets.QDialog):
if old_tab == "details":
self._publish_details_widget.close_details_popup()
- if new_tab in ("create", "publish"):
- animate = True
- if old_tab not in ("create", "publish"):
- animate = False
- self._content_stacked_layout.setCurrentWidget(
- self._overview_widget
- )
- self._overview_widget.set_state(new_tab, animate)
-
- elif new_tab == "details":
+ if new_tab == "details":
self._content_stacked_layout.setCurrentWidget(
self._publish_details_widget
)
@@ -654,6 +645,21 @@ class PublisherWindow(QtWidgets.QDialog):
self._report_widget
)
+ old_on_overview = old_tab in ("create", "publish")
+ if new_tab in ("create", "publish"):
+ self._content_stacked_layout.setCurrentWidget(
+ self._overview_widget
+ )
+ # Overview state is animated only when switching between
+ # 'create' and 'publish' tab
+ self._overview_widget.set_state(new_tab, old_on_overview)
+
+ elif old_on_overview:
+ # Make sure animation finished if previous tab was 'create'
+ # or 'publish'. That is just for safety to avoid stuck animation
+ # when user clicks too fast.
+ self._overview_widget.make_sure_animation_is_finished()
+
is_create = new_tab == "create"
if is_create:
self._install_app_event_listener()
diff --git a/server_addon/applications/server/__init__.py b/server_addon/applications/server/__init__.py
index fdec05006b..e782e8a591 100644
--- a/server_addon/applications/server/__init__.py
+++ b/server_addon/applications/server/__init__.py
@@ -2,12 +2,68 @@ import os
import json
import copy
-from ayon_server.addons import BaseServerAddon
+from ayon_server.addons import BaseServerAddon, AddonLibrary
from ayon_server.lib.postgres import Postgres
from .version import __version__
from .settings import ApplicationsAddonSettings, DEFAULT_VALUES
+try:
+ import semver
+except ImportError:
+ semver = None
+
+
+def sort_versions(addon_versions, reverse=False):
+ if semver is None:
+ for addon_version in sorted(addon_versions, reverse=reverse):
+ yield addon_version
+ return
+
+ version_objs = []
+ invalid_versions = []
+ for addon_version in addon_versions:
+ try:
+ version_objs.append(
+ (addon_version, semver.VersionInfo.parse(addon_version))
+ )
+ except ValueError:
+ invalid_versions.append(addon_version)
+
+ valid_versions = [
+ addon_version
+ for addon_version, _ in sorted(version_objs, key=lambda x: x[1])
+ ]
+ sorted_versions = list(sorted(invalid_versions)) + valid_versions
+ if reverse:
+ sorted_versions = reversed(sorted_versions)
+ for addon_version in sorted_versions:
+ yield addon_version
+
+
+def merge_groups(output, new_groups):
+ groups_by_name = {
+ o_group["name"]: o_group
+ for o_group in output
+ }
+ extend_groups = []
+ for new_group in new_groups:
+ group_name = new_group["name"]
+ if group_name not in groups_by_name:
+ extend_groups.append(new_group)
+ continue
+ existing_group = groups_by_name[group_name]
+ existing_variants = existing_group["variants"]
+ existing_variants_by_name = {
+ variant["name"]: variant
+ for variant in existing_variants
+ }
+ for new_variant in new_group["variants"]:
+ if new_variant["name"] not in existing_variants_by_name:
+ existing_variants.append(new_variant)
+
+ output.extend(extend_groups)
+
def get_enum_items_from_groups(groups):
label_by_name = {}
@@ -22,12 +78,11 @@ def get_enum_items_from_groups(groups):
full_name = f"{group_name}/{variant_name}"
full_label = f"{group_label} {variant_label}"
label_by_name[full_name] = full_label
- enum_items = []
- for full_name in sorted(label_by_name):
- enum_items.append(
- {"value": full_name, "label": label_by_name[full_name]}
- )
- return enum_items
+
+ return [
+ {"value": full_name, "label": label_by_name[full_name]}
+ for full_name in sorted(label_by_name)
+ ]
class ApplicationsAddon(BaseServerAddon):
@@ -48,6 +103,19 @@ class ApplicationsAddon(BaseServerAddon):
return self.get_settings_model()(**default_values)
+ async def pre_setup(self):
+ """Make sure older version of addon use the new way of attributes."""
+
+ instance = AddonLibrary.getinstance()
+ app_defs = instance.data.get(self.name)
+ old_addon = app_defs.versions.get("0.1.0")
+ if old_addon is not None:
+ # Override 'create_applications_attribute' for older versions
+ # - avoid infinite server restart loop
+ old_addon.create_applications_attribute = (
+ self.create_applications_attribute
+ )
+
async def setup(self):
need_restart = await self.create_applications_attribute()
if need_restart:
@@ -60,21 +128,32 @@ class ApplicationsAddon(BaseServerAddon):
bool: 'True' if an attribute was created or updated.
"""
- settings_model = await self.get_studio_settings()
- studio_settings = settings_model.dict()
- applications = studio_settings["applications"]
- _applications = applications.pop("additional_apps")
- for name, value in applications.items():
- value["name"] = name
- _applications.append(value)
+ instance = AddonLibrary.getinstance()
+ app_defs = instance.data.get(self.name)
+ all_applications = []
+ all_tools = []
+ for addon_version in sort_versions(
+ app_defs.versions.keys(), reverse=True
+ ):
+ addon = app_defs.versions[addon_version]
+ for variant in ("production", "staging"):
+ settings_model = await addon.get_studio_settings(variant)
+ studio_settings = settings_model.dict()
+ application_settings = studio_settings["applications"]
+ app_groups = application_settings.pop("additional_apps")
+ for group_name, value in application_settings.items():
+ value["name"] = group_name
+ app_groups.append(value)
+ merge_groups(all_applications, app_groups)
+ merge_groups(all_tools, studio_settings["tool_groups"])
query = "SELECT name, position, scope, data from public.attributes"
apps_attrib_name = "applications"
tools_attrib_name = "tools"
- apps_enum = get_enum_items_from_groups(_applications)
- tools_enum = get_enum_items_from_groups(studio_settings["tool_groups"])
+ apps_enum = get_enum_items_from_groups(all_applications)
+ tools_enum = get_enum_items_from_groups(all_tools)
apps_attribute_data = {
"type": "list_of_strings",
"title": "Applications",
diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py
index 3dc1f76bc6..485f44ac21 100644
--- a/server_addon/applications/server/version.py
+++ b/server_addon/applications/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.0"
+__version__ = "0.1.1"
diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py
index ca5d0a4ea5..4155c75eb7 100644
--- a/server_addon/houdini/server/settings/publish_plugins.py
+++ b/server_addon/houdini/server/settings/publish_plugins.py
@@ -54,7 +54,7 @@ class CreatePluginsModel(BaseSettingsModel):
DEFAULT_HOUDINI_CREATE_SETTINGS = {
"CreateArnoldAss": {
"enabled": True,
- "defaults": [],
+ "default_variants": [],
"ext": ".ass"
},
"CreateAlembicCamera": {
diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py
index 3dc1f76bc6..485f44ac21 100644
--- a/server_addon/houdini/server/version.py
+++ b/server_addon/houdini/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.0"
+__version__ = "0.1.1"
diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py
index 291b3ec660..039b027898 100644
--- a/server_addon/maya/server/settings/creators.py
+++ b/server_addon/maya/server/settings/creators.py
@@ -224,7 +224,7 @@ DEFAULT_CREATORS_SETTINGS = {
"enabled": True,
"make_tx": True,
"rs_tex": False,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -236,7 +236,7 @@ DEFAULT_CREATORS_SETTINGS = {
},
"CreateUnrealStaticMesh": {
"enabled": True,
- "defaults": [
+ "default_variants": [
"",
"_Main"
],
@@ -250,7 +250,7 @@ DEFAULT_CREATORS_SETTINGS = {
},
"CreateUnrealSkeletalMesh": {
"enabled": True,
- "defaults": [],
+ "default_variants": [],
"joint_hints": "jnt_org"
},
"CreateMultiverseLook": {
@@ -262,7 +262,7 @@ DEFAULT_CREATORS_SETTINGS = {
"write_face_sets": False,
"include_parent_hierarchy": False,
"include_user_defined_attributes": False,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -270,7 +270,7 @@ DEFAULT_CREATORS_SETTINGS = {
"enabled": True,
"write_color_sets": False,
"write_face_sets": False,
- "defaults": [
+ "default_variants": [
"Main",
"Proxy",
"Sculpt"
@@ -281,7 +281,7 @@ DEFAULT_CREATORS_SETTINGS = {
"write_color_sets": False,
"write_face_sets": False,
"include_user_defined_attributes": False,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -289,7 +289,7 @@ DEFAULT_CREATORS_SETTINGS = {
"enabled": True,
"write_color_sets": False,
"write_face_sets": False,
- "defaults": [
+ "default_variants": [
"Main"
]
},
@@ -313,7 +313,7 @@ DEFAULT_CREATORS_SETTINGS = {
},
"CreateAss": {
"enabled": True,
- "defaults": [
+ "default_variants": [
"Main"
],
"expandProcedurals": False,
@@ -363,7 +363,7 @@ DEFAULT_CREATORS_SETTINGS = {
},
"CreateReview": {
"enabled": True,
- "defaults": [
+ "default_variants": [
"Main"
],
"useMayaTimeline": True
@@ -387,7 +387,7 @@ DEFAULT_CREATORS_SETTINGS = {
"enabled": True,
"vrmesh": True,
"alembic": True,
- "defaults": [
+ "default_variants": [
"Main"
]
},
diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py
index a242f0e757..df0c92f1e2 100644
--- a/server_addon/maya/server/version.py
+++ b/server_addon/maya/server/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
-__version__ = "0.1.1"
+__version__ = "0.1.2"