Merge branch 'develop' into bugfix/traypublish-editorial-avoid-audio-track

This commit is contained in:
Jakub Ježek 2024-02-07 10:59:07 +01:00 committed by GitHub
commit 2669a6d923
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 855 additions and 162 deletions

View file

@ -35,6 +35,10 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.18.7-nightly.2
- 3.18.7-nightly.1
- 3.18.6
- 3.18.6-nightly.2
- 3.18.6-nightly.1
- 3.18.5
- 3.18.5-nightly.3
@ -131,10 +135,6 @@ body:
- 3.15.10-nightly.1
- 3.15.9
- 3.15.9-nightly.2
- 3.15.9-nightly.1
- 3.15.8
- 3.15.8-nightly.3
- 3.15.8-nightly.2
validations:
required: true
- type: dropdown

View file

@ -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**
<details>
<summary>AYON: Use `SettingsField` from ayon server <a href="https://github.com/ynput/OpenPype/pull/6173">#6173</a></summary>
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.
___
</details>
<details>
<summary>Nuke: Expose write knobs - OP-7592 <a href="https://github.com/ynput/OpenPype/pull/6137">#6137</a></summary>
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.
___
</details>
<details>
<summary>AYON: Remove kitsu addon <a href="https://github.com/ynput/OpenPype/pull/6172">#6172</a></summary>
Removed kitsu addon from server addons because already has own repository.
___
</details>
### **🐛 Bug fixes**
<details>
<summary>Fusion: provide better logging for validate saver crash due type error <a href="https://github.com/ynput/OpenPype/pull/6082">#6082</a></summary>
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"`.
___
</details>
<details>
<summary>Workfile Template Builder: Use correct variable in create placeholder <a href="https://github.com/ynput/OpenPype/pull/6141">#6141</a></summary>
Use correct variable where failed instances are stored for validation.
___
</details>
<details>
<summary>ExtractOIIOTranscode: Missing product_names to subsets conversion <a href="https://github.com/ynput/OpenPype/pull/6159">#6159</a></summary>
The `Product Names` filtering should be fixed with this.
___
</details>
<details>
<summary>Blender: Fix missing animation data when updating blend assets <a href="https://github.com/ynput/OpenPype/pull/6165">#6165</a></summary>
Fix missing animation data when updating blend assets.
___
</details>
<details>
<summary>TrayPublisher: Pre-fill of version works in AYON <a href="https://github.com/ynput/OpenPype/pull/6180">#6180</a></summary>
Use `folderPath` instead of `asset` in AYON mode to calculate next available version.
___
</details>
### **🔀 Refactored code**
<details>
<summary>Chore: remove Muster <a href="https://github.com/ynput/OpenPype/pull/6085">#6085</a></summary>
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.
___
</details>
### **Merged pull requests**
<details>
<summary>Maya: change label in the render settings to be more readable <a href="https://github.com/ynput/OpenPype/pull/6134">#6134</a></summary>
AYON replacement for #5713.
___
</details>
## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5)

View file

@ -43,7 +43,8 @@ from .lib import (
get_node_data,
set_node_data,
update_node_data,
create_write_node
create_write_node,
link_knobs
)
from .utils import (
colorspace_exists_on_node,
@ -95,6 +96,7 @@ __all__ = (
"set_node_data",
"update_node_data",
"create_write_node",
"link_knobs",
"colorspace_exists_on_node",
"get_colorspace_list",

View file

@ -3499,3 +3499,27 @@ def create_camera_node_by_version():
return nuke.createNode("Camera4")
else:
return nuke.createNode("Camera2")
def link_knobs(knobs, node, group_node):
"""Link knobs from inside `group_node`"""
missing_knobs = []
for knob in knobs:
if knob in group_node.knobs():
continue
if knob not in node.knobs().keys():
missing_knobs.append(knob)
link = nuke.Link_Knob("")
link.makeLink(node.name(), knob)
link.setName(knob)
link.setFlag(0x1000)
group_node.addKnob(link)
if missing_knobs:
raise ValueError(
"Write node exposed knobs missing:\n\n{}\n\nPlease review"
" project settings.".format("\n".join(missing_knobs))
)

View file

@ -44,7 +44,8 @@ from .lib import (
get_view_process_node,
get_viewer_config_from_string,
deprecated,
get_filenames_without_hash
get_filenames_without_hash,
link_knobs
)
from .pipeline import (
list_instances,
@ -1344,3 +1345,11 @@ def _remove_old_knobs(node):
node.removeKnob(knob)
except ValueError:
pass
def exposed_write_knobs(settings, plugin_name, instance_node):
exposed_knobs = settings["nuke"]["create"][plugin_name]["exposed_knobs"]
if exposed_knobs:
instance_node.addKnob(nuke.Text_Knob('', 'Write Knobs'))
write_node = nuke.allNodes(group=instance_node, filter="Write")[0]
link_knobs(exposed_knobs, write_node, instance_node)

View file

@ -12,6 +12,7 @@ from openpype.lib import (
EnumDef
)
from openpype.hosts.nuke import api as napi
from openpype.hosts.nuke.api.plugin import exposed_write_knobs
class CreateWriteImage(napi.NukeWriteCreator):
@ -132,6 +133,10 @@ class CreateWriteImage(napi.NukeWriteCreator):
instance.data_to_store()
)
exposed_write_knobs(
self.project_settings, self.__class__.__name__, instance_node
)
return instance
except Exception as er:

View file

@ -9,6 +9,7 @@ from openpype.lib import (
BoolDef
)
from openpype.hosts.nuke import api as napi
from openpype.hosts.nuke.api.plugin import exposed_write_knobs
class CreateWritePrerender(napi.NukeWriteCreator):
@ -119,6 +120,10 @@ class CreateWritePrerender(napi.NukeWriteCreator):
instance.data_to_store()
)
exposed_write_knobs(
self.project_settings, self.__class__.__name__, instance_node
)
return instance
except Exception as er:

View file

@ -9,6 +9,7 @@ from openpype.lib import (
BoolDef
)
from openpype.hosts.nuke import api as napi
from openpype.hosts.nuke.api.plugin import exposed_write_knobs
class CreateWriteRender(napi.NukeWriteCreator):
@ -113,6 +114,10 @@ class CreateWriteRender(napi.NukeWriteCreator):
instance.data_to_store()
)
exposed_write_knobs(
self.project_settings, self.__class__.__name__, instance_node
)
return instance
except Exception as er:

View file

@ -0,0 +1,77 @@
import pyblish.api
from openpype.pipeline.publish import get_errored_instances_from_context
from openpype.hosts.nuke.api.lib import link_knobs
from openpype.pipeline.publish import (
OptionalPyblishPluginMixin,
PublishValidationError
)
class RepairExposedKnobs(pyblish.api.Action):
label = "Repair"
on = "failed"
icon = "wrench"
def process(self, context, plugin):
instances = get_errored_instances_from_context(context)
for instance in instances:
child_nodes = (
instance.data.get("transientData", {}).get("childNodes")
or instance
)
write_group_node = instance.data["transientData"]["node"]
# get write node from inside of group
write_node = None
for x in child_nodes:
if x.Class() == "Write":
write_node = x
plugin_name = plugin.families_mapping[instance.data["family"]]
nuke_settings = instance.context.data["project_settings"]["nuke"]
create_settings = nuke_settings["create"][plugin_name]
exposed_knobs = create_settings["exposed_knobs"]
link_knobs(exposed_knobs, write_node, write_group_node)
class ValidateExposedKnobs(
OptionalPyblishPluginMixin,
pyblish.api.InstancePlugin
):
""" Validate write node exposed knobs.
Compare exposed linked knobs to settings.
"""
order = pyblish.api.ValidatorOrder
optional = True
families = ["render", "prerender", "image"]
label = "Validate Exposed Knobs"
actions = [RepairExposedKnobs]
hosts = ["nuke"]
families_mapping = {
"render": "CreateWriteRender",
"prerender": "CreateWritePrerender",
"image": "CreateWriteImage"
}
def process(self, instance):
if not self.is_active(instance.data):
return
plugin = self.families_mapping[instance.data["family"]]
group_node = instance.data["transientData"]["node"]
nuke_settings = instance.context.data["project_settings"]["nuke"]
create_settings = nuke_settings["create"][plugin]
exposed_knobs = create_settings["exposed_knobs"]
unexposed_knobs = []
for knob in exposed_knobs:
if knob not in group_node.knobs():
unexposed_knobs.append(knob)
if unexposed_knobs:
raise PublishValidationError(
"Missing exposed knobs: {}".format(unexposed_knobs)
)

View file

@ -10,7 +10,7 @@ from openpype.hosts.nuke.api.lib import (
from openpype.pipeline.publish import (
PublishXmlValidationError,
OptionalPyblishPluginMixin,
OptionalPyblishPluginMixin
)

View file

@ -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)

View file

@ -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():

View file

@ -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

View file

@ -221,9 +221,16 @@ class SettingsCreator(TrayPublishCreator):
):
filtered_instance_data.append(instance)
asset_names = {
instance["asset"]
for instance in filtered_instance_data}
if AYON_SERVER_ENABLED:
asset_names = {
instance["folderPath"]
for instance in filtered_instance_data
}
else:
asset_names = {
instance["asset"]
for instance in filtered_instance_data
}
subset_names = {
instance["subset"]
for instance in filtered_instance_data}
@ -231,7 +238,10 @@ class SettingsCreator(TrayPublishCreator):
asset_names, subset_names
)
for instance in filtered_instance_data:
asset_name = instance["asset"]
if AYON_SERVER_ENABLED:
asset_name = instance["folderPath"]
else:
asset_name = instance["asset"]
subset_name = instance["subset"]
version = subset_docs_by_asset_id[asset_name][subset_name]
instance["creator_attributes"]["version_to_use"] = version

View file

@ -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.

View file

@ -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:

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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",

View file

@ -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,

View file

@ -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]

View file

@ -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.
"""

View file

@ -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)

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.18.6-nightly.1"
__version__ = "3.18.7-nightly.2"

View file

@ -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 <info@openpype.io>"]
license = "MIT License"

View file

@ -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",

View file

@ -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,

View file

@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "0.1.4"

View file

@ -55,7 +55,10 @@ class CreateWriteRenderModel(BaseSettingsModel):
enum_resolver=instance_attributes_enum,
title="Instance attributes"
)
exposed_knobs: list[str] = SettingsField(
title="Write Node Exposed Knobs",
default_factory=list
)
prenodes: list[PrenodeModel] = SettingsField(
default_factory=list,
title="Preceding nodes",
@ -81,7 +84,10 @@ class CreateWritePrerenderModel(BaseSettingsModel):
enum_resolver=instance_attributes_enum,
title="Instance attributes"
)
exposed_knobs: list[str] = SettingsField(
title="Write Node Exposed Knobs",
default_factory=list
)
prenodes: list[PrenodeModel] = SettingsField(
default_factory=list,
title="Preceding nodes",
@ -107,7 +113,10 @@ class CreateWriteImageModel(BaseSettingsModel):
enum_resolver=instance_attributes_enum,
title="Instance attributes"
)
exposed_knobs: list[str] = SettingsField(
title="Write Node Exposed Knobs",
default_factory=list
)
prenodes: list[PrenodeModel] = SettingsField(
default_factory=list,
title="Preceding nodes",
@ -146,6 +155,7 @@ DEFAULT_CREATE_SETTINGS = {
"reviewable",
"farm_rendering"
],
"exposed_knobs": [],
"prenodes": [
{
"name": "Reformat01",
@ -179,6 +189,7 @@ DEFAULT_CREATE_SETTINGS = {
"farm_rendering",
"use_range_limit"
],
"exposed_knobs": [],
"prenodes": []
},
"CreateWriteImage": {
@ -191,6 +202,7 @@ DEFAULT_CREATE_SETTINGS = {
"instance_attributes": [
"use_range_limit"
],
"exposed_knobs": [],
"prenodes": [
{
"name": "FrameHold01",

View file

@ -1 +1 @@
__version__ = "0.1.8"
__version__ = "0.1.9"