From 2af09665865b33b6104ceb23fa4c415d3fe9a4ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:02:21 +0200 Subject: [PATCH 01/38] Fix support for `ayon+settings://core/tools/loader/product_type_filter_profiles` in Loader UI --- client/ayon_core/tools/loader/abstract.py | 21 +++++++++++ client/ayon_core/tools/loader/control.py | 36 +++++++++++++++++-- .../tools/loader/ui/product_types_widget.py | 20 +++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 6a68af1eb5..e7e8488d05 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import List from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -346,6 +347,16 @@ class ActionItem: return cls(**data) +class ProductTypesFilter: + """Product types filter. + + Defines the filtering for product types. + """ + def __init__(self, product_types: List[str], is_include: bool): + self.product_types: List[str] = product_types + self.is_include: bool = is_include + + class _BaseLoaderController(ABC): """Base loader controller abstraction. @@ -1006,3 +1017,13 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + + @abstractmethod + def get_current_context_product_types_filter(self): + """Return product type filter for current context. + + Returns: + ProductTypesFilter: Product type filter for current context + """ + + pass diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index f4b00e985f..085b1a0b31 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -3,7 +3,10 @@ import uuid import ayon_api -from ayon_core.lib import NestedCacheItem, CacheItem +from ayon_core.settings import get_current_project_settings +from ayon_core.pipeline import get_current_host_name +from ayon_core.pipeline.context_tools import get_current_task_entity +from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context from ayon_core.host import ILoadHost @@ -13,7 +16,11 @@ from ayon_core.tools.common_models import ( ThumbnailsModel, ) -from .abstract import BackendLoaderController, FrontendLoaderController +from .abstract import ( + BackendLoaderController, + FrontendLoaderController, + ProductTypesFilter +) from .models import ( SelectionModel, ProductsModel, @@ -425,3 +432,28 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") + + def get_current_context_product_types_filter(self): + context = get_current_context() + # There may be cases where there is no current context, like + # Tray Loader so we only do this when we have a context + if all(context.values()): + settings = get_current_project_settings() + profiles = settings["core"]["tools"]["loader"]["product_type_filter_profiles"] # noqa + if profiles: + task_entity = get_current_task_entity(fields={"taskType"}) + profile = filter_profiles(profiles, key_values={ + "hosts": get_current_host_name(), + "task_types": (task_entity or {}).get("taskType") + }) + if profile: + return ProductTypesFilter( + is_include=profile["is_include"], + product_types=profile["filter_product_types"] + ) + + # Default to all as allowed + return ProductTypesFilter( + is_include=False, + product_types=[] + ) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 180994fd7f..4e024c4417 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -151,6 +151,7 @@ class ProductTypesView(QtWidgets.QListView): ) self._controller = controller + self._refresh_product_types_filter = False self._product_types_model = product_types_model self._product_types_proxy_model = product_types_proxy_model @@ -162,7 +163,26 @@ class ProductTypesView(QtWidgets.QListView): project_name = event["project_name"] self._product_types_model.refresh(project_name) + def showEvent(self, event): + self._refresh_product_types_filter = True + super().showEvent(event) + def _on_refresh_finished(self): + + # Apply product types filter + if self._refresh_product_types_filter: + product_types_filter = ( + self._controller.get_current_context_product_types_filter() + ) + if product_types_filter.is_include: + self._on_disable_all() + else: + self._on_enable_all() + self._product_types_model.change_states( + product_types_filter.is_include, + product_types_filter.product_types + ) + self.filter_changed.emit() def _on_filter_change(self): From 674375093df91cbe5d1046a2a4f7fcc85f171277 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:02:57 +0200 Subject: [PATCH 02/38] Remove empty default product type filter --- server/settings/tools.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 3ed12d3d0a..ca19d495f8 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -499,14 +499,7 @@ DEFAULT_TOOLS_VALUES = { "workfile_lock_profiles": [] }, "loader": { - "product_type_filter_profiles": [ - { - "hosts": [], - "task_types": [], - "is_include": True, - "filter_product_types": [] - } - ] + "product_type_filter_profiles": [] }, "publish": { "template_name_profiles": [ From ef1f94016a0a220ede9d2af6d9945453c15c769a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:04:47 +0200 Subject: [PATCH 03/38] Add missing `imagesequence` product type --- server/settings/tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/tools.py b/server/settings/tools.py index ca19d495f8..62674eee2c 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -195,6 +195,7 @@ def _product_types_enum(): "editorial", "gizmo", "image", + "imagesequence", "layout", "look", "matchmove", From 9106fbba5d8b1edf4457a8b54da0e417e69a5f92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 02:05:16 +0200 Subject: [PATCH 04/38] Remove `usdShade` product type that does not actually exist in AYON --- server/settings/tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 62674eee2c..9368e29990 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -213,7 +213,6 @@ def _product_types_enum(): "setdress", "take", "usd", - "usdShade", "vdbcache", "vrayproxy", "workfile", From becf14ed68a5aa2d60a9dca712972baef0194325 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:34:37 +0200 Subject: [PATCH 05/38] Update client/ayon_core/tools/loader/control.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/control.py | 58 +++++++++++++++--------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 085b1a0b31..fa0443d876 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -433,27 +433,43 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") - def get_current_context_product_types_filter(self): - context = get_current_context() - # There may be cases where there is no current context, like - # Tray Loader so we only do this when we have a context - if all(context.values()): - settings = get_current_project_settings() - profiles = settings["core"]["tools"]["loader"]["product_type_filter_profiles"] # noqa - if profiles: - task_entity = get_current_task_entity(fields={"taskType"}) - profile = filter_profiles(profiles, key_values={ - "hosts": get_current_host_name(), - "task_types": (task_entity or {}).get("taskType") - }) - if profile: - return ProductTypesFilter( - is_include=profile["is_include"], - product_types=profile["filter_product_types"] - ) - - # Default to all as allowed - return ProductTypesFilter( + def get_product_types_filter(self, project_name): + output = ProductTypesFilter( is_include=False, product_types=[] ) + # Without host is not determined context + if self._host is None: + return output + + context = self.get_current_context() + if ( + not all(context.values()) + or context["project_name"] != project_name + ): + return output + settings = get_current_project_settings() + profiles = ( + settings + ["core"] + ["tools"] + ["loader"] + ["product_type_filter_profiles"] + ) + if not profiles: + return output + task_entity = get_current_task_entity(fields={"taskType"}) + host_name = getattr(self._host, "name", get_current_host_name()) + profile = filter_profiles( + profiles, + { + "hosts": host_name, + "task_types": (task_entity or {}).get("taskType") + } + ) + if profile: + output = ProductTypesFilter( + is_include=profile["is_include"], + product_types=profile["filter_product_types"] + ) + return output From 7105ca8d736f351738fcb391a213193d0f835a83 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:36:13 +0200 Subject: [PATCH 06/38] Refactor method --- client/ayon_core/tools/loader/abstract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index e7e8488d05..dfc83cfc20 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1019,8 +1019,11 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_current_context_product_types_filter(self): - """Return product type filter for current context. + def get_product_types_filter(self, project_name): + """Return product type filter for project name (and current context). + + Args: + project_name (str): Project name. Returns: ProductTypesFilter: Product type filter for current context From d8fcc4c85cd131b8a18f5162fcb8724435dc7f9d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 13:49:44 +0200 Subject: [PATCH 07/38] Fix for refactored method --- client/ayon_core/tools/loader/ui/product_types_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 4e024c4417..5401e8830e 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -171,8 +171,9 @@ class ProductTypesView(QtWidgets.QListView): # Apply product types filter if self._refresh_product_types_filter: + project_name = self._controller.get_selected_project_name() product_types_filter = ( - self._controller.get_current_context_product_types_filter() + self._controller.get_product_types_filter(project_name) ) if product_types_filter.is_include: self._on_disable_all() From 88959b2c54a5a9569d9e426a29587bd829c3b72f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 Jul 2024 14:04:38 +0200 Subject: [PATCH 08/38] Move product types filter logic to the model --- .../tools/loader/ui/product_types_widget.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 5401e8830e..ff62ec0bd5 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -71,6 +71,21 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self._refreshing = False self.refreshed.emit() + def reset_product_types_filter(self): + + project_name = self._controller.get_selected_project_name() + product_types_filter = ( + self._controller.get_product_types_filter(project_name) + ) + if product_types_filter.is_include: + self.change_state_for_all(False) + else: + self.change_state_for_all(True) + self.change_states( + product_types_filter.is_include, + product_types_filter.product_types + ) + def setData(self, index, value, role=None): checkstate_changed = False if role is None: @@ -169,20 +184,9 @@ class ProductTypesView(QtWidgets.QListView): def _on_refresh_finished(self): - # Apply product types filter + # Apply product types filter on first show if self._refresh_product_types_filter: - project_name = self._controller.get_selected_project_name() - product_types_filter = ( - self._controller.get_product_types_filter(project_name) - ) - if product_types_filter.is_include: - self._on_disable_all() - else: - self._on_enable_all() - self._product_types_model.change_states( - product_types_filter.is_include, - product_types_filter.product_types - ) + self._product_types_model.reset_product_types_filter() self.filter_changed.emit() From eda080d86d987775697728949973019d8d2c1a1d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 15:59:16 +0200 Subject: [PATCH 09/38] Added profile to filter environment variables on farm --- server/settings/main.py | 46 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 40e16e7e91..1329f465e0 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -169,6 +169,46 @@ class VersionStartCategoryModel(BaseSettingsModel): ) +class EnvironmentReplacementModel(BaseSettingsModel): + environment_key: str = SettingsField("", title="Enviroment variable") + pattern: str = SettingsField("", title="Pattern") + replacement: str = SettingsField("", title="Replacement") + + +class FilterFarmEnvironmentModel(BaseSettingsModel): + _layout = "expanded" + + hosts: list[str] = SettingsField( + default_factory=list, + title="Host names" + ) + + task_types: list[str] = SettingsField( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names" + ) + + folders: list[str] = SettingsField( + default_factory=list, + title="Folders" + ) + + skip_environment: list[str] = SettingsField( + default_factory=list, + title="Skip environment variables" + ) + replace_in_environment: list[EnvironmentReplacementModel] = SettingsField( + default_factory=list, + title="Replace values in environment" + ) + + class CoreSettings(BaseSettingsModel): studio_name: str = SettingsField("", title="Studio name", scope=["studio"]) studio_code: str = SettingsField("", title="Studio code", scope=["studio"]) @@ -219,6 +259,9 @@ class CoreSettings(BaseSettingsModel): title="Project environments", section="---" ) + filter_farm_environment: list[FilterFarmEnvironmentModel] = SettingsField( + default_factory=list, + ) @validator( "environments", @@ -313,5 +356,6 @@ DEFAULT_VALUES = { "project_environments": json.dumps( {}, indent=4 - ) + ), + "filter_farm_environment": [], } From 274ed655e9631aacc616695028eb29b59637b226 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:00:14 +0200 Subject: [PATCH 10/38] Added hook filtering farm environment variables Should be triggered only on farm. Used to modify env var on farm machines like license path etc. --- .../hooks/pre_filter_farm_environments.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 client/ayon_core/hooks/pre_filter_farm_environments.py diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py new file mode 100644 index 0000000000..9a52f53950 --- /dev/null +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -0,0 +1,78 @@ +import copy +import re + +from ayon_applications import PreLaunchHook, LaunchTypes +from ayon_core.lib import filter_profiles + + +class FilterFarmEnvironments(PreLaunchHook): + """Filter or modify calculated environment variables for farm rendering. + + This hook must run last, only after all other hooks are finished to get + correct environment for launch context. + + Implemented modifications to self.launch_context.env: + - skipping (list) of environment variable keys + - removing value in environment variable: + - supports regular expression in pattern + - doesn't remove env var if value empty! + """ + order = 1000 + + launch_types = {LaunchTypes.farm_publish} + + def execute(self): + data = self.launch_context.data + project_settings = data["project_settings"] + filter_env_profiles = ( + project_settings["core"]["filter_farm_environment"]) + + if not filter_env_profiles: + self.log.debug("No profiles found for env var filtering") + return + + task_entity = data["task_entity"] + + filter_data = { + "hosts": self.host_name, + "task_types": task_entity["taskType"], + "tasks": task_entity["name"], + "folders": data["folder_path"] + } + matching_profile = filter_profiles( + filter_env_profiles, filter_data, logger=self.log + ) + if not matching_profile: + self.log.debug("No matching profile found for env var filtering " + f"for {filter_data}") + return + + calculated_env = copy.deepcopy(self.launch_context.env) + + calculated_env = self._skip_environment_variables( + calculated_env, matching_profile) + + calculated_env = self._modify_environment_variables( + calculated_env, matching_profile) + + self.launch_context.env = calculated_env + + def _modify_environment_variables(self, calculated_env, matching_profile): + """Modify environment variable values.""" + for env_item in matching_profile["replace_in_environment"]: + value = calculated_env.get(env_item["environment_key"]) + if not value: + continue + + value = re.sub(value, env_item["pattern"], env_item["replacement"]) + calculated_env[env_item["environment_key"]] = value + + return calculated_env + + def _skip_environment_variables(self, calculated_env, matching_profile): + """Skips list of environment variable names""" + for skip_env in matching_profile["skip_environment"]: + self.log.info(f"Skipping {skip_env}") + calculated_env.pop(skip_env) + + return calculated_env From 3f4a491e8f9ce83458fc5ae76305b495690f0c57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:39:28 +0200 Subject: [PATCH 11/38] Update variable name for skipped env vars Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 1329f465e0..717897a70b 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -199,7 +199,7 @@ class FilterFarmEnvironmentModel(BaseSettingsModel): title="Folders" ) - skip_environment: list[str] = SettingsField( + skip_env_keys: list[str] = SettingsField( default_factory=list, title="Skip environment variables" ) From 291e3eaa4c217cdacde15456080ad6c963263194 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:46:41 +0200 Subject: [PATCH 12/38] Update names of profile fields to be more descriptive --- client/ayon_core/hooks/pre_filter_farm_environments.py | 6 +++--- server/settings/main.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 9a52f53950..837116d5eb 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -34,10 +34,10 @@ class FilterFarmEnvironments(PreLaunchHook): task_entity = data["task_entity"] filter_data = { - "hosts": self.host_name, + "host_names": self.host_name, "task_types": task_entity["taskType"], - "tasks": task_entity["name"], - "folders": data["folder_path"] + "task_names": task_entity["name"], + "folder_paths": data["folder_path"] } matching_profile = filter_profiles( filter_env_profiles, filter_data, logger=self.log diff --git a/server/settings/main.py b/server/settings/main.py index 1329f465e0..b6cfbe36ae 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -178,7 +178,7 @@ class EnvironmentReplacementModel(BaseSettingsModel): class FilterFarmEnvironmentModel(BaseSettingsModel): _layout = "expanded" - hosts: list[str] = SettingsField( + host_names: list[str] = SettingsField( default_factory=list, title="Host names" ) @@ -194,9 +194,9 @@ class FilterFarmEnvironmentModel(BaseSettingsModel): title="Task names" ) - folders: list[str] = SettingsField( + folder_paths: list[str] = SettingsField( default_factory=list, - title="Folders" + title="Folder paths" ) skip_environment: list[str] = SettingsField( From 3dc12c7954fae1dacba9e90db6505292a4583ce6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 Jul 2024 16:49:33 +0200 Subject: [PATCH 13/38] Simplified methods for manipulating environments --- .../hooks/pre_filter_farm_environments.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 837116d5eb..95ddec990c 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -1,4 +1,3 @@ -import copy import re from ayon_applications import PreLaunchHook, LaunchTypes @@ -47,15 +46,11 @@ class FilterFarmEnvironments(PreLaunchHook): f"for {filter_data}") return - calculated_env = copy.deepcopy(self.launch_context.env) + self._skip_environment_variables( + self.launch_context.env, matching_profile) - calculated_env = self._skip_environment_variables( - calculated_env, matching_profile) - - calculated_env = self._modify_environment_variables( - calculated_env, matching_profile) - - self.launch_context.env = calculated_env + self._modify_environment_variables( + self.launch_context.env, matching_profile) def _modify_environment_variables(self, calculated_env, matching_profile): """Modify environment variable values.""" @@ -67,12 +62,8 @@ class FilterFarmEnvironments(PreLaunchHook): value = re.sub(value, env_item["pattern"], env_item["replacement"]) calculated_env[env_item["environment_key"]] = value - return calculated_env - def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" for skip_env in matching_profile["skip_environment"]: self.log.info(f"Skipping {skip_env}") calculated_env.pop(skip_env) - - return calculated_env From f473e987dfa44b0985dbef9380b0a5fbe9d45cf0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 Jul 2024 11:27:29 +0200 Subject: [PATCH 14/38] Fix key name from Settings --- client/ayon_core/hooks/pre_filter_farm_environments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 95ddec990c..cabd705d81 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -64,6 +64,6 @@ class FilterFarmEnvironments(PreLaunchHook): def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" - for skip_env in matching_profile["skip_environment"]: + for skip_env in matching_profile["skip_env_keys"]: self.log.info(f"Skipping {skip_env}") calculated_env.pop(skip_env) From 07d0bcc7526ee643b6f8c04e00a254493070b88c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 Jul 2024 12:10:11 +0200 Subject: [PATCH 15/38] Remove empty environment variable --- client/ayon_core/hooks/pre_filter_farm_environments.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index cabd705d81..0f83c0d3e0 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -14,7 +14,6 @@ class FilterFarmEnvironments(PreLaunchHook): - skipping (list) of environment variable keys - removing value in environment variable: - supports regular expression in pattern - - doesn't remove env var if value empty! """ order = 1000 @@ -55,12 +54,16 @@ class FilterFarmEnvironments(PreLaunchHook): def _modify_environment_variables(self, calculated_env, matching_profile): """Modify environment variable values.""" for env_item in matching_profile["replace_in_environment"]: - value = calculated_env.get(env_item["environment_key"]) + key = env_item["environment_key"] + value = calculated_env.get(key) if not value: continue value = re.sub(value, env_item["pattern"], env_item["replacement"]) - calculated_env[env_item["environment_key"]] = value + if value: + calculated_env[key] = value + else: + calculated_env.pop(key) def _skip_environment_variables(self, calculated_env, matching_profile): """Skips list of environment variable names""" From df5623e9f6f1d003237cce5e781580088ead3a60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:17:03 +0200 Subject: [PATCH 16/38] added option to trigger tray message --- client/ayon_core/tools/tray/ui/tray.py | 36 +++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..6077820fab 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -133,6 +133,7 @@ class TrayManager: kwargs["msecs"] = msecs self.tray_widget.showMessage(*args, **kwargs) + # TODO validate 'self.tray_widget.supportsMessages()' def initialize_addons(self): """Add addons to tray.""" @@ -145,6 +146,9 @@ class TrayManager: self._addons_manager.add_route( "GET", "/tray", self._get_web_tray_info ) + self._addons_manager.add_route( + "POST", "/tray/message", self._web_show_tray_message + ) admin_submenu = ITrayAction.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) @@ -285,7 +289,37 @@ class TrayManager: }, "installer_version": os.getenv("AYON_VERSION"), "running_time": time.time() - self._start_time, - })) + }) + + async def _web_show_tray_message(self, request: Request) -> Response: + data = await request.json() + try: + title = data["title"] + message = data["message"] + icon = data.get("icon") + msecs = data.get("msecs") + except KeyError as exc: + return json_response( + { + "error": f"Missing required data. {exc}", + "success": False, + }, + status=400, + ) + + if icon == "information": + icon = QtWidgets.QSystemTrayIconInformation + elif icon == "warning": + icon = QtWidgets.QSystemTrayIconWarning + elif icon == "critical": + icon = QtWidgets.QSystemTrayIcon.Critical + else: + icon = None + + self.execute_in_main_thread( + self.show_tray_message, title, message, icon, msecs + ) + return json_response({"success": True}) def _on_update_check_timer(self): try: From 3156e91e978f4cf6d686a6c12581a95712fc5c16 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:46:44 +0200 Subject: [PATCH 17/38] added helper function to send message to tray --- client/ayon_core/tools/tray/lib.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e13c682ab0..377a844321 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -344,6 +344,38 @@ def is_tray_running( return state != TrayState.NOT_RUNNING +def show_message_in_tray( + title, message, icon=None, msecs=None, tray_url=None +): + """Show message in tray. + + Args: + title (str): Message title. + message (str): Message content. + icon (Optional[str]): Icon for the message. + msecs (Optional[int]): Duration of the message. + tray_url (Optional[str]): Tray server url. + + """ + if not tray_url: + tray_url = get_tray_server_url() + + # TODO handle this case, e.g. raise an error? + if not tray_url: + return + + # TODO handle response, can fail whole request or can fail on status + requests.post( + f"{tray_url}/tray/message", + json={ + "title": title, + "message": message, + "icon": icon, + "msecs": msecs + } + ) + + def main(): from ayon_core.tools.tray.ui import main From dce3a4b6a25205a138d9cf26c7cf78eab98e7383 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:48:33 +0200 Subject: [PATCH 18/38] trigger show message if tray is already running --- client/ayon_core/tools/tray/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 377a844321..926a0c03cd 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -383,6 +383,10 @@ def main(): state = get_tray_state() if state == TrayState.RUNNING: + show_message_in_tray( + "Tray is already running", + "Your AYON tray application is already running." + ) print("Tray is already running.") return From 947ecfd9182b405a618af1a89e476676cb927e4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:16:39 +0200 Subject: [PATCH 19/38] add username to tray information --- client/ayon_core/tools/tray/ui/tray.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 660c61ac94..aed1fe2139 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -3,12 +3,11 @@ import sys import time import collections import atexit -import json import platform -from aiohttp.web_response import Response import ayon_api from qtpy import QtCore, QtGui, QtWidgets +from aiohttp.web import Response, json_response, Request from ayon_core import resources, style from ayon_core.lib import ( @@ -91,6 +90,10 @@ class TrayManager: self._services_submenu = None self._start_time = time.time() + # Cache AYON username used in process + # - it can change only by changing ayon_api global connection + # should be safe for tray application to cache the value only once + self._cached_username = None self._closing = False try: set_tray_server_url( @@ -143,7 +146,7 @@ class TrayManager: self._addons_manager.initialize(tray_menu) self._addons_manager.add_route( - "GET", "/tray", self._get_web_tray_info + "GET", "/tray", self._web_get_tray_info ) admin_submenu = ITrayAction.admin_submenu(tray_menu) @@ -274,8 +277,12 @@ class TrayManager: return item - async def _get_web_tray_info(self, request): - return Response(text=json.dumps({ + async def _web_get_tray_info(self, _request: Request) -> Response: + if self._cached_username is None: + self._cached_username = ayon_api.get_user()["name"] + + return json_response({ + "username": self._cached_username, "bundle": os.getenv("AYON_BUNDLE_NAME"), "dev_mode": is_dev_mode_enabled(), "staging_mode": is_staging_enabled(), From 6bbd48e989cd8251e921fff520a4b513bdb234e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:17:39 +0200 Subject: [PATCH 20/38] fix closing bracket --- client/ayon_core/tools/tray/ui/tray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index aed1fe2139..16e8434302 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -292,7 +292,7 @@ class TrayManager: }, "installer_version": os.getenv("AYON_VERSION"), "running_time": time.time() - self._start_time, - })) + }) def _on_update_check_timer(self): try: From aa1a3928d3dbfbc0c0f6e8326bd330ea85c53cd2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 Jul 2024 22:21:14 +0200 Subject: [PATCH 21/38] Remove newlines, or just write a first chapter book MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > Why empty first line? It is like opening book that starts with 2nd chapter 🙂 Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/ui/product_types_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index ff62ec0bd5..0303f97d09 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -72,7 +72,6 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self.refreshed.emit() def reset_product_types_filter(self): - project_name = self._controller.get_selected_project_name() product_types_filter = ( self._controller.get_product_types_filter(project_name) @@ -183,7 +182,6 @@ class ProductTypesView(QtWidgets.QListView): super().showEvent(event) def _on_refresh_finished(self): - # Apply product types filter on first show if self._refresh_product_types_filter: self._product_types_model.reset_product_types_filter() From b9067cde3d1f8ca0dab0ec0b07cac6504c9a1ea5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:39:00 +0200 Subject: [PATCH 22/38] remove 'checked' attribute from product type item --- client/ayon_core/tools/loader/abstract.py | 5 +---- client/ayon_core/tools/loader/models/products.py | 4 ++-- client/ayon_core/tools/loader/ui/product_types_widget.py | 5 ----- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index dfc83cfc20..c715b9ce99 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -14,19 +14,16 @@ class ProductTypeItem: Args: name (str): Product type name. icon (dict[str, Any]): Product type icon definition. - checked (bool): Is product type checked for filtering. """ - def __init__(self, name, icon, checked): + def __init__(self, name, icon): self.name = name self.icon = icon - self.checked = checked def to_data(self): return { "name": self.name, "icon": self.icon, - "checked": self.checked, } @classmethod diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index c9325c4480..58eab0cabe 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -123,7 +123,7 @@ def product_type_item_from_data(product_type_data): "color": "#0091B2", } # TODO implement checked logic - return ProductTypeItem(product_type_data["name"], icon, True) + return ProductTypeItem(product_type_data["name"], icon) def create_default_product_type_item(product_type): @@ -132,7 +132,7 @@ def create_default_product_type_item(product_type): "name": "fa.folder", "color": "#0091B2", } - return ProductTypeItem(product_type, icon, True) + return ProductTypeItem(product_type, icon) class ProductsModel: diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index 0303f97d09..dfccd8f349 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -52,11 +52,6 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): new_items.append(item) self._items_by_name[name] = item - item.setCheckState( - QtCore.Qt.Checked - if product_type_item.checked - else QtCore.Qt.Unchecked - ) icon = get_qt_icon(product_type_item.icon) item.setData(icon, QtCore.Qt.DecorationRole) From c75dbd6c4ed971988a37adaaec314fde0eb19b81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:40:32 +0200 Subject: [PATCH 23/38] receive information only from context data --- client/ayon_core/tools/loader/control.py | 28 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index fa0443d876..b83cb74e76 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -3,9 +3,8 @@ import uuid import ayon_api -from ayon_core.settings import get_current_project_settings +from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_host_name -from ayon_core.pipeline.context_tools import get_current_task_entity from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import Anatomy, get_current_context @@ -443,12 +442,10 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): return output context = self.get_current_context() - if ( - not all(context.values()) - or context["project_name"] != project_name - ): + project_name = context.get("project_name") + if not project_name: return output - settings = get_current_project_settings() + settings = get_project_settings(project_name) profiles = ( settings ["core"] @@ -458,13 +455,26 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): ) if not profiles: return output - task_entity = get_current_task_entity(fields={"taskType"}) + + folder_id = context.get("folder_id") + task_name = context.get("task_name") + task_type = None + if folder_id and task_name: + task_entity = ayon_api.get_task_by_name( + project_name, + folder_id, + task_name, + fields={"taskType"} + ) + if task_entity: + task_type = task_entity.get("taskType") + host_name = getattr(self._host, "name", get_current_host_name()) profile = filter_profiles( profiles, { "hosts": host_name, - "task_types": (task_entity or {}).get("taskType") + "task_types": task_type, } ) if profile: From 9af0e6e1cdffae9422eeaed16accb4f6da3505b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:42:37 +0200 Subject: [PATCH 24/38] rename 'is_include' to 'is_allow_list' --- client/ayon_core/tools/loader/abstract.py | 4 ++-- client/ayon_core/tools/loader/control.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index c715b9ce99..14ed831d4b 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -349,9 +349,9 @@ class ProductTypesFilter: Defines the filtering for product types. """ - def __init__(self, product_types: List[str], is_include: bool): + def __init__(self, product_types: List[str], is_allow_list: bool): self.product_types: List[str] = product_types - self.is_include: bool = is_include + self.is_allow_list: bool = is_allow_list class _BaseLoaderController(ABC): diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index b83cb74e76..181e52218f 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -434,7 +434,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_product_types_filter(self, project_name): output = ProductTypesFilter( - is_include=False, + is_allow_list=False, product_types=[] ) # Without host is not determined context @@ -479,7 +479,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): ) if profile: output = ProductTypesFilter( - is_include=profile["is_include"], + is_allow_list=profile["is_include"], product_types=profile["filter_product_types"] ) return output From 1cacc3b723f6862f5dab7ec7a6328f68f8d78a43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:12 +0200 Subject: [PATCH 25/38] don't require project name in 'get_product_types_filter' --- client/ayon_core/tools/loader/abstract.py | 5 +---- client/ayon_core/tools/loader/control.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 14ed831d4b..4c8893bf95 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1016,12 +1016,9 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_product_types_filter(self, project_name): + def get_product_types_filter(self): """Return product type filter for project name (and current context). - Args: - project_name (str): Project name. - Returns: ProductTypesFilter: Product type filter for current context """ diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 181e52218f..6a809967f7 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -432,7 +432,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def _emit_event(self, topic, data=None): self._event_system.emit(topic, data or {}, "controller") - def get_product_types_filter(self, project_name): + def get_product_types_filter(self): output = ProductTypesFilter( is_allow_list=False, product_types=[] From 4521188ecf6fe3cabf46c01560c113c9dbe35b3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:30 +0200 Subject: [PATCH 26/38] modified product types widget to work as expected --- .../tools/loader/ui/product_types_widget.py | 60 ++++++++++++------- client/ayon_core/tools/loader/ui/window.py | 2 + 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py index dfccd8f349..9b1bf6326f 100644 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ b/client/ayon_core/tools/loader/ui/product_types_widget.py @@ -13,10 +13,17 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): super(ProductTypesQtModel, self).__init__() self._controller = controller + self._reset_filters_on_refresh = True self._refreshing = False self._bulk_change = False + self._last_project = None self._items_by_name = {} + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset_finish, + ) + def is_refreshing(self): return self._refreshing @@ -37,14 +44,19 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): self._refreshing = True product_type_items = self._controller.get_product_type_items( project_name) + self._last_project = project_name items_to_remove = set(self._items_by_name.keys()) new_items = [] + items_filter_required = {} for product_type_item in product_type_items: name = product_type_item.name items_to_remove.discard(name) - item = self._items_by_name.get(product_type_item.name) + item = self._items_by_name.get(name) + # Apply filter to new items or if filters reset is requested + filter_required = self._reset_filters_on_refresh if item is None: + filter_required = True item = QtGui.QStandardItem(name) item.setData(name, PRODUCT_TYPE_ROLE) item.setEditable(False) @@ -52,9 +64,26 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): new_items.append(item) self._items_by_name[name] = item + if filter_required: + items_filter_required[name] = item + icon = get_qt_icon(product_type_item.icon) item.setData(icon, QtCore.Qt.DecorationRole) + if items_filter_required: + product_types_filter = self._controller.get_product_types_filter() + for product_type, item in items_filter_required.items(): + matching = ( + int(product_type in product_types_filter.product_types) + + int(product_types_filter.is_allow_list) + ) + state = ( + QtCore.Qt.Checked + if matching % 2 == 0 + else QtCore.Qt.Unchecked + ) + item.setCheckState(state) + root_item = self.invisibleRootItem() if new_items: root_item.appendRows(new_items) @@ -63,22 +92,12 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): item = self._items_by_name.pop(name) root_item.removeRow(item.row()) + self._reset_filters_on_refresh = False self._refreshing = False self.refreshed.emit() - def reset_product_types_filter(self): - project_name = self._controller.get_selected_project_name() - product_types_filter = ( - self._controller.get_product_types_filter(project_name) - ) - if product_types_filter.is_include: - self.change_state_for_all(False) - else: - self.change_state_for_all(True) - self.change_states( - product_types_filter.is_include, - product_types_filter.product_types - ) + def reset_product_types_filter_on_refresh(self): + self._reset_filters_on_refresh = True def setData(self, index, value, role=None): checkstate_changed = False @@ -131,6 +150,9 @@ class ProductTypesQtModel(QtGui.QStandardItemModel): if changed: self.filter_changed.emit() + def _on_controller_reset_finish(self): + self.refresh(self._last_project) + class ProductTypesView(QtWidgets.QListView): filter_changed = QtCore.Signal() @@ -168,19 +190,15 @@ class ProductTypesView(QtWidgets.QListView): def get_filter_info(self): return self._product_types_model.get_filter_info() + def reset_product_types_filter_on_refresh(self): + self._product_types_model.reset_product_types_filter_on_refresh() + def _on_project_change(self, event): project_name = event["project_name"] self._product_types_model.refresh(project_name) - def showEvent(self, event): - self._refresh_product_types_filter = True - super().showEvent(event) - def _on_refresh_finished(self): # Apply product types filter on first show - if self._refresh_product_types_filter: - self._product_types_model.reset_product_types_filter() - self.filter_changed.emit() def _on_filter_change(self): diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 58af6f0b1f..31c9908b23 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -345,6 +345,8 @@ class LoaderWindow(QtWidgets.QWidget): def closeEvent(self, event): super(LoaderWindow, self).closeEvent(event) + self._product_types_widget.reset_product_types_filter_on_refresh() + self._reset_on_show = True def keyPressEvent(self, event): From d812395af99b6b545eef488bb2092fcd661cc6c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:43:44 +0200 Subject: [PATCH 27/38] use full variable names --- client/ayon_core/tools/loader/control.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 6a809967f7..0ea2903544 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -337,11 +337,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name = context.get("project_name") folder_path = context.get("folder_path") if project_name and folder_path: - folder = ayon_api.get_folder_by_path( + folder_entity = ayon_api.get_folder_by_path( project_name, folder_path, fields=["id"] ) - if folder: - folder_id = folder["id"] + if folder_entity: + folder_id = folder_entity["id"] return { "project_name": project_name, "folder_id": folder_id, From 2eecac36da7ac9e7c873bac308d5b7d709e69035 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:53:04 +0200 Subject: [PATCH 28/38] change settings for better readability --- client/ayon_core/tools/loader/control.py | 6 +++++- server/settings/tools.py | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 0ea2903544..2da77337fb 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -478,8 +478,12 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): } ) if profile: + # TODO remove 'is_include' after release '0.4.3' + is_allow_list = profile.get("is_include") + if is_allow_list is None: + is_allow_list = profile["filter_type"] == "is_allow_list" output = ProductTypesFilter( - is_allow_list=profile["is_include"], + is_allow_list=is_allow_list, product_types=profile["filter_product_types"] ) return output diff --git a/server/settings/tools.py b/server/settings/tools.py index 9368e29990..85a66f6a70 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -222,6 +222,13 @@ def _product_types_enum(): ] +def filter_type_enum(): + return [ + {"value": "is_allow_list", "label": "Allow list"}, + {"value": "is_deny_list", "label": "Deny list"}, + ] + + class LoaderProductTypeFilterProfile(BaseSettingsModel): _layout = "expanded" # TODO this should use hosts enum @@ -231,9 +238,15 @@ class LoaderProductTypeFilterProfile(BaseSettingsModel): title="Task types", enum_resolver=task_types_enum ) - is_include: bool = SettingsField(True, title="Exclude / Include") + filter_type: str = SettingsField( + "is_allow_list", + title="Filter type", + section="Product type filter", + enum_resolver=filter_type_enum + ) filter_product_types: list[str] = SettingsField( default_factory=list, + title="Product types", enum_resolver=_product_types_enum ) From ea547ed53974a0c2868c55a20603cfd411d28e9e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 Jul 2024 14:14:45 +0200 Subject: [PATCH 29/38] Update client/ayon_core/tools/loader/abstract.py --- client/ayon_core/tools/loader/abstract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 4c8893bf95..0b790dfbbd 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1017,7 +1017,7 @@ class FrontendLoaderController(_BaseLoaderController): @abstractmethod def get_product_types_filter(self): - """Return product type filter for project name (and current context). + """Return product type filter for current context. Returns: ProductTypesFilter: Product type filter for current context From aad7e2902dba233aedb99585c95192882c5b16be Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:33:55 +0200 Subject: [PATCH 30/38] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 6596a9ecba..564dd92bd2 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -259,7 +259,7 @@ class CoreSettings(BaseSettingsModel): title="Project environments", section="---" ) - filter_farm_environment: list[FilterFarmEnvironmentModel] = SettingsField( + filter_env_profiles: list[FilterEnvsProfileModel] = SettingsField( default_factory=list, ) From 6b66de7daadd93cae586c8829468a7886f758cac Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:34:22 +0200 Subject: [PATCH 31/38] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 564dd92bd2..986a9ed1c5 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -357,5 +357,5 @@ DEFAULT_VALUES = { {}, indent=4 ), - "filter_farm_environment": [], + "filter_env_profiles": [], } From 6a4196c5b48e133fade1aec7bc396691f42f6469 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:34:35 +0200 Subject: [PATCH 32/38] Change name of variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hooks/pre_filter_farm_environments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_filter_farm_environments.py b/client/ayon_core/hooks/pre_filter_farm_environments.py index 0f83c0d3e0..d231acf5e9 100644 --- a/client/ayon_core/hooks/pre_filter_farm_environments.py +++ b/client/ayon_core/hooks/pre_filter_farm_environments.py @@ -23,7 +23,7 @@ class FilterFarmEnvironments(PreLaunchHook): data = self.launch_context.data project_settings = data["project_settings"] filter_env_profiles = ( - project_settings["core"]["filter_farm_environment"]) + project_settings["core"]["filter_env_profiles"]) if not filter_env_profiles: self.log.debug("No profiles found for env var filtering") From 2003ae81b40d7c12585c6ef60de250af923eff2b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jul 2024 14:35:06 +0200 Subject: [PATCH 33/38] Change name of model Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/main.py b/server/settings/main.py index 986a9ed1c5..0972ccdfb9 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -175,7 +175,7 @@ class EnvironmentReplacementModel(BaseSettingsModel): replacement: str = SettingsField("", title="Replacement") -class FilterFarmEnvironmentModel(BaseSettingsModel): +class FilterEnvsProfileModel(BaseSettingsModel): _layout = "expanded" host_names: list[str] = SettingsField( From f90ac20f463e696b751544f2505297c7e9499d68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:34:25 +0200 Subject: [PATCH 34/38] add Literal to docstring --- client/ayon_core/tools/tray/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7c80f467f2..ad190482a8 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -364,7 +364,8 @@ def show_message_in_tray( Args: title (str): Message title. message (str): Message content. - icon (Optional[str]): Icon for the message. + icon (Optional[Literal["information", "warning", "critical"]]): Icon + for the message. msecs (Optional[int]): Duration of the message. tray_url (Optional[str]): Tray server url. From a7f56175d6bbdfc3accba9843f1d322e9405997b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:36:41 +0200 Subject: [PATCH 35/38] move some logic from cli_commands.py to cli.py --- client/ayon_core/cli.py | 38 ++++++++++++++++++++------------ client/ayon_core/cli_commands.py | 34 ---------------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 0a9bb2aa9c..acc7dfb6d4 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -5,6 +5,7 @@ import sys import code import traceback from pathlib import Path +import warnings import click import acre @@ -116,14 +117,25 @@ def extractenvironments( This function is deprecated and will be removed in future. Please use 'addon applications extractenvironments ...' instead. """ - Commands.extractenvironments( - output_json_path, - project, - asset, - task, - app, - envgroup, - ctx.obj["addons_manager"] + warnings.warn( + ( + "Command 'extractenvironments' is deprecated and will be" + " removed in future. Please use" + " 'addon applications extractenvironments ...' instead." + ), + DeprecationWarning + ) + + addons_manager = ctx.obj["addons_manager"] + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is None: + raise RuntimeError( + "Applications addon is not available or enabled." + ) + + # Please ignore the fact this is using private method + applications_addon._cli_extract_environments( + output_json_path, project, asset, task, app, envgroup ) @@ -170,12 +182,10 @@ def contextselection( Context is project name, folder path and task name. The result is stored into json file which path is passed in first argument. """ - Commands.contextselection( - output_path, - project, - folder, - strict - ) + from ayon_core.tools.context_dialog import main + + main(output_path, project, folder, strict) + @main_cli.command( diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 8ae1ebb3ba..085dd5bb04 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -2,7 +2,6 @@ """Implementation of AYON commands.""" import os import sys -import warnings from typing import Optional, List from ayon_core.addon import AddonsManager @@ -136,36 +135,3 @@ class Commands: log.info("Publish finished.") - @staticmethod - def extractenvironments( - output_json_path, project, asset, task, app, env_group, addons_manager - ): - """Produces json file with environment based on project and app. - - Called by Deadline plugin to propagate environment into render jobs. - """ - warnings.warn( - ( - "Command 'extractenvironments' is deprecated and will be" - " removed in future. Please use " - "'addon applications extractenvironments ...' instead." - ), - DeprecationWarning - ) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is None: - raise RuntimeError( - "Applications addon is not available or enabled." - ) - - # Please ignore the fact this is using private method - applications_addon._cli_extract_environments( - output_json_path, project, asset, task, app, env_group - ) - - @staticmethod - def contextselection(output_path, project_name, folder_path, strict): - from ayon_core.tools.context_dialog import main - - main(output_path, project_name, folder_path, strict) From 78c278cdde4da37404d79b4008ff7323411b4af2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:47:28 +0200 Subject: [PATCH 36/38] move publish to cli.py --- client/ayon_core/cli.py | 99 +++++++++++++++++++++- client/ayon_core/cli_commands.py | 137 ------------------------------- 2 files changed, 97 insertions(+), 139 deletions(-) delete mode 100644 client/ayon_core/cli_commands.py diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index acc7dfb6d4..b7dad94346 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -19,7 +19,6 @@ from ayon_core.lib import ( Logger, ) -from .cli_commands import Commands class AliasedGroup(click.Group): @@ -152,7 +151,103 @@ def publish(ctx, path, targets, gui): Publish collects json from path provided as an argument. """ - Commands.publish(path, targets, gui, ctx.obj["addons_manager"]) + import ayon_api + import pyblish.util + + from ayon_core.pipeline import ( + install_ayon_plugins, + get_global_context, + ) + + # Register target and host + if not isinstance(path, str): + raise RuntimeError("Path to JSON must be a string.") + + # Fix older jobs + for src_key, dst_key in ( + ("AVALON_PROJECT", "AYON_PROJECT_NAME"), + ("AVALON_ASSET", "AYON_FOLDER_PATH"), + ("AVALON_TASK", "AYON_TASK_NAME"), + ("AVALON_WORKDIR", "AYON_WORKDIR"), + ("AVALON_APP_NAME", "AYON_APP_NAME"), + ("AVALON_APP", "AYON_HOST_NAME"), + ): + if src_key in os.environ and dst_key not in os.environ: + os.environ[dst_key] = os.environ[src_key] + # Remove old keys, so we're sure they're not used + os.environ.pop(src_key, None) + + log = Logger.get_logger("CLI-publish") + + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + + install_ayon_plugins() + + addons_manager = ctx.obj["addons_manager"] + + # TODO validate if this has to happen + # - it should happen during 'install_ayon_plugins' + publish_paths = addons_manager.collect_plugin_paths()["publish"] + for plugin_path in publish_paths: + pyblish.api.register_plugin_path(plugin_path) + + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is not None: + context = get_global_context() + env = applications_addon.get_farm_publish_environment_variables( + context["project_name"], + context["folder_path"], + context["task_name"], + ) + os.environ.update(env) + + pyblish.api.register_host("shell") + + if targets: + for target in targets: + print(f"setting target: {target}") + pyblish.api.register_target(target) + else: + pyblish.api.register_target("farm") + + os.environ["AYON_PUBLISH_DATA"] = path + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + + log.info("Running publish ...") + + plugins = pyblish.api.discover() + print("Using plugins:") + for plugin in plugins: + print(plugin) + + if gui: + from ayon_core.tools.utils.host_tools import show_publish + from ayon_core.tools.utils.lib import qt_app_context + with qt_app_context(): + show_publish() + else: + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") + + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) + + log.info("Publish finished.") @main_cli.command(context_settings={"ignore_unknown_options": True}) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py deleted file mode 100644 index 085dd5bb04..0000000000 --- a/client/ayon_core/cli_commands.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -"""Implementation of AYON commands.""" -import os -import sys -from typing import Optional, List - -from ayon_core.addon import AddonsManager - - -class Commands: - """Class implementing commands used by AYON. - - Most of its methods are called by :mod:`cli` module. - """ - @staticmethod - def publish( - path: str, - targets: Optional[List[str]] = None, - gui: Optional[bool] = False, - addons_manager: Optional[AddonsManager] = None, - ) -> None: - """Start headless publishing. - - Publish use json from passed path argument. - - Args: - path (str): Path to JSON. - targets (Optional[List[str]]): List of pyblish targets. - gui (Optional[bool]): Show publish UI. - addons_manager (Optional[AddonsManager]): Addons manager instance. - - Raises: - RuntimeError: When there is no path to process. - RuntimeError: When executed with list of JSON paths. - - """ - from ayon_core.lib import Logger - - from ayon_core.addon import AddonsManager - from ayon_core.pipeline import ( - install_ayon_plugins, - get_global_context, - ) - - import ayon_api - import pyblish.util - - # Register target and host - if not isinstance(path, str): - raise RuntimeError("Path to JSON must be a string.") - - # Fix older jobs - for src_key, dst_key in ( - ("AVALON_PROJECT", "AYON_PROJECT_NAME"), - ("AVALON_ASSET", "AYON_FOLDER_PATH"), - ("AVALON_TASK", "AYON_TASK_NAME"), - ("AVALON_WORKDIR", "AYON_WORKDIR"), - ("AVALON_APP_NAME", "AYON_APP_NAME"), - ("AVALON_APP", "AYON_HOST_NAME"), - ): - if src_key in os.environ and dst_key not in os.environ: - os.environ[dst_key] = os.environ[src_key] - # Remove old keys, so we're sure they're not used - os.environ.pop(src_key, None) - - log = Logger.get_logger("CLI-publish") - - # Make public ayon api behave as other user - # - this works only if public ayon api is using service user - username = os.environ.get("AYON_USERNAME") - if username: - # NOTE: ayon-python-api does not have public api function to find - # out if is used service user. So we need to have try > except - # block. - con = ayon_api.get_server_api_connection() - try: - con.set_default_service_username(username) - except ValueError: - pass - - install_ayon_plugins() - - if addons_manager is None: - addons_manager = AddonsManager() - - publish_paths = addons_manager.collect_plugin_paths()["publish"] - - for plugin_path in publish_paths: - pyblish.api.register_plugin_path(plugin_path) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is not None: - context = get_global_context() - env = applications_addon.get_farm_publish_environment_variables( - context["project_name"], - context["folder_path"], - context["task_name"], - ) - os.environ.update(env) - - pyblish.api.register_host("shell") - - if targets: - for target in targets: - print(f"setting target: {target}") - pyblish.api.register_target(target) - else: - pyblish.api.register_target("farm") - - os.environ["AYON_PUBLISH_DATA"] = path - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - - log.info("Running publish ...") - - plugins = pyblish.api.discover() - print("Using plugins:") - for plugin in plugins: - print(plugin) - - if gui: - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context - with qt_app_context(): - show_publish() - else: - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") - - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) - - log.info("Publish finished.") - From 856a30cd5a2e34349a35cdc39ac51a2d9cc87ad8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:48:31 +0200 Subject: [PATCH 37/38] remove gui option --- client/ayon_core/cli.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index b7dad94346..c1b5e5d5fc 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -143,9 +143,7 @@ def extractenvironments( @click.argument("path", required=True) @click.option("-t", "--targets", help="Targets", default=None, multiple=True) -@click.option("-g", "--gui", is_flag=True, - help="Show Publish UI", default=False) -def publish(ctx, path, targets, gui): +def publish(ctx, path, targets): """Start CLI publishing. Publish collects json from path provided as an argument. @@ -231,21 +229,15 @@ def publish(ctx, path, targets, gui): for plugin in plugins: print(plugin) - if gui: - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context - with qt_app_context(): - show_publish() - else: - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) log.info("Publish finished.") From f82c420fe499bfd168d0ee3e773bf2dc064c17ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:54:42 +0200 Subject: [PATCH 38/38] create function for cli in publish --- client/ayon_core/cli.py | 92 +------------- client/ayon_core/pipeline/publish/__init__.py | 4 + client/ayon_core/pipeline/publish/lib.py | 114 +++++++++++++++++- 3 files changed, 119 insertions(+), 91 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index c1b5e5d5fc..db6674d88f 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -149,97 +149,9 @@ def publish(ctx, path, targets): Publish collects json from path provided as an argument. """ - import ayon_api - import pyblish.util + from ayon_core.pipeline.publish import main_cli_publish - from ayon_core.pipeline import ( - install_ayon_plugins, - get_global_context, - ) - - # Register target and host - if not isinstance(path, str): - raise RuntimeError("Path to JSON must be a string.") - - # Fix older jobs - for src_key, dst_key in ( - ("AVALON_PROJECT", "AYON_PROJECT_NAME"), - ("AVALON_ASSET", "AYON_FOLDER_PATH"), - ("AVALON_TASK", "AYON_TASK_NAME"), - ("AVALON_WORKDIR", "AYON_WORKDIR"), - ("AVALON_APP_NAME", "AYON_APP_NAME"), - ("AVALON_APP", "AYON_HOST_NAME"), - ): - if src_key in os.environ and dst_key not in os.environ: - os.environ[dst_key] = os.environ[src_key] - # Remove old keys, so we're sure they're not used - os.environ.pop(src_key, None) - - log = Logger.get_logger("CLI-publish") - - # Make public ayon api behave as other user - # - this works only if public ayon api is using service user - username = os.environ.get("AYON_USERNAME") - if username: - # NOTE: ayon-python-api does not have public api function to find - # out if is used service user. So we need to have try > except - # block. - con = ayon_api.get_server_api_connection() - try: - con.set_default_service_username(username) - except ValueError: - pass - - install_ayon_plugins() - - addons_manager = ctx.obj["addons_manager"] - - # TODO validate if this has to happen - # - it should happen during 'install_ayon_plugins' - publish_paths = addons_manager.collect_plugin_paths()["publish"] - for plugin_path in publish_paths: - pyblish.api.register_plugin_path(plugin_path) - - applications_addon = addons_manager.get_enabled_addon("applications") - if applications_addon is not None: - context = get_global_context() - env = applications_addon.get_farm_publish_environment_variables( - context["project_name"], - context["folder_path"], - context["task_name"], - ) - os.environ.update(env) - - pyblish.api.register_host("shell") - - if targets: - for target in targets: - print(f"setting target: {target}") - pyblish.api.register_target(target) - else: - pyblish.api.register_target("farm") - - os.environ["AYON_PUBLISH_DATA"] = path - os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib - - log.info("Running publish ...") - - plugins = pyblish.api.discover() - print("Using plugins:") - for plugin in plugins: - print(plugin) - - # Error exit as soon as any error occurs. - error_format = ("Failed {plugin.__name__}: " - "{error} -- {error.traceback}") - - for result in pyblish.util.publish_iter(): - if result["error"]: - log.error(error_format.format(**result)) - # uninstall() - sys.exit(1) - - log.info("Publish finished.") + main_cli_publish(path, targets, ctx.obj["addons_manager"]) @main_cli.command(context_settings={"ignore_unknown_options": True}) diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index d507972664..ab19b6e360 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -42,6 +42,8 @@ from .lib import ( get_plugin_settings, get_publish_instance_label, get_publish_instance_families, + + main_cli_publish, ) from .abstract_expected_files import ExpectedFiles @@ -92,6 +94,8 @@ __all__ = ( "get_publish_instance_label", "get_publish_instance_families", + "main_cli_publish", + "ExpectedFiles", "RenderInstance", diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c4e7b2a42c..8b82622e4c 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -4,8 +4,9 @@ import inspect import copy import tempfile import xml.etree.ElementTree -from typing import Optional, Union +from typing import Optional, Union, List +import ayon_api import pyblish.util import pyblish.plugin import pyblish.api @@ -16,6 +17,7 @@ from ayon_core.lib import ( filter_profiles, ) from ayon_core.settings import get_project_settings +from ayon_core.addon import AddonsManager from ayon_core.pipeline import ( tempdir, Anatomy @@ -978,3 +980,113 @@ def get_instance_expected_output_path( path_template_obj = anatomy.get_template_item("publish", "default")["path"] template_filled = path_template_obj.format_strict(template_data) return os.path.normpath(template_filled) + + +def main_cli_publish( + path: str, + targets: Optional[List[str]] = None, + addons_manager: Optional[AddonsManager] = None, +): + """Start headless publishing. + + Publish use json from passed path argument. + + Args: + path (str): Path to JSON. + targets (Optional[List[str]]): List of pyblish targets. + addons_manager (Optional[AddonsManager]): Addons manager instance. + + Raises: + RuntimeError: When there is no path to process or when executed with + list of JSON paths. + + """ + from ayon_core.pipeline import ( + install_ayon_plugins, + get_global_context, + ) + + # Register target and host + if not isinstance(path, str): + raise RuntimeError("Path to JSON must be a string.") + + # Fix older jobs + for src_key, dst_key in ( + ("AVALON_PROJECT", "AYON_PROJECT_NAME"), + ("AVALON_ASSET", "AYON_FOLDER_PATH"), + ("AVALON_TASK", "AYON_TASK_NAME"), + ("AVALON_WORKDIR", "AYON_WORKDIR"), + ("AVALON_APP_NAME", "AYON_APP_NAME"), + ("AVALON_APP", "AYON_HOST_NAME"), + ): + if src_key in os.environ and dst_key not in os.environ: + os.environ[dst_key] = os.environ[src_key] + # Remove old keys, so we're sure they're not used + os.environ.pop(src_key, None) + + log = Logger.get_logger("CLI-publish") + + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + + install_ayon_plugins() + + if addons_manager is None: + addons_manager = AddonsManager() + + # TODO validate if this has to happen + # - it should happen during 'install_ayon_plugins' + publish_paths = addons_manager.collect_plugin_paths()["publish"] + for plugin_path in publish_paths: + pyblish.api.register_plugin_path(plugin_path) + + applications_addon = addons_manager.get_enabled_addon("applications") + if applications_addon is not None: + context = get_global_context() + env = applications_addon.get_farm_publish_environment_variables( + context["project_name"], + context["folder_path"], + context["task_name"], + ) + os.environ.update(env) + + pyblish.api.register_host("shell") + + if targets: + for target in targets: + print(f"setting target: {target}") + pyblish.api.register_target(target) + else: + pyblish.api.register_target("farm") + + os.environ["AYON_PUBLISH_DATA"] = path + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib + + log.info("Running publish ...") + + plugins = pyblish.api.discover() + print("Using plugins:") + for plugin in plugins: + print(plugin) + + # Error exit as soon as any error occurs. + error_format = ("Failed {plugin.__name__}: " + "{error} -- {error.traceback}") + + for result in pyblish.util.publish_iter(): + if result["error"]: + log.error(error_format.format(**result)) + # uninstall() + sys.exit(1) + + log.info("Publish finished.")