diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index cd81171b73..54a9d69bdc 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,8 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.18.7-nightly.1
+ - 3.18.6
- 3.18.6-nightly.2
- 3.18.6-nightly.1
- 3.18.5
@@ -133,8 +135,6 @@ body:
- 3.15.9
- 3.15.9-nightly.2
- 3.15.9-nightly.1
- - 3.15.8
- - 3.15.8-nightly.3
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14f0bc469f..009150ae7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,132 @@
# Changelog
+## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.5...3.18.6)
+
+### **🚀 Enhancements**
+
+
+
+AYON: Use `SettingsField` from ayon server #6173
+
+This is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used.
+
+
+___
+
+
+
+
+
+Nuke: Expose write knobs - OP-7592 #6137
+
+This PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust.
+
+
+___
+
+
+
+
+
+AYON: Remove kitsu addon #6172
+
+Removed kitsu addon from server addons because already has own repository.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Fusion: provide better logging for validate saver crash due type error #6082
+
+Handles reported issue for `NoneType` error thrown in conversion `int(tool["Comments"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `"Validate Saver Has Input"`.
+
+
+___
+
+
+
+
+
+Workfile Template Builder: Use correct variable in create placeholder #6141
+
+Use correct variable where failed instances are stored for validation.
+
+
+___
+
+
+
+
+
+ExtractOIIOTranscode: Missing product_names to subsets conversion #6159
+
+The `Product Names` filtering should be fixed with this.
+
+
+___
+
+
+
+
+
+Blender: Fix missing animation data when updating blend assets #6165
+
+Fix missing animation data when updating blend assets.
+
+
+___
+
+
+
+
+
+TrayPublisher: Pre-fill of version works in AYON #6180
+
+Use `folderPath` instead of `asset` in AYON mode to calculate next available version.
+
+
+___
+
+
+
+### **🔀 Refactored code**
+
+
+
+Chore: remove Muster #6085
+
+Muster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way.
+
+
+___
+
+
+
+### **Merged pull requests**
+
+
+
+Maya: change label in the render settings to be more readable #6134
+
+AYON replacement for #5713.
+
+
+___
+
+
+
+
+
+
## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5)
diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py
index ff520348f0..d4d4995e6d 100644
--- a/openpype/hosts/photoshop/api/lib.py
+++ b/openpype/hosts/photoshop/api/lib.py
@@ -3,12 +3,11 @@ import sys
import contextlib
import traceback
-from qtpy import QtWidgets
-
from openpype.lib import env_value_to_bool, Logger
from openpype.modules import ModulesManager
from openpype.pipeline import install_host
from openpype.tools.utils import host_tools
+from openpype.tools.utils import get_openpype_qt_app
from openpype.tests.lib import is_in_tests
from .launch_logic import ProcessLauncher, stub
@@ -30,7 +29,7 @@ def main(*subprocess_args):
# coloring in StdOutBroker
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
- app = QtWidgets.QApplication([])
+ app = get_openpype_qt_app()
app.setQuitOnLastWindowClosed(False)
launcher = ProcessLauncher(subprocess_args)
diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py
index 67204b581b..607c4ecdae 100644
--- a/openpype/hosts/standalonepublisher/addon.py
+++ b/openpype/hosts/standalonepublisher/addon.py
@@ -1,10 +1,13 @@
import os
-import click
-
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
-from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
+from openpype.modules import (
+ click_wrap,
+ OpenPypeModule,
+ ITrayAction,
+ IHostAddon,
+)
STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -37,10 +40,10 @@ class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon):
run_detached_process(args)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(
+@click_wrap.group(
StandAlonePublishAddon.name,
help="StandalonePublisher related commands.")
def cli_main():
diff --git a/openpype/hosts/traypublisher/addon.py b/openpype/hosts/traypublisher/addon.py
index 3b34f9e6e8..ca60760bab 100644
--- a/openpype/hosts/traypublisher/addon.py
+++ b/openpype/hosts/traypublisher/addon.py
@@ -1,10 +1,13 @@
import os
-import click
-
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
-from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
+from openpype.modules import (
+ click_wrap,
+ OpenPypeModule,
+ ITrayAction,
+ IHostAddon,
+)
TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -38,10 +41,12 @@ class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
run_detached_process(args)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(TrayPublishAddon.name, help="TrayPublisher related commands.")
+@click_wrap.group(
+ TrayPublishAddon.name,
+ help="TrayPublisher related commands.")
def cli_main():
pass
diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py
index 4438775b03..810d9aa6c3 100644
--- a/openpype/hosts/webpublisher/addon.py
+++ b/openpype/hosts/webpublisher/addon.py
@@ -1,8 +1,6 @@
import os
-import click
-
-from openpype.modules import OpenPypeModule, IHostAddon
+from openpype.modules import click_wrap, OpenPypeModule, IHostAddon
WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -38,10 +36,10 @@ class WebpublisherAddon(OpenPypeModule, IHostAddon):
)
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(
+@click_wrap.group(
WebpublisherAddon.name,
help="Webpublisher related commands.")
def cli_main():
@@ -49,10 +47,10 @@ def cli_main():
@cli_main.command()
-@click.argument("path")
-@click.option("-u", "--user", help="User email address")
-@click.option("-p", "--project", help="Project")
-@click.option("-t", "--targets", help="Targets", default=None,
+@click_wrap.argument("path")
+@click_wrap.option("-u", "--user", help="User email address")
+@click_wrap.option("-p", "--project", help="Project")
+@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publish(project, path, user=None, targets=None):
"""Start publishing (Inner command).
@@ -67,11 +65,11 @@ def publish(project, path, user=None, targets=None):
@cli_main.command()
-@click.argument("path")
-@click.option("-p", "--project", help="Project")
-@click.option("-h", "--host", help="Host")
-@click.option("-u", "--user", help="User email address")
-@click.option("-t", "--targets", help="Targets", default=None,
+@click_wrap.argument("path")
+@click_wrap.option("-p", "--project", help="Project")
+@click_wrap.option("-h", "--host", help="Host")
+@click_wrap.option("-u", "--user", help="User email address")
+@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publishfromapp(project, path, host, user=None, targets=None):
"""Start publishing through application (Inner command).
@@ -86,10 +84,10 @@ def publishfromapp(project, path, host, user=None, targets=None):
@cli_main.command()
-@click.option("-e", "--executable", help="Executable")
-@click.option("-u", "--upload_dir", help="Upload dir")
-@click.option("-h", "--host", help="Host", default=None)
-@click.option("-p", "--port", help="Port", default=None)
+@click_wrap.option("-e", "--executable", help="Executable")
+@click_wrap.option("-u", "--upload_dir", help="Upload dir")
+@click_wrap.option("-h", "--host", help="Host", default=None)
+@click_wrap.option("-p", "--port", help="Port", default=None)
def webserver(executable, upload_dir, host=None, port=None):
"""Start service for communication with Webpublish Front end.
diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py
index c8ddbde061..1cfe9ac14b 100644
--- a/openpype/lib/transcoding.py
+++ b/openpype/lib/transcoding.py
@@ -1227,12 +1227,8 @@ def get_rescaled_command_arguments(
target_par = target_par or 1.0
input_par = 1.0
- # ffmpeg command
- input_file_metadata = get_ffprobe_data(input_path, logger=log)
- stream = input_file_metadata["streams"][0]
- input_width = int(stream["width"])
- input_height = int(stream["height"])
- stream_input_par = stream.get("sample_aspect_ratio")
+ input_height, input_width, stream_input_par = _get_image_dimensions(
+ application, input_path, log)
if stream_input_par:
input_par = (
float(stream_input_par.split(":")[0])
@@ -1345,6 +1341,48 @@ def get_rescaled_command_arguments(
return command_args
+def _get_image_dimensions(application, input_path, log):
+ """Uses 'ffprobe' first and then 'oiiotool' if available to get dim.
+
+ Args:
+ application (str): "oiiotool"|"ffmpeg"
+ input_path (str): path to image file
+ log (Optional[logging.Logger]): Logger used for logging.
+ Returns:
+ (tuple) (int, int, dict) - (height, width, sample_aspect_ratio)
+ Raises:
+ RuntimeError if image dimensions couldn't be parsed out.
+ """
+ # ffmpeg command
+ input_file_metadata = get_ffprobe_data(input_path, logger=log)
+ input_width = input_height = 0
+ stream = next(
+ (
+ s for s in input_file_metadata["streams"]
+ if s.get("codec_type") == "video"
+ ),
+ {}
+ )
+ if stream:
+ input_width = int(stream["width"])
+ input_height = int(stream["height"])
+
+ # fallback for weird files with width=0, height=0
+ if (input_width == 0 or input_height == 0) and application == "oiiotool":
+ # Load info about file from oiio tool
+ input_info = get_oiio_info_for_input(input_path, logger=log)
+ if input_info:
+ input_width = int(input_info["width"])
+ input_height = int(input_info["height"])
+
+ if input_width == 0 or input_height == 0:
+ raise RuntimeError("Couldn't read {} either "
+ "with ffprobe or oiiotool".format(input_path))
+
+ stream_input_par = stream.get("sample_aspect_ratio")
+ return input_height, input_width, stream_input_par
+
+
def convert_color_values(application, color_value):
"""Get color mapping for ffmpeg and oiiotool.
Args:
diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py
index 3097805353..87f3233afc 100644
--- a/openpype/modules/__init__.py
+++ b/openpype/modules/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+from . import click_wrap
from .interfaces import (
ILaunchHookPaths,
IPluginPaths,
@@ -28,6 +29,8 @@ from .base import (
__all__ = (
+ "click_wrap",
+
"ILaunchHookPaths",
"IPluginPaths",
"ITrayModule",
diff --git a/openpype/modules/click_wrap.py b/openpype/modules/click_wrap.py
new file mode 100644
index 0000000000..ed67035ec8
--- /dev/null
+++ b/openpype/modules/click_wrap.py
@@ -0,0 +1,365 @@
+"""Simplified wrapper for 'click' python module.
+
+Module 'click' is used as main cli handler in AYON/OpenPype. Addons can
+register their own subcommands with options. This wrapper allows to define
+commands and options as with 'click', but without any dependency.
+
+Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype
+is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+).
+And updating 'click' would break other DCCs.
+
+How to use it? If you already have cli commands defined in addon, just replace
+'click' with 'click_wrap' and it should work and modify your addon's cli
+method to convert 'click_wrap' object to 'click' object.
+
+Before
+```python
+import click
+from openpype.modules import OpenPypeModule
+
+
+class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ click_group.add_command(cli_main)
+
+
+@click.group(ExampleAddon.name, help="Example addon")
+def cli_main():
+ pass
+
+
+@cli_main.command(help="Example command")
+@click.option("--arg1", help="Example argument 1", default="default1")
+@click.option("--arg2", help="Example argument 2", is_flag=True)
+def mycommand(arg1, arg2):
+ print(arg1, arg2)
+```
+
+Now
+```
+from openpype import click_wrap
+from openpype.modules import OpenPypeModule
+
+
+class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ click_group.add_command(cli_main.to_click_obj())
+
+
+@click_wrap.group(ExampleAddon.name, help="Example addon")
+def cli_main():
+ pass
+
+
+@cli_main.command(help="Example command")
+@click_wrap.option("--arg1", help="Example argument 1", default="default1")
+@click_wrap.option("--arg2", help="Example argument 2", is_flag=True)
+def mycommand(arg1, arg2):
+ print(arg1, arg2)
+```
+
+
+Added small enhancements:
+- most of the methods can be used as chained calls
+- functions/methods 'command' and 'group' can be used in a way that
+ first argument is callback function and the rest are arguments
+ for click
+
+Example:
+ ```python
+ from openpype import click_wrap
+ from openpype.modules import OpenPypeModule
+
+
+ class ExampleAddon(OpenPypeModule):
+ name = "example"
+
+ def cli(self, click_group):
+ # Define main command (name 'example')
+ main = click_wrap.group(
+ self._cli_main, name=self.name, help="Example addon"
+ )
+ # Add subcommand (name 'mycommand')
+ (
+ main.command(
+ self._cli_command, name="mycommand", help="Example command"
+ )
+ .option(
+ "--arg1", help="Example argument 1", default="default1"
+ )
+ .option(
+ "--arg2", help="Example argument 2", is_flag=True,
+ )
+ )
+ # Convert main command to click object and add it to parent group
+ click_group.add_command(main.to_click_obj())
+
+ def _cli_main(self):
+ pass
+
+ def _cli_command(self, arg1, arg2):
+ print(arg1, arg2)
+ ```
+
+ ```shell
+ openpype_console addon example mycommand --arg1 value1 --arg2
+ ```
+"""
+
+import collections
+
+FUNC_ATTR_NAME = "__ayon_cli_options__"
+
+
+class Command(object):
+ def __init__(self, func, *args, **kwargs):
+ # Command function
+ self._func = func
+ # Command definition arguments
+ self._args = args
+ # Command definition kwargs
+ self._kwargs = kwargs
+ # Both 'options' and 'arguments' are stored to the same variable
+ # - keep order of options and arguments
+ self._options = getattr(func, FUNC_ATTR_NAME, [])
+
+ def to_click_obj(self):
+ """Converts this object to click object.
+
+ Returns:
+ click.Command: Click command object.
+ """
+ return convert_to_click(self)
+
+ # --- Methods for 'convert_to_click' function ---
+ def get_args(self):
+ """
+ Returns:
+ tuple: Command definition arguments.
+ """
+ return self._args
+
+ def get_kwargs(self):
+ """
+ Returns:
+ dict[str, Any]: Command definition kwargs.
+ """
+ return self._kwargs
+
+ def get_func(self):
+ """
+ Returns:
+ Function: Function to invoke on command trigger.
+ """
+ return self._func
+
+ def iter_options(self):
+ """
+ Yields:
+ tuple[str, tuple, dict]: Option type name with args and kwargs.
+ """
+ for item in self._options:
+ yield item
+ # -----------------------------------------------
+
+ def add_option(self, *args, **kwargs):
+ return self.add_option_by_type("option", *args, **kwargs)
+
+ def add_argument(self, *args, **kwargs):
+ return self.add_option_by_type("argument", *args, **kwargs)
+
+ option = add_option
+ argument = add_argument
+
+ def add_option_by_type(self, option_name, *args, **kwargs):
+ self._options.append((option_name, args, kwargs))
+ return self
+
+
+class Group(Command):
+ def __init__(self, func, *args, **kwargs):
+ super(Group, self).__init__(func, *args, **kwargs)
+ # Store sub-groupd and sub-commands to the same variable
+ self._commands = []
+
+ # --- Methods for 'convert_to_click' function ---
+ def iter_commands(self):
+ for command in self._commands:
+ yield command
+ # -----------------------------------------------
+
+ def add_command(self, command):
+ """Add prepared command object as child.
+
+ Args:
+ command (Command): Prepared command object.
+ """
+ if command not in self._commands:
+ self._commands.append(command)
+
+ def add_group(self, group):
+ """Add prepared group object as child.
+
+ Args:
+ group (Group): Prepared group object.
+ """
+ if group not in self._commands:
+ self._commands.append(group)
+
+ def command(self, *args, **kwargs):
+ """Add child command.
+
+ Returns:
+ Union[Command, Function]: New command object, or wrapper function.
+ """
+ return self._add_new(Command, *args, **kwargs)
+
+ def group(self, *args, **kwargs):
+ """Add child group.
+
+ Returns:
+ Union[Group, Function]: New group object, or wrapper function.
+ """
+ return self._add_new(Group, *args, **kwargs)
+
+ def _add_new(self, target_cls, *args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ out = target_cls(_func, *args, **kwargs)
+ self._commands.append(out)
+ return out
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def convert_to_click(obj_to_convert):
+ """Convert wrapped object to click object.
+
+ Args:
+ obj_to_convert (Command): Object to convert to click object.
+
+ Returns:
+ click.Command: Click command object.
+ """
+ import click
+
+ commands_queue = collections.deque()
+ commands_queue.append((obj_to_convert, None))
+ top_obj = None
+ while commands_queue:
+ item = commands_queue.popleft()
+ command_obj, parent_obj = item
+ if not isinstance(command_obj, Command):
+ raise TypeError(
+ "Invalid type '{}' expected 'Command'".format(
+ type(command_obj)
+ )
+ )
+
+ if isinstance(command_obj, Group):
+ click_obj = (
+ click.group(
+ *command_obj.get_args(),
+ **command_obj.get_kwargs()
+ )(command_obj.get_func())
+ )
+
+ else:
+ click_obj = (
+ click.command(
+ *command_obj.get_args(),
+ **command_obj.get_kwargs()
+ )(command_obj.get_func())
+ )
+
+ for item in command_obj.iter_options():
+ option_name, args, kwargs = item
+ if option_name == "option":
+ click.option(*args, **kwargs)(click_obj)
+ elif option_name == "argument":
+ click.argument(*args, **kwargs)(click_obj)
+ else:
+ raise ValueError(
+ "Invalid option name '{}'".format(option_name)
+ )
+
+ if top_obj is None:
+ top_obj = click_obj
+
+ if parent_obj is not None:
+ parent_obj.add_command(click_obj)
+
+ if isinstance(command_obj, Group):
+ for command in command_obj.iter_commands():
+ commands_queue.append((command, click_obj))
+
+ return top_obj
+
+
+def group(*args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ return Group(_func, *args, **kwargs)
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def command(*args, **kwargs):
+ func = None
+ if args and callable(args[0]):
+ args = list(args)
+ func = args.pop(0)
+ args = tuple(args)
+
+ def decorator(_func):
+ return Command(_func, *args, **kwargs)
+
+ if func is not None:
+ return decorator(func)
+ return decorator
+
+
+def argument(*args, **kwargs):
+ def decorator(func):
+ return _add_option_to_func(
+ func, "argument", *args, **kwargs
+ )
+ return decorator
+
+
+def option(*args, **kwargs):
+ def decorator(func):
+ return _add_option_to_func(
+ func, "option", *args, **kwargs
+ )
+ return decorator
+
+
+def _add_option_to_func(func, option_name, *args, **kwargs):
+ if isinstance(func, Command):
+ func.add_option_by_type(option_name, *args, **kwargs)
+ return func
+
+ if not hasattr(func, FUNC_ATTR_NAME):
+ setattr(func, FUNC_ATTR_NAME, [])
+ cli_options = getattr(func, FUNC_ATTR_NAME)
+ cli_options.append((option_name, args, kwargs))
+ return func
diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py
index be1d3ff920..e9de0c1bf5 100644
--- a/openpype/modules/example_addons/example_addon/addon.py
+++ b/openpype/modules/example_addons/example_addon/addon.py
@@ -8,9 +8,9 @@ in global space here until are required or used.
"""
import os
-import click
from openpype.modules import (
+ click_wrap,
JsonFilesSettingsDef,
OpenPypeAddOn,
ModulesManager,
@@ -115,10 +115,12 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(ExampleAddon.name, help="Example addon dynamic cli commands.")
+@click_wrap.group(
+ ExampleAddon.name,
+ help="Example addon dynamic cli commands.")
def cli_main():
pass
diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py
index b5152ff9c4..2042367a7e 100644
--- a/openpype/modules/ftrack/ftrack_module.py
+++ b/openpype/modules/ftrack/ftrack_module.py
@@ -3,9 +3,8 @@ import json
import collections
import platform
-import click
-
from openpype.modules import (
+ click_wrap,
OpenPypeModule,
ITrayModule,
IPluginPaths,
@@ -489,7 +488,7 @@ class FtrackModule(
return cred.get("username"), cred.get("api_key")
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
def _check_ftrack_url(url):
@@ -540,24 +539,24 @@ def resolve_ftrack_url(url, logger=None):
return ftrack_url
-@click.group(FtrackModule.name, help="Ftrack module related commands.")
+@click_wrap.group(FtrackModule.name, help="Ftrack module related commands.")
def cli_main():
pass
@cli_main.command()
-@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
-@click.option("--ftrack-url", envvar="FTRACK_SERVER",
+@click_wrap.option("-d", "--debug", is_flag=True, help="Print debug messages")
+@click_wrap.option("--ftrack-url", envvar="FTRACK_SERVER",
help="Ftrack server url")
-@click.option("--ftrack-user", envvar="FTRACK_API_USER",
+@click_wrap.option("--ftrack-user", envvar="FTRACK_API_USER",
help="Ftrack api user")
-@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
+@click_wrap.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
help="Ftrack api key")
-@click.option("--legacy", is_flag=True,
+@click_wrap.option("--legacy", is_flag=True,
help="run event server without mongo storing")
-@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
+@click_wrap.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
help="Clockify API key.")
-@click.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
+@click_wrap.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
help="Clockify workspace")
def eventserver(
debug,
diff --git a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
index ac4e499e41..5c780a51c4 100644
--- a/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
+++ b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
@@ -131,6 +131,8 @@ class PostFtrackHook(PostLaunchHook):
for key, value in status_mapping.items():
if key in already_tested:
continue
+
+ value = value.lower()
if actual_status in value or "__any__" in value:
if key != "__ignore__":
next_status_name = key
diff --git a/openpype/modules/job_queue/module.py b/openpype/modules/job_queue/module.py
index 7075fcea14..6792cd2aca 100644
--- a/openpype/modules/job_queue/module.py
+++ b/openpype/modules/job_queue/module.py
@@ -41,8 +41,7 @@ import json
import copy
import platform
-import click
-from openpype.modules import OpenPypeModule
+from openpype.modules import OpenPypeModule, click_wrap
from openpype.settings import get_system_settings
@@ -153,7 +152,7 @@ class JobQueueModule(OpenPypeModule):
return requests.get(api_path).json()
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
@classmethod
def get_server_url_from_settings(cls):
@@ -213,7 +212,7 @@ class JobQueueModule(OpenPypeModule):
return main(str(executable), server_url)
-@click.group(
+@click_wrap.group(
JobQueueModule.name,
help="Application job server. Can be used as render farm."
)
@@ -225,8 +224,8 @@ def cli_main():
"start_server",
help="Start server handling workers and their jobs."
)
-@click.option("--port", help="Server port")
-@click.option("--host", help="Server host (ip address)")
+@click_wrap.option("--port", help="Server port")
+@click_wrap.option("--host", help="Server host (ip address)")
def cli_start_server(port, host):
JobQueueModule.start_server(port, host)
@@ -236,7 +235,9 @@ def cli_start_server(port, host):
"Start a worker for a specific application. (e.g. \"tvpaint/11.5\")"
)
)
-@click.argument("app_name")
-@click.option("--server_url", help="Server url which handle workers and jobs.")
+@click_wrap.argument("app_name")
+@click_wrap.option(
+ "--server_url",
+ help="Server url which handle workers and jobs.")
def cli_start_worker(app_name, server_url):
JobQueueModule.start_worker(app_name, server_url)
diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py
index 8d2d5ccd60..0ab627ba75 100644
--- a/openpype/modules/kitsu/kitsu_module.py
+++ b/openpype/modules/kitsu/kitsu_module.py
@@ -1,9 +1,9 @@
"""Kitsu module."""
-import click
import os
from openpype.modules import (
+ click_wrap,
OpenPypeModule,
IPluginPaths,
ITrayAction,
@@ -98,17 +98,17 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
-@click.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
+@click_wrap.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
def cli_main():
pass
@cli_main.command()
-@click.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
-@click.option(
+@click_wrap.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
+@click_wrap.option(
"--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
def push_to_zou(login, password):
@@ -124,11 +124,11 @@ def push_to_zou(login, password):
@cli_main.command()
-@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
-@click.option(
+@click_wrap.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
+@click_wrap.option(
"-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
-@click.option(
+@click_wrap.option(
"-prj",
"--project",
"projects",
@@ -136,7 +136,7 @@ def push_to_zou(login, password):
default=[],
help="Sync specific kitsu projects",
)
-@click.option(
+@click_wrap.option(
"-lo",
"--listen-only",
"listen_only",
diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py
index 8a92697920..3d6f76ad55 100644
--- a/openpype/modules/sync_server/sync_server_module.py
+++ b/openpype/modules/sync_server/sync_server_module.py
@@ -7,7 +7,6 @@ import copy
import signal
from collections import deque, defaultdict
-import click
from bson.objectid import ObjectId
from openpype.client import (
@@ -15,7 +14,12 @@ from openpype.client import (
get_representations,
get_representation_by_id,
)
-from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths
+from openpype.modules import (
+ OpenPypeModule,
+ ITrayModule,
+ IPluginPaths,
+ click_wrap,
+)
from openpype.settings import (
get_project_settings,
get_system_settings,
@@ -2405,7 +2409,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
return presets[project_name]['sites'][site_name]['root']
def cli(self, click_group):
- click_group.add_command(cli_main)
+ click_group.add_command(cli_main.to_click_obj())
# Webserver module implementation
def webserver_initialization(self, server_manager):
@@ -2417,13 +2421,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
)
-@click.group(SyncServerModule.name, help="SyncServer module related commands.")
+@click_wrap.group(
+ SyncServerModule.name,
+ help="SyncServer module related commands.")
def cli_main():
pass
@cli_main.command()
-@click.option(
+@click_wrap.option(
"-a",
"--active_site",
required=True,
diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py
index 4948f2431c..2c851c054d 100644
--- a/openpype/settings/ayon_settings.py
+++ b/openpype/settings/ayon_settings.py
@@ -1458,7 +1458,7 @@ class _AyonSettingsCache:
variant = "production"
if is_dev_mode_enabled():
- variant = cls._get_dev_mode_settings_variant()
+ variant = cls._get_bundle_name()
elif is_staging_enabled():
variant = "staging"
@@ -1474,28 +1474,6 @@ class _AyonSettingsCache:
def _get_bundle_name(cls):
return os.environ["AYON_BUNDLE_NAME"]
- @classmethod
- def _get_dev_mode_settings_variant(cls):
- """Develop mode settings variant.
-
- Returns:
- str: Name of settings variant.
- """
-
- con = get_ayon_server_api_connection()
- bundles = con.get_bundles()
- user = con.get_user()
- username = user["name"]
- for bundle in bundles["bundles"]:
- if (
- bundle.get("isDev")
- and bundle.get("activeUser") == username
- ):
- return bundle["name"]
- # Return fake variant - distribution logic will tell user that he
- # does not have set any dev bundle
- return "dev"
-
@classmethod
def get_value_by_project(cls, project_name):
cache_item = _AyonSettingsCache.cache_by_project_name[project_name]
diff --git a/openpype/tools/ayon_loader/abstract.py b/openpype/tools/ayon_loader/abstract.py
index bf3e81d485..1d93716e07 100644
--- a/openpype/tools/ayon_loader/abstract.py
+++ b/openpype/tools/ayon_loader/abstract.py
@@ -531,6 +531,9 @@ class FrontendLoaderController(_BaseLoaderController):
Product types have defined if are checked for filtering or not.
+ Args:
+ project_name (Union[str, None]): Project name.
+
Returns:
list[ProductTypeItem]: List of product type items for a project.
"""
diff --git a/openpype/tools/ayon_loader/models/products.py b/openpype/tools/ayon_loader/models/products.py
index 135f28df97..40b6474d12 100644
--- a/openpype/tools/ayon_loader/models/products.py
+++ b/openpype/tools/ayon_loader/models/products.py
@@ -179,12 +179,15 @@ class ProductsModel:
"""Product type items for project.
Args:
- project_name (str): Project name.
+ project_name (Union[str, None]): Project name.
Returns:
list[ProductTypeItem]: Product type items.
"""
+ if not project_name:
+ return []
+
cache = self._product_type_items_cache[project_name]
if not cache.is_valid:
product_types = ayon_api.get_project_product_types(project_name)
diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py
index a6d40d52e7..8982d92c0f 100644
--- a/openpype/tools/ayon_loader/ui/window.py
+++ b/openpype/tools/ayon_loader/ui/window.py
@@ -322,6 +322,7 @@ class LoaderWindow(QtWidgets.QWidget):
)
def refresh(self):
+ self._reset_on_show = False
self._controller.reset()
def showEvent(self, event):
@@ -332,6 +333,13 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.start()
+ def closeEvent(self, event):
+ super(LoaderWindow, self).closeEvent(event)
+ # Deselect project so current context will be selected
+ # on next 'showEvent'
+ self._controller.set_selected_project(None)
+ self._reset_on_show = True
+
def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
@@ -378,8 +386,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.stop()
if self._reset_on_show:
- self._reset_on_show = False
- self._controller.reset()
+ self.refresh()
def _show_group_dialog(self):
project_name = self._projects_combobox.get_selected_project_name()
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
index 2ebed7f89b..1d1bd1adbc 100644
--- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
@@ -1212,12 +1212,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
))
version_ids = set()
- version_docs_by_parent_id = {}
+ version_docs_by_parent_id_and_name = collections.defaultdict(dict)
for version_doc in version_docs:
- parent_id = version_doc["parent"]
- if parent_id not in version_docs_by_parent_id:
- version_ids.add(version_doc["_id"])
- version_docs_by_parent_id[parent_id] = version_doc
+ version_ids.add(version_doc["_id"])
+ product_id = version_doc["parent"]
+ name = version_doc["name"]
+ version_docs_by_parent_id_and_name[product_id][name] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
@@ -1242,7 +1242,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
)
@@ -1256,10 +1256,10 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container,
loader,
selected_folder_id,
- product_name,
+ selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
- version_docs_by_parent_id,
+ version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
):
@@ -1272,15 +1272,18 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container_product_id = container_version["parent"]
container_product = self._product_docs_by_id[container_product_id]
+ container_product_name = container_product["name"]
+
+ container_folder_id = container_product["parent"]
if selected_folder_id:
folder_id = selected_folder_id
else:
- folder_id = container_product["parent"]
+ folder_id = container_folder_id
products_by_name = product_docs_by_parent_and_name[folder_id]
- if product_name:
- product_doc = products_by_name[product_name]
+ if selected_product_name:
+ product_doc = products_by_name[selected_product_name]
else:
product_doc = products_by_name[container_product["name"]]
@@ -1300,7 +1303,26 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
- version_doc = version_docs_by_parent_id[product_id]
+ version_docs_by_name = (
+ version_docs_by_parent_id_and_name[product_id]
+ )
+ # If asset or subset are selected for switching, we use latest
+ # version else we try to keep the current container version.
+ version_name = None
+ if (
+ selected_folder_id in (None, container_folder_id)
+ and selected_product_name in (None, container_product_name)
+ ):
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
+
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:
diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py
index 150e369678..695f47b4d4 100644
--- a/openpype/tools/sceneinventory/switch_dialog.py
+++ b/openpype/tools/sceneinventory/switch_dialog.py
@@ -1299,15 +1299,21 @@ class SwitchAssetDialog(QtWidgets.QDialog):
# If asset or subset are selected for switching, we use latest
# version else we try to keep the current container version.
+ version_name = None
if (
- selected_asset not in (None, container_asset_name)
- or selected_subset not in (None, container_subset_name)
+ selected_asset in (None, container_asset_name)
+ and selected_subset in (None, container_subset_name)
):
- version_name = max(version_docs_by_name)
- else:
- version_name = container_version["name"]
+ version_name = container_version.get("name")
+
+ version_doc = None
+ if version_name is not None:
+ version_doc = version_docs_by_name.get(version_name)
+
+ if version_doc is None:
+ version_name = max(version_docs_by_name)
+ version_doc = version_docs_by_name[version_name]
- version_doc = version_docs_by_name[version_name]
version_id = version_doc["_id"]
repres_docs_by_name = repre_docs_by_parent_id_by_name[
version_id
diff --git a/openpype/version.py b/openpype/version.py
index 6cbe5ba6cd..db6da9f656 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.18.6-nightly.2"
+__version__ = "3.18.7-nightly.1"
diff --git a/pyproject.toml b/pyproject.toml
index 24172aa77f..453833aae2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.18.5" # OpenPype
+version = "3.18.6" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json
index b0b12b2003..82dfd3b8d3 100644
--- a/server_addon/applications/server/applications.json
+++ b/server_addon/applications/server/applications.json
@@ -1175,30 +1175,6 @@
}
]
},
- "djvview": {
- "enabled": true,
- "label": "DJV View",
- "icon": "{}/app_icons/djvView.png",
- "host_name": "",
- "environment": "{}",
- "variants": [
- {
- "name": "1-1",
- "label": "1.1",
- "executables": {
- "windows": [],
- "darwin": [],
- "linux": []
- },
- "arguments": {
- "windows": [],
- "darwin": [],
- "linux": []
- },
- "environment": "{}"
- }
- ]
- },
"wrap": {
"enabled": true,
"label": "Wrap",
diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py
index b157ce9e40..a913db16da 100644
--- a/server_addon/fusion/server/settings.py
+++ b/server_addon/fusion/server/settings.py
@@ -57,9 +57,9 @@ class CreateSaverPluginModel(BaseSettingsModel):
enum_resolver=_create_saver_instance_attributes_enum,
title="Instance attributes"
)
- output_formats: list[str] = SettingsField(
- default_factory=list,
- title="Output formats"
+ image_format: str = SettingsField(
+ enum_resolver=_image_format_enum,
+ title="Output Image Format"
)
@@ -90,6 +90,8 @@ class CreateImageSaverModel(CreateSaverPluginModel):
0,
title="Default rendered frame"
)
+
+
class CreatPluginsModel(BaseSettingsModel):
CreateSaver: CreateSaverModel = SettingsField(
default_factory=CreateSaverModel,
diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py
index ae7362549b..bbab0242f6 100644
--- a/server_addon/fusion/server/version.py
+++ b/server_addon/fusion/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.3"
+__version__ = "0.1.4"