From 44128f77e5eda00ebf91f0d31a631f687424944b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Jan 2024 16:34:58 +0100 Subject: [PATCH 01/18] implemented the same logic to keep version on switch in ayon switch tool --- .../switch_dialog/dialog.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py index 2ebed7f89b..fade09305a 100644 --- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py @@ -1212,12 +1212,11 @@ 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 + subset_id = version_doc["parent"] + name = version_doc["name"] + version_docs_by_parent_id_and_name[subset_id][name] = version_doc hero_version_docs_by_parent_id = {} for hero_version_doc in hero_version_docs: @@ -1242,7 +1241,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 +1255,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 +1271,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 +1302,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: From 4444f17892948748d67453132ed81ef2a27ae930 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Jan 2024 16:35:11 +0100 Subject: [PATCH 02/18] make version doc resolving a little bit more safe --- openpype/tools/sceneinventory/switch_dialog.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 From 90fc4e27167052b0fd091b038ebc4593870c0668 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 15:21:30 +0100 Subject: [PATCH 03/18] implemented base of click wrapper --- openpype/click_wrap.py | 371 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 openpype/click_wrap.py diff --git a/openpype/click_wrap.py b/openpype/click_wrap.py new file mode 100644 index 0000000000..218825bf04 --- /dev/null +++ b/openpype/click_wrap.py @@ -0,0 +1,371 @@ +"""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 From 83c5e7d0de4a0779ce3e1ea3a2c86064f750cd94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 15:22:24 +0100 Subject: [PATCH 04/18] use 'click_wrap' in ftrack --- openpype/modules/ftrack/ftrack_module.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index b5152ff9c4..c7df45d6a4 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -3,8 +3,7 @@ import json import collections import platform -import click - +from openpype import click_wrap from openpype.modules import ( OpenPypeModule, ITrayModule, @@ -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(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, From ade8d4d47cfa45780a63b9d376bdb1fe059a0c83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Nov 2023 16:28:29 +0100 Subject: [PATCH 05/18] small fixes --- openpype/click_wrap.py | 8 ++++++-- openpype/modules/ftrack/ftrack_module.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/click_wrap.py b/openpype/click_wrap.py index 218825bf04..3db5037b2f 100644 --- a/openpype/click_wrap.py +++ b/openpype/click_wrap.py @@ -272,7 +272,9 @@ def convert_to_click(obj_to_convert): command_obj, parent_obj = item if not isinstance(command_obj, Command): raise TypeError( - "Invalid type '{}' expected 'Command'".format(type(command_obj)) + "Invalid type '{}' expected 'Command'".format( + type(command_obj) + ) ) if isinstance(command_obj, Group): @@ -298,7 +300,9 @@ def convert_to_click(obj_to_convert): elif option_name == "argument": click.argument(*args, **kwargs)(click_obj) else: - raise ValueError("Invalid option name '{}'".format(option_name)) + raise ValueError( + "Invalid option name '{}'".format(option_name) + ) if top_obj is None: top_obj = click_obj diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index c7df45d6a4..ed48b170a1 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -488,7 +488,7 @@ class FtrackModule( return cred.get("username"), cred.get("api_key") def cli(self, click_group): - click_group.add_command(main.to_click_obj()) + click_group.add_command(cli_main.to_click_obj()) def _check_ftrack_url(url): From eb7d264900b61892b843a68ae5238c6538795c60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Jan 2024 17:48:58 +0100 Subject: [PATCH 06/18] moved 'click_wrap.py' to './modules' --- openpype/modules/__init__.py | 3 +++ openpype/{ => modules}/click_wrap.py | 0 2 files changed, 3 insertions(+) rename openpype/{ => modules}/click_wrap.py (100%) 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/click_wrap.py b/openpype/modules/click_wrap.py similarity index 100% rename from openpype/click_wrap.py rename to openpype/modules/click_wrap.py From 159a2d1dbc83f82f2c7c7fec507b5d436e25bddd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 22 Jan 2024 17:49:52 +0100 Subject: [PATCH 07/18] use new click_wrap in existing openpype modules --- openpype/hosts/standalonepublisher/addon.py | 13 ++++--- openpype/hosts/traypublisher/addon.py | 15 +++++--- openpype/hosts/webpublisher/addon.py | 34 +++++++++---------- .../example_addons/example_addon/addon.py | 6 ++-- openpype/modules/ftrack/ftrack_module.py | 2 +- openpype/modules/job_queue/module.py | 15 ++++---- openpype/modules/kitsu/kitsu_module.py | 18 +++++----- .../modules/sync_server/sync_server_module.py | 16 ++++++--- 8 files changed, 65 insertions(+), 54 deletions(-) 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/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index be1d3ff920..e9bcee85bb 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,10 @@ 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 ed48b170a1..2042367a7e 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -3,8 +3,8 @@ import json import collections import platform -from openpype import click_wrap from openpype.modules import ( + click_wrap, OpenPypeModule, ITrayModule, IPluginPaths, diff --git a/openpype/modules/job_queue/module.py b/openpype/modules/job_queue/module.py index 7075fcea14..c267329a61 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,7 @@ 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, From 6f5be5ab7ff369f040413638b7afaade2829a61b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Jan 2024 10:55:07 +0100 Subject: [PATCH 08/18] fix line length --- openpype/modules/example_addons/example_addon/addon.py | 4 +++- openpype/modules/job_queue/module.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index e9bcee85bb..e9de0c1bf5 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -118,7 +118,9 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): click_group.add_command(cli_main.to_click_obj()) -@click_wrap.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/job_queue/module.py b/openpype/modules/job_queue/module.py index c267329a61..6792cd2aca 100644 --- a/openpype/modules/job_queue/module.py +++ b/openpype/modules/job_queue/module.py @@ -236,6 +236,8 @@ def cli_start_server(port, host): ) ) @click_wrap.argument("app_name") -@click_wrap.option("--server_url", help="Server url which handle workers and jobs.") +@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) From 94702cc2cda1177cd8973e64fbd62e4635a442fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Jan 2024 10:56:09 +0100 Subject: [PATCH 09/18] reset loader window on reopen --- openpype/tools/ayon_loader/ui/window.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py index a6d40d52e7..d0455c901d 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,10 @@ class LoaderWindow(QtWidgets.QWidget): self._show_timer.start() + def closeEvent(self, event): + super(LoaderWindow, self).closeEvent(event) + self._reset_on_show = True + def keyPressEvent(self, event): modifiers = event.modifiers() ctrl_pressed = QtCore.Qt.ControlModifier & modifiers @@ -378,8 +383,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() From d2ee1b91f55449f43b0783f7b6ab59532352606c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 26 Jan 2024 11:15:08 +0100 Subject: [PATCH 10/18] deselect project on close --- openpype/tools/ayon_loader/ui/window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/ayon_loader/ui/window.py b/openpype/tools/ayon_loader/ui/window.py index d0455c901d..8982d92c0f 100644 --- a/openpype/tools/ayon_loader/ui/window.py +++ b/openpype/tools/ayon_loader/ui/window.py @@ -335,6 +335,9 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super(LoaderWindow, self).closeEvent(event) + # Deselect project so current context will be selected + # on next 'showEvent' + self._controller.set_selected_project(None) self._reset_on_show = True def keyPressEvent(self, event): From 7be9463e5169f260686b214b616820df6d70c5f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Feb 2024 15:48:43 +0100 Subject: [PATCH 11/18] removed djvview group from default applications the djvview does not have model and is unused, probably forgotten --- .../applications/server/applications.json | 24 ------------------- 1 file changed, 24 deletions(-) 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", From 2c3761ca37fd9ed7b815f03d086de70e84e83b57 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Feb 2024 17:16:55 +0000 Subject: [PATCH 12/18] Make values for project_settings/ftrack/events/status_update case insensitive --- openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py | 2 ++ 1 file changed, 2 insertions(+) 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 From 29d169e2b124aa9171698e540fc2b52f704af299 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Feb 2024 03:26:23 +0000 Subject: [PATCH 13/18] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e5dd558409..54a9d69bdc 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.18.7-nightly.1 - 3.18.6 - 3.18.6-nightly.2 - 3.18.6-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 - - 3.15.8 validations: required: true - type: dropdown From ed339ed516391071948271d49a55a8e6564cceb8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 5 Feb 2024 10:26:55 +0100 Subject: [PATCH 14/18] General: added fallback for broken ffprobe return (#6189) * OP-8090 - added fallback for ffprobe issue Customer provided .exr returned width and height equal to 0 which caused error in extract_thumbnail. This tries to use oiiotool to get metadata about file, in our case it read it correctly. * OP-8090 - extract logic `get_rescaled_command_arguments` is long enough right now, new method is better testable too. * Update openpype/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/transcoding.py | 50 ++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) 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: From d377b28f9e45036e1cc4892838c85f7d5196d5d6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 5 Feb 2024 10:32:18 +0100 Subject: [PATCH 15/18] OP-8104 - fix unwanted change to field name (#6193) It should be image_format but in previous refactoring PR it fell back to original output_formats --- server_addon/fusion/server/settings.py | 8 +++++--- server_addon/fusion/server/version.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) 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" From 907b5fb606675c5758ede110cfbb2ad64bafbccb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 5 Feb 2024 10:38:57 +0100 Subject: [PATCH 16/18] modified docstrings for sphinx --- openpype/modules/click_wrap.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/modules/click_wrap.py b/openpype/modules/click_wrap.py index 3db5037b2f..ed67035ec8 100644 --- a/openpype/modules/click_wrap.py +++ b/openpype/modules/click_wrap.py @@ -12,7 +12,7 @@ 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 +Before ```python import click from openpype.modules import OpenPypeModule @@ -37,7 +37,7 @@ def mycommand(arg1, arg2): print(arg1, arg2) ``` -# Now +Now ``` from openpype import click_wrap from openpype.modules import OpenPypeModule @@ -133,7 +133,6 @@ class Command(object): Returns: click.Command: Click command object. """ - return convert_to_click(self) # --- Methods for 'convert_to_click' function --- @@ -142,7 +141,6 @@ class Command(object): Returns: tuple: Command definition arguments. """ - return self._args def get_kwargs(self): @@ -150,7 +148,6 @@ class Command(object): Returns: dict[str, Any]: Command definition kwargs. """ - return self._kwargs def get_func(self): @@ -158,7 +155,6 @@ class Command(object): Returns: Function: Function to invoke on command trigger. """ - return self._func def iter_options(self): @@ -166,7 +162,6 @@ class Command(object): Yields: tuple[str, tuple, dict]: Option type name with args and kwargs. """ - for item in self._options: yield item # ----------------------------------------------- @@ -203,7 +198,6 @@ class Group(Command): Args: command (Command): Prepared command object. """ - if command not in self._commands: self._commands.append(command) @@ -213,7 +207,6 @@ class Group(Command): Args: group (Group): Prepared group object. """ - if group not in self._commands: self._commands.append(group) @@ -223,7 +216,6 @@ class Group(Command): Returns: Union[Command, Function]: New command object, or wrapper function. """ - return self._add_new(Command, *args, **kwargs) def group(self, *args, **kwargs): @@ -232,7 +224,6 @@ class Group(Command): 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): @@ -261,7 +252,6 @@ def convert_to_click(obj_to_convert): Returns: click.Command: Click command object. """ - import click commands_queue = collections.deque() From fe5ef4aa8c39b851b548064b9219027d0741311f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 5 Feb 2024 11:23:44 +0100 Subject: [PATCH 17/18] store version id to versions set --- openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py index fade09305a..1d1bd1adbc 100644 --- a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py +++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py @@ -1214,9 +1214,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): version_ids = set() version_docs_by_parent_id_and_name = collections.defaultdict(dict) for version_doc in version_docs: - subset_id = version_doc["parent"] + version_ids.add(version_doc["_id"]) + product_id = version_doc["parent"] name = version_doc["name"] - version_docs_by_parent_id_and_name[subset_id][name] = version_doc + 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: From 7a7a4b1e9a084956d6923658848eeb896f3d88c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 5 Feb 2024 12:04:55 +0100 Subject: [PATCH 18/18] handle empty project in 'get_project_product_types' --- openpype/tools/ayon_loader/abstract.py | 3 +++ openpype/tools/ayon_loader/models/products.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) 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)