mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/usd_workflow_use_entity_uri
This commit is contained in:
commit
a8a0161799
14 changed files with 497 additions and 225 deletions
|
|
@ -5,6 +5,7 @@ import sys
|
|||
import code
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
|
@ -18,7 +19,6 @@ from ayon_core.lib import (
|
|||
Logger,
|
||||
)
|
||||
|
||||
from .cli_commands import Commands
|
||||
|
||||
|
||||
class AliasedGroup(click.Group):
|
||||
|
|
@ -116,14 +116,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
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -132,15 +143,15 @@ 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.
|
||||
|
||||
"""
|
||||
Commands.publish(path, targets, gui, ctx.obj["addons_manager"])
|
||||
from ayon_core.pipeline.publish import main_cli_publish
|
||||
|
||||
main_cli_publish(path, targets, ctx.obj["addons_manager"])
|
||||
|
||||
|
||||
@main_cli.command(context_settings={"ignore_unknown_options": True})
|
||||
|
|
@ -170,12 +181,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(
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Implementation of AYON commands."""
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
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.")
|
||||
|
||||
@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)
|
||||
72
client/ayon_core/hooks/pre_filter_farm_environments.py
Normal file
72
client/ayon_core/hooks/pre_filter_farm_environments.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
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
|
||||
"""
|
||||
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_env_profiles"])
|
||||
|
||||
if not filter_env_profiles:
|
||||
self.log.debug("No profiles found for env var filtering")
|
||||
return
|
||||
|
||||
task_entity = data["task_entity"]
|
||||
|
||||
filter_data = {
|
||||
"host_names": self.host_name,
|
||||
"task_types": task_entity["taskType"],
|
||||
"task_names": task_entity["name"],
|
||||
"folder_paths": 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
|
||||
|
||||
self._skip_environment_variables(
|
||||
self.launch_context.env, matching_profile)
|
||||
|
||||
self._modify_environment_variables(
|
||||
self.launch_context.env, matching_profile)
|
||||
|
||||
def _modify_environment_variables(self, calculated_env, matching_profile):
|
||||
"""Modify environment variable values."""
|
||||
for env_item in matching_profile["replace_in_environment"]:
|
||||
key = env_item["environment_key"]
|
||||
value = calculated_env.get(key)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
value = re.sub(value, env_item["pattern"], env_item["replacement"])
|
||||
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"""
|
||||
for skip_env in matching_profile["skip_env_keys"]:
|
||||
self.log.info(f"Skipping {skip_env}")
|
||||
calculated_env.pop(skip_env)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
AbstractAttrDef,
|
||||
|
|
@ -13,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
|
||||
|
|
@ -346,6 +344,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_allow_list: bool):
|
||||
self.product_types: List[str] = product_types
|
||||
self.is_allow_list: bool = is_allow_list
|
||||
|
||||
|
||||
class _BaseLoaderController(ABC):
|
||||
"""Base loader controller abstraction.
|
||||
|
||||
|
|
@ -1006,3 +1014,13 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_types_filter(self):
|
||||
"""Return product type filter for current context.
|
||||
|
||||
Returns:
|
||||
ProductTypesFilter: Product type filter for current context
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import uuid
|
|||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib import NestedCacheItem, CacheItem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import get_current_host_name
|
||||
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 +15,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,
|
||||
|
|
@ -331,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,
|
||||
|
|
@ -425,3 +431,59 @@ 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):
|
||||
output = ProductTypesFilter(
|
||||
is_allow_list=False,
|
||||
product_types=[]
|
||||
)
|
||||
# Without host is not determined context
|
||||
if self._host is None:
|
||||
return output
|
||||
|
||||
context = self.get_current_context()
|
||||
project_name = context.get("project_name")
|
||||
if not project_name:
|
||||
return output
|
||||
settings = get_project_settings(project_name)
|
||||
profiles = (
|
||||
settings
|
||||
["core"]
|
||||
["tools"]
|
||||
["loader"]
|
||||
["product_type_filter_profiles"]
|
||||
)
|
||||
if not profiles:
|
||||
return output
|
||||
|
||||
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_type,
|
||||
}
|
||||
)
|
||||
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=is_allow_list,
|
||||
product_types=profile["filter_product_types"]
|
||||
)
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,14 +64,26 @@ 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
|
||||
)
|
||||
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)
|
||||
|
|
@ -68,9 +92,13 @@ 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_on_refresh(self):
|
||||
self._reset_filters_on_refresh = True
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
checkstate_changed = False
|
||||
if role is None:
|
||||
|
|
@ -122,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()
|
||||
|
|
@ -151,6 +182,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
|
||||
|
|
@ -158,11 +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 _on_refresh_finished(self):
|
||||
# Apply product types filter on first show
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_filter_change(self):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -356,6 +356,39 @@ 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[Literal["information", "warning", "critical"]]): 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 make_sure_tray_is_running(
|
||||
ayon_url: Optional[str] = None,
|
||||
variant: Optional[str] = None,
|
||||
|
|
@ -412,6 +445,10 @@ def main(force=False):
|
|||
state = TrayState.NOT_RUNNING
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -133,6 +136,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."""
|
||||
|
|
@ -143,7 +147,10 @@ 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
|
||||
)
|
||||
self._addons_manager.add_route(
|
||||
"POST", "/tray/message", self._web_show_tray_message
|
||||
)
|
||||
|
||||
admin_submenu = ITrayAction.admin_submenu(tray_menu)
|
||||
|
|
@ -274,8 +281,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(),
|
||||
|
|
@ -285,7 +296,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:
|
||||
|
|
|
|||
|
|
@ -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 FilterEnvsProfileModel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
|
||||
host_names: 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"
|
||||
)
|
||||
|
||||
folder_paths: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Folder paths"
|
||||
)
|
||||
|
||||
skip_env_keys: 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_env_profiles: list[FilterEnvsProfileModel] = SettingsField(
|
||||
default_factory=list,
|
||||
)
|
||||
|
||||
@validator(
|
||||
"environments",
|
||||
|
|
@ -313,5 +356,6 @@ DEFAULT_VALUES = {
|
|||
"project_environments": json.dumps(
|
||||
{},
|
||||
indent=4
|
||||
)
|
||||
),
|
||||
"filter_env_profiles": [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ def _product_types_enum():
|
|||
"editorial",
|
||||
"gizmo",
|
||||
"image",
|
||||
"imagesequence",
|
||||
"layout",
|
||||
"look",
|
||||
"matchmove",
|
||||
|
|
@ -212,7 +213,6 @@ def _product_types_enum():
|
|||
"setdress",
|
||||
"take",
|
||||
"usd",
|
||||
"usdShade",
|
||||
"vdbcache",
|
||||
"vrayproxy",
|
||||
"workfile",
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
@ -499,14 +512,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": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue