Merge branch 'develop' of https://github.com/ynput/ayon-core into bugfix/AY-3059_Deadline-Allowed-env-keys

This commit is contained in:
Petr Kalis 2024-05-02 11:51:11 +02:00
commit 1bced2e13a
17 changed files with 387 additions and 325 deletions

View file

@ -2,7 +2,7 @@ import os
import json
from ayon_core.pipeline import publish
from ayon_core.hosts.maya.api.lib import extract_alembic
from ayon_core.hosts.maya.api.alembic import extract_alembic
from maya import cmds

View file

@ -3,8 +3,8 @@ import os
from maya import cmds
from ayon_core.pipeline import publish
from ayon_core.hosts.maya.api.alembic import extract_alembic
from ayon_core.hosts.maya.api.lib import (
extract_alembic,
suspended_refresh,
maintained_selection,
iter_visible_nodes_in_range

View file

@ -5,8 +5,8 @@ import os
from maya import cmds # noqa
from ayon_core.pipeline import publish
from ayon_core.hosts.maya.api.alembic import extract_alembic
from ayon_core.hosts.maya.api.lib import (
extract_alembic,
suspended_refresh,
maintained_selection
)

View file

@ -260,11 +260,11 @@ class UEProjectGenerationWorker(UEWorker):
self.failed.emit(msg, return_code)
raise RuntimeError(msg)
# ensure we have PySide2 installed in engine
# ensure we have PySide2/6 installed in engine
self.progress.emit(0)
self.stage_begin.emit(
(f"Checking PySide2 installation... {stage_count} "
(f"Checking Qt bindings installation... {stage_count} "
f" out of {stage_count}"))
python_path = None
if platform.system().lower() == "windows":
@ -287,11 +287,30 @@ class UEProjectGenerationWorker(UEWorker):
msg = f"Unreal Python not found at {python_path}"
self.failed.emit(msg, 1)
raise RuntimeError(msg)
pyside_cmd = [python_path.as_posix(),
"-m",
"pip",
"install",
"pyside2"]
pyside_version = "PySide2"
ue_version = self.ue_version.split(".")
if int(ue_version[0]) == 5 and int(ue_version[1]) >= 4:
# Use PySide6 6.6.3 because 6.7.0 had a bug
# - 'QPushButton' can't be added to 'QBoxLayout'
pyside_version = "PySide6==6.6.3"
site_packages_prefix = python_path.parent.as_posix()
pyside_cmd = [
python_path.as_posix(),
"-m", "pip",
"install",
"--ignore-installed",
pyside_version,
]
if platform.system().lower() == "windows":
pyside_cmd += ["--target", site_packages_prefix]
print(f"--- Installing {pyside_version} ...")
print(" ".join(pyside_cmd))
pyside_install = subprocess.Popen(pyside_cmd,
stdout=subprocess.PIPE,
@ -306,8 +325,8 @@ class UEProjectGenerationWorker(UEWorker):
return_code = pyside_install.wait()
if return_code and return_code != 0:
msg = ("Failed to create the project! "
"The installation of PySide2 has failed!")
msg = (f"Failed to create the project! {return_code} "
f"The installation of {pyside_version} has failed!: {pyside_install}")
self.failed.emit(msg, return_code)
raise RuntimeError(msg)

View file

@ -27,6 +27,10 @@ from .local_settings import (
get_openpype_username,
)
from .ayon_connection import initialize_ayon_connection
from .cache import (
CacheItem,
NestedCacheItem,
)
from .events import (
emit_event,
register_event_callback
@ -157,6 +161,9 @@ __all__ = [
"initialize_ayon_connection",
"CacheItem",
"NestedCacheItem",
"emit_event",
"register_event_callback",

View file

@ -0,0 +1,250 @@
import time
import collections
InitInfo = collections.namedtuple(
"InitInfo",
["default_factory", "lifetime"]
)
def _default_factory_func():
return None
class CacheItem:
"""Simple cache item with lifetime and default factory for default value.
Default factory should return default value that is used on init
and on reset.
Args:
default_factory (Optional[callable]): Function that returns default
value used on init and on reset.
lifetime (Optional[int]): Lifetime of the cache data in seconds.
Default lifetime is 120 seconds.
"""
def __init__(self, default_factory=None, lifetime=None):
if lifetime is None:
lifetime = 120
self._lifetime = lifetime
self._last_update = None
if default_factory is None:
default_factory = _default_factory_func
self._default_factory = default_factory
self._data = default_factory()
@property
def is_valid(self):
"""Is cache valid to use.
Return:
bool: True if cache is valid, False otherwise.
"""
if self._last_update is None:
return False
return (time.time() - self._last_update) < self._lifetime
def set_lifetime(self, lifetime):
"""Change lifetime of cache item.
Args:
lifetime (int): Lifetime of the cache data in seconds.
"""
self._lifetime = lifetime
def set_invalid(self):
"""Set cache as invalid."""
self._last_update = None
def reset(self):
"""Set cache as invalid and reset data."""
self._last_update = None
self._data = self._default_factory()
def get_data(self):
"""Receive cached data.
Returns:
Any: Any data that are cached.
"""
return self._data
def update_data(self, data):
"""Update cache data.
Args:
data (Any): Any data that are cached.
"""
self._data = data
self._last_update = time.time()
class NestedCacheItem:
"""Helper for cached items stored in nested structure.
Example:
>>> cache = NestedCacheItem(levels=2, default_factory=lambda: 0)
>>> cache["a"]["b"].is_valid
False
>>> cache["a"]["b"].get_data()
0
>>> cache["a"]["b"] = 1
>>> cache["a"]["b"].is_valid
True
>>> cache["a"]["b"].get_data()
1
>>> cache.reset()
>>> cache["a"]["b"].is_valid
False
Args:
levels (int): Number of nested levels where read cache is stored.
default_factory (Optional[callable]): Function that returns default
value used on init and on reset.
lifetime (Optional[int]): Lifetime of the cache data in seconds.
Default value is based on default value of 'CacheItem'.
_init_info (Optional[InitInfo]): Private argument. Init info for
nested cache where created from parent item.
"""
def __init__(
self, levels=1, default_factory=None, lifetime=None, _init_info=None
):
if levels < 1:
raise ValueError("Nested levels must be greater than 0")
self._data_by_key = {}
if _init_info is None:
_init_info = InitInfo(default_factory, lifetime)
self._init_info = _init_info
self._levels = levels
def __getitem__(self, key):
"""Get cached data.
Args:
key (str): Key of the cache item.
Returns:
Union[NestedCacheItem, CacheItem]: Cache item.
"""
cache = self._data_by_key.get(key)
if cache is None:
if self._levels > 1:
cache = NestedCacheItem(
levels=self._levels - 1,
_init_info=self._init_info
)
else:
cache = CacheItem(
self._init_info.default_factory,
self._init_info.lifetime
)
self._data_by_key[key] = cache
return cache
def __setitem__(self, key, value):
"""Update cached data.
Args:
key (str): Key of the cache item.
value (Any): Any data that are cached.
"""
if self._levels > 1:
raise AttributeError((
"{} does not support '__setitem__'. Lower nested level by {}"
).format(self.__class__.__name__, self._levels - 1))
cache = self[key]
cache.update_data(value)
def get(self, key):
"""Get cached data.
Args:
key (str): Key of the cache item.
Returns:
Union[NestedCacheItem, CacheItem]: Cache item.
"""
return self[key]
def cached_count(self):
"""Amount of cached items.
Returns:
int: Amount of cached items.
"""
return len(self._data_by_key)
def clear_key(self, key):
"""Clear cached item by key.
Args:
key (str): Key of the cache item.
"""
self._data_by_key.pop(key, None)
def clear_invalid(self):
"""Clear all invalid cache items.
Note:
To clear all cache items use 'reset'.
"""
changed = {}
children_are_nested = self._levels > 1
for key, cache in tuple(self._data_by_key.items()):
if children_are_nested:
output = cache.clear_invalid()
if output:
changed[key] = output
if not cache.cached_count():
self._data_by_key.pop(key)
elif not cache.is_valid:
changed[key] = cache.get_data()
self._data_by_key.pop(key)
return changed
def reset(self):
"""Reset cache.
Note:
To clear only invalid cache items use 'clear_invalid'.
"""
self._data_by_key = {}
def set_lifetime(self, lifetime):
"""Change lifetime of all children cache items.
Args:
lifetime (int): Lifetime of the cache data in seconds.
"""
self._init_info.lifetime = lifetime
for cache in self._data_by_key.values():
cache.set_lifetime(lifetime)
@property
def is_valid(self):
"""Raise reasonable error when called on wrong level.
Raises:
AttributeError: If called on nested cache item.
"""
raise AttributeError((
"{} does not support 'is_valid'. Lower nested level by '{}'"
).format(self.__class__.__name__, self._levels))

View file

@ -3,11 +3,16 @@ import re
import copy
import platform
import collections
import time
import ayon_api
from ayon_core.lib import Logger, get_local_site_id, StringTemplate
from ayon_core.lib import (
Logger,
get_local_site_id,
StringTemplate,
CacheItem,
NestedCacheItem,
)
from ayon_core.addon import AddonsManager
from .exceptions import RootCombinationError, ProjectNotSet
@ -397,62 +402,11 @@ class BaseAnatomy(object):
)
class CacheItem:
"""Helper to cache data.
Helper does not handle refresh of data and does not mark data as outdated.
Who uses the object should check of outdated state on his own will.
"""
default_lifetime = 10
def __init__(self, lifetime=None):
self._data = None
self._cached = None
self._lifetime = lifetime or self.default_lifetime
@property
def data(self):
"""Cached data/object.
Returns:
Any: Whatever was cached.
"""
return self._data
@property
def is_outdated(self):
"""Item has outdated cache.
Lifetime of cache item expired or was not yet set.
Returns:
bool: Item is outdated.
"""
if self._cached is None:
return True
return (time.time() - self._cached) > self._lifetime
def update_data(self, data):
"""Update cache of data.
Args:
data (Any): Data to cache.
"""
self._data = data
self._cached = time.time()
class Anatomy(BaseAnatomy):
_sitesync_addon_cache = CacheItem()
_project_cache = collections.defaultdict(CacheItem)
_default_site_id_cache = collections.defaultdict(CacheItem)
_root_overrides_cache = collections.defaultdict(
lambda: collections.defaultdict(CacheItem)
)
_project_cache = NestedCacheItem(lifetime=10)
_sitesync_addon_cache = CacheItem(lifetime=60)
_default_site_id_cache = NestedCacheItem(lifetime=60)
_root_overrides_cache = NestedCacheItem(2, lifetime=60)
def __init__(
self, project_name=None, site_name=None, project_entity=None
@ -477,18 +431,18 @@ class Anatomy(BaseAnatomy):
@classmethod
def get_project_entity_from_cache(cls, project_name):
project_cache = cls._project_cache[project_name]
if project_cache.is_outdated:
if not project_cache.is_valid:
project_cache.update_data(ayon_api.get_project(project_name))
return copy.deepcopy(project_cache.data)
return copy.deepcopy(project_cache.get_data())
@classmethod
def get_sitesync_addon(cls):
if cls._sitesync_addon_cache.is_outdated:
if not cls._sitesync_addon_cache.is_valid:
manager = AddonsManager()
cls._sitesync_addon_cache.update_data(
manager.get_enabled_addon("sitesync")
)
return cls._sitesync_addon_cache.data
return cls._sitesync_addon_cache.get_data()
@classmethod
def _get_studio_roots_overrides(cls, project_name):
@ -533,14 +487,14 @@ class Anatomy(BaseAnatomy):
elif not site_name:
# Use sync server to receive active site name
project_cache = cls._default_site_id_cache[project_name]
if project_cache.is_outdated:
if not project_cache.is_valid:
project_cache.update_data(
sitesync_addon.get_active_site_type(project_name)
)
site_name = project_cache.data
site_name = project_cache.get_data()
site_cache = cls._root_overrides_cache[project_name][site_name]
if site_cache.is_outdated:
if not site_cache.is_valid:
if site_name == "studio":
# Handle studio root overrides without sync server
# - studio root overrides can be done even without sync server
@ -553,4 +507,4 @@ class Anatomy(BaseAnatomy):
project_name, site_name
)
site_cache.update_data(roots_overrides)
return site_cache.data
return site_cache.get_data()

View file

@ -1,239 +1,31 @@
import time
import collections
import warnings
InitInfo = collections.namedtuple(
"InitInfo",
["default_factory", "lifetime"]
from ayon_core.lib import CacheItem as _CacheItem
from ayon_core.lib import NestedCacheItem as _NestedCacheItem
# Cache classes were moved to `ayon_core.lib.cache`
class CacheItem(_CacheItem):
def __init__(self, *args, **kwargs):
warnings.warn(
"Used 'CacheItem' from deprecated location "
"'ayon_core.tools.common_models', use 'ayon_core.lib' instead.",
DeprecationWarning,
)
super().__init__(*args, **kwargs)
class NestedCacheItem(_NestedCacheItem):
def __init__(self, *args, **kwargs):
warnings.warn(
"Used 'NestedCacheItem' from deprecated location "
"'ayon_core.tools.common_models', use 'ayon_core.lib' instead.",
DeprecationWarning,
)
super().__init__(*args, **kwargs)
__all__ = (
"CacheItem",
"NestedCacheItem",
)
def _default_factory_func():
return None
class CacheItem:
"""Simple cache item with lifetime and default value.
Args:
default_factory (Optional[callable]): Function that returns default
value used on init and on reset.
lifetime (Optional[int]): Lifetime of the cache data in seconds.
"""
def __init__(self, default_factory=None, lifetime=None):
if lifetime is None:
lifetime = 120
self._lifetime = lifetime
self._last_update = None
if default_factory is None:
default_factory = _default_factory_func
self._default_factory = default_factory
self._data = default_factory()
@property
def is_valid(self):
"""Is cache valid to use.
Return:
bool: True if cache is valid, False otherwise.
"""
if self._last_update is None:
return False
return (time.time() - self._last_update) < self._lifetime
def set_lifetime(self, lifetime):
"""Change lifetime of cache item.
Args:
lifetime (int): Lifetime of the cache data in seconds.
"""
self._lifetime = lifetime
def set_invalid(self):
"""Set cache as invalid."""
self._last_update = None
def reset(self):
"""Set cache as invalid and reset data."""
self._last_update = None
self._data = self._default_factory()
def get_data(self):
"""Receive cached data.
Returns:
Any: Any data that are cached.
"""
return self._data
def update_data(self, data):
self._data = data
self._last_update = time.time()
class NestedCacheItem:
"""Helper for cached items stored in nested structure.
Example:
>>> cache = NestedCacheItem(levels=2, default_factory=lambda: 0)
>>> cache["a"]["b"].is_valid
False
>>> cache["a"]["b"].get_data()
0
>>> cache["a"]["b"] = 1
>>> cache["a"]["b"].is_valid
True
>>> cache["a"]["b"].get_data()
1
>>> cache.reset()
>>> cache["a"]["b"].is_valid
False
Args:
levels (int): Number of nested levels where read cache is stored.
default_factory (Optional[callable]): Function that returns default
value used on init and on reset.
lifetime (Optional[int]): Lifetime of the cache data in seconds.
_init_info (Optional[InitInfo]): Private argument. Init info for
nested cache where created from parent item.
"""
def __init__(
self, levels=1, default_factory=None, lifetime=None, _init_info=None
):
if levels < 1:
raise ValueError("Nested levels must be greater than 0")
self._data_by_key = {}
if _init_info is None:
_init_info = InitInfo(default_factory, lifetime)
self._init_info = _init_info
self._levels = levels
def __getitem__(self, key):
"""Get cached data.
Args:
key (str): Key of the cache item.
Returns:
Union[NestedCacheItem, CacheItem]: Cache item.
"""
cache = self._data_by_key.get(key)
if cache is None:
if self._levels > 1:
cache = NestedCacheItem(
levels=self._levels - 1,
_init_info=self._init_info
)
else:
cache = CacheItem(
self._init_info.default_factory,
self._init_info.lifetime
)
self._data_by_key[key] = cache
return cache
def __setitem__(self, key, value):
"""Update cached data.
Args:
key (str): Key of the cache item.
value (Any): Any data that are cached.
"""
if self._levels > 1:
raise AttributeError((
"{} does not support '__setitem__'. Lower nested level by {}"
).format(self.__class__.__name__, self._levels - 1))
cache = self[key]
cache.update_data(value)
def get(self, key):
"""Get cached data.
Args:
key (str): Key of the cache item.
Returns:
Union[NestedCacheItem, CacheItem]: Cache item.
"""
return self[key]
def cached_count(self):
"""Amount of cached items.
Returns:
int: Amount of cached items.
"""
return len(self._data_by_key)
def clear_key(self, key):
"""Clear cached item by key.
Args:
key (str): Key of the cache item.
"""
self._data_by_key.pop(key, None)
def clear_invalid(self):
"""Clear all invalid cache items.
Note:
To clear all cache items use 'reset'.
"""
changed = {}
children_are_nested = self._levels > 1
for key, cache in tuple(self._data_by_key.items()):
if children_are_nested:
output = cache.clear_invalid()
if output:
changed[key] = output
if not cache.cached_count():
self._data_by_key.pop(key)
elif not cache.is_valid:
changed[key] = cache.get_data()
self._data_by_key.pop(key)
return changed
def reset(self):
"""Reset cache.
Note:
To clear only invalid cache items use 'clear_invalid'.
"""
self._data_by_key = {}
def set_lifetime(self, lifetime):
"""Change lifetime of all children cache items.
Args:
lifetime (int): Lifetime of the cache data in seconds.
"""
self._init_info.lifetime = lifetime
for cache in self._data_by_key.values():
cache.set_lifetime(lifetime)
@property
def is_valid(self):
"""Raise reasonable error when called on wront level.
Raises:
AttributeError: If called on nested cache item.
"""
raise AttributeError((
"{} does not support 'is_valid'. Lower nested level by '{}'"
).format(self.__class__.__name__, self._levels))

View file

@ -6,8 +6,7 @@ import ayon_api
import six
from ayon_core.style import get_default_entity_icon_color
from .cache import NestedCacheItem
from ayon_core.lib import NestedCacheItem
HIERARCHY_MODEL_SENDER = "hierarchy.model"

View file

@ -5,8 +5,7 @@ import ayon_api
import six
from ayon_core.style import get_default_entity_icon_color
from .cache import CacheItem
from ayon_core.lib import CacheItem
PROJECTS_MODEL_SENDER = "projects.model"

View file

@ -5,7 +5,7 @@ import collections
import ayon_api
import appdirs
from .cache import NestedCacheItem
from ayon_core.lib import NestedCacheItem
FileInfo = collections.namedtuple(
"FileInfo",

View file

@ -6,6 +6,7 @@ import uuid
import ayon_api
from ayon_core.lib import NestedCacheItem
from ayon_core.pipeline.load import (
discover_loader_plugins,
ProductLoaderPlugin,
@ -17,7 +18,6 @@ from ayon_core.pipeline.load import (
LoadError,
IncompatibleLoaderError,
)
from ayon_core.tools.common_models import NestedCacheItem
from ayon_core.tools.loader.abstract import ActionItem
ACTIONS_MODEL_SENDER = "actions.model"

View file

@ -5,8 +5,8 @@ import arrow
import ayon_api
from ayon_api.operations import OperationsSession
from ayon_core.lib import NestedCacheItem
from ayon_core.style import get_default_entity_icon_color
from ayon_core.tools.common_models import NestedCacheItem
from ayon_core.tools.loader.abstract import (
ProductTypeItem,
ProductItem,

View file

@ -2,9 +2,8 @@ import collections
from ayon_api import get_representations, get_versions_links
from ayon_core.lib import Logger
from ayon_core.lib import Logger, NestedCacheItem
from ayon_core.addon import AddonsManager
from ayon_core.tools.common_models import NestedCacheItem
from ayon_core.tools.loader.abstract import ActionItem
DOWNLOAD_IDENTIFIER = "sitesync.download"

View file

@ -106,7 +106,7 @@ line-ending = "auto"
[tool.codespell]
# Ignore words that are not in the dictionary.
ignore-words-list = "ayon,ynput,parms,parm,hda,developpement"
ignore-words-list = "ayon,ynput,parms,parm,hda,developpement,ue"
skip = "./.*,./package/*,*/vendor/*,*/unreal/integration/*,*/aftereffects/api/extension/js/libs/*"
count = true

View file

@ -110,6 +110,26 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
]
}
def launch_application(
self, app_name, project_name, folder_path, task_name
):
"""Launch application.
Args:
app_name (str): Full application name e.g. 'maya/2024'.
project_name (str): Project name.
folder_path (str): Folder path.
task_name (str): Task name.
"""
app_manager = self.get_applications_manager()
return app_manager.launch(
app_name,
project_name=project_name,
folder_path=folder_path,
task_name=task_name,
)
# --- CLI ---
def cli(self, addon_click_group):
main_group = click_wrap.group(
@ -134,6 +154,17 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
default=None
)
)
(
main_group.command(
self._cli_launch_applications,
name="launch",
help="Launch application"
)
.option("--app", required=True, help="Application name")
.option("--project", required=True, help="Project name")
.option("--folder", required=True, help="Folder path")
.option("--task", required=True, help="Task name")
)
# Convert main command to click object and add it to parent group
addon_click_group.add_command(
main_group.to_click_obj()
@ -171,3 +202,15 @@ class ApplicationsAddon(AYONAddon, IPluginPaths):
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)
def _cli_launch_applications(self, project, folder, task, app):
"""Launch application.
Args:
project (str): Project name.
folder (str): Folder path.
task (str): Task name.
app (str): Full application name e.g. 'maya/2024'.
"""
self.launch_application(app, project, folder, task)

View file

@ -1,6 +1,6 @@
name = "applications"
title = "Applications"
version = "0.2.0"
version = "0.2.1"
ayon_server_version = ">=1.0.7"
ayon_launcher_version = ">=1.0.2"