Merge branch 'develop' into enhancement/874-publisher-editorial-linked-instances-with-grouping-view

This commit is contained in:
Jakub Trllo 2025-08-07 10:38:25 +02:00 committed by GitHub
commit 39a925986d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 247 additions and 210 deletions

View file

@ -35,6 +35,8 @@ body:
label: Version label: Version
description: What version are you running? Look to AYON Tray description: What version are you running? Look to AYON Tray
options: options:
- 1.5.2
- 1.5.1
- 1.5.0 - 1.5.0
- 1.4.1 - 1.4.1
- 1.4.0 - 1.4.0

View file

@ -8,6 +8,7 @@ import inspect
import logging import logging
import threading import threading
import collections import collections
import warnings
from uuid import uuid4 from uuid import uuid4
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional from typing import Optional
@ -815,10 +816,26 @@ class AddonsManager:
Unknown keys are logged out. Unknown keys are logged out.
Deprecated:
Use targeted methods 'collect_launcher_action_paths',
'collect_create_plugin_paths', 'collect_load_plugin_paths',
'collect_publish_plugin_paths' and
'collect_inventory_action_paths' to collect plugin paths.
Returns: Returns:
dict: Output is dictionary with keys "publish", "create", "load", dict: Output is dictionary with keys "publish", "create", "load",
"actions" and "inventory" each containing list of paths. "actions" and "inventory" each containing list of paths.
""" """
warnings.warn(
"Used deprecated method 'collect_plugin_paths'. Please use"
" targeted methods 'collect_launcher_action_paths',"
" 'collect_create_plugin_paths', 'collect_load_plugin_paths'"
" 'collect_publish_plugin_paths' and"
" 'collect_inventory_action_paths'",
DeprecationWarning,
stacklevel=2
)
# Output structure # Output structure
output = { output = {
"publish": [], "publish": [],
@ -874,24 +891,28 @@ class AddonsManager:
if not isinstance(addon, IPluginPaths): if not isinstance(addon, IPluginPaths):
continue continue
paths = None
method = getattr(addon, method_name) method = getattr(addon, method_name)
try: try:
paths = method(*args, **kwargs) paths = method(*args, **kwargs)
except Exception: except Exception:
self.log.warning( self.log.warning(
( "Failed to get plugin paths from addon"
"Failed to get plugin paths from addon" f" '{addon.name}' using '{method_name}'.",
" '{}' using '{}'."
).format(addon.__class__.__name__, method_name),
exc_info=True exc_info=True
) )
if not paths:
continue continue
if paths: if isinstance(paths, str):
# Convert to list if value is not list paths = [paths]
if not isinstance(paths, (list, tuple, set)): self.log.warning(
paths = [paths] f"Addon '{addon.name}' returned invalid output type"
output.extend(paths) f" from '{method_name}'."
f" Got 'str' expected 'list[str]'."
)
output.extend(paths)
return output return output
def collect_launcher_action_paths(self): def collect_launcher_action_paths(self):

View file

@ -1,6 +1,7 @@
"""Addon interfaces for AYON.""" """Addon interfaces for AYON."""
from __future__ import annotations from __future__ import annotations
import warnings
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Callable, Optional, Type from typing import TYPE_CHECKING, Callable, Optional, Type
@ -39,26 +40,29 @@ class AYONInterface(metaclass=_AYONInterfaceMeta):
class IPluginPaths(AYONInterface): class IPluginPaths(AYONInterface):
"""Addon has plugin paths to return. """Addon wants to register plugin paths."""
Expected result is dictionary with keys "publish", "create", "load",
"actions" or "inventory" and values as list or string.
{
"publish": ["path/to/publish_plugins"]
}
"""
@abstractmethod
def get_plugin_paths(self) -> dict[str, list[str]]: def get_plugin_paths(self) -> dict[str, list[str]]:
"""Return plugin paths for addon. """Return plugin paths for addon.
This method was abstract (required) in the past, so raise the required
'core' addon version when 'get_plugin_paths' is removed from
addon.
Deprecated:
Please implement specific methods 'get_create_plugin_paths',
'get_load_plugin_paths', 'get_inventory_action_paths' and
'get_publish_plugin_paths' to return plugin paths.
Returns: Returns:
dict[str, list[str]]: Plugin paths for addon. dict[str, list[str]]: Plugin paths for addon.
""" """
return {}
def _get_plugin_paths_by_type( def _get_plugin_paths_by_type(
self, plugin_type: str) -> list[str]: self, plugin_type: str
) -> list[str]:
"""Get plugin paths by type. """Get plugin paths by type.
Args: Args:
@ -78,6 +82,24 @@ class IPluginPaths(AYONInterface):
if not isinstance(paths, (list, tuple, set)): if not isinstance(paths, (list, tuple, set)):
paths = [paths] paths = [paths]
new_function_name = "get_launcher_action_paths"
if plugin_type == "create":
new_function_name = "get_create_plugin_paths"
elif plugin_type == "load":
new_function_name = "get_load_plugin_paths"
elif plugin_type == "publish":
new_function_name = "get_publish_plugin_paths"
elif plugin_type == "inventory":
new_function_name = "get_inventory_action_paths"
warnings.warn(
f"Addon '{self.name}' returns '{plugin_type}' paths using"
" 'get_plugin_paths' method. Please implement"
f" '{new_function_name}' instead.",
DeprecationWarning,
stacklevel=2
)
return paths return paths
def get_launcher_action_paths(self) -> list[str]: def get_launcher_action_paths(self) -> list[str]:

View file

@ -944,6 +944,8 @@ class IWorkfileHost:
self._emit_workfile_save_event(event_data, after_save=False) self._emit_workfile_save_event(event_data, after_save=False)
workdir = os.path.dirname(filepath) workdir = os.path.dirname(filepath)
if not os.path.exists(workdir):
os.makedirs(workdir, exist_ok=True)
# Set 'AYON_WORKDIR' environment variable # Set 'AYON_WORKDIR' environment variable
os.environ["AYON_WORKDIR"] = workdir os.environ["AYON_WORKDIR"] = workdir
@ -1072,10 +1074,13 @@ class IWorkfileHost:
prepared_data=prepared_data, prepared_data=prepared_data,
) )
workfile_entities_by_path = { workfile_entities_by_path = {}
workfile_entity["path"]: workfile_entity for workfile_entity in list_workfiles_context.workfile_entities:
for workfile_entity in list_workfiles_context.workfile_entities rootless_path = workfile_entity["path"]
} path = os.path.normpath(
list_workfiles_context.anatomy.fill_root(rootless_path)
)
workfile_entities_by_path[path] = workfile_entity
workdir_data = get_template_data( workdir_data = get_template_data(
list_workfiles_context.project_entity, list_workfiles_context.project_entity,
@ -1114,10 +1119,10 @@ class IWorkfileHost:
rootless_path = f"{rootless_workdir}/{filename}" rootless_path = f"{rootless_workdir}/{filename}"
workfile_entity = workfile_entities_by_path.pop( workfile_entity = workfile_entities_by_path.pop(
rootless_path, None filepath, None
) )
version = comment = None version = comment = None
if workfile_entity: if workfile_entity is not None:
_data = workfile_entity["data"] _data = workfile_entity["data"]
version = _data.get("version") version = _data.get("version")
comment = _data.get("comment") comment = _data.get("comment")
@ -1137,7 +1142,7 @@ class IWorkfileHost:
) )
items.append(item) items.append(item)
for workfile_entity in workfile_entities_by_path.values(): for filepath, workfile_entity in workfile_entities_by_path.items():
# Workfile entity is not in the filesystem # Workfile entity is not in the filesystem
# but it is in the database # but it is in the database
rootless_path = workfile_entity["path"] rootless_path = workfile_entity["path"]
@ -1154,13 +1159,13 @@ class IWorkfileHost:
version = parsed_data.version version = parsed_data.version
comment = parsed_data.comment comment = parsed_data.comment
filepath = list_workfiles_context.anatomy.fill_root(rootless_path) available = os.path.exists(filepath)
items.append(WorkfileInfo.new( items.append(WorkfileInfo.new(
filepath, filepath,
rootless_path, rootless_path,
version=version, version=version,
comment=comment, comment=comment,
available=False, available=available,
workfile_entity=workfile_entity, workfile_entity=workfile_entity,
)) ))

View file

@ -8,6 +8,7 @@ import warnings
from datetime import datetime from datetime import datetime
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from functools import lru_cache from functools import lru_cache
from typing import Optional, Any
import platformdirs import platformdirs
import ayon_api import ayon_api
@ -15,26 +16,31 @@ import ayon_api
_PLACEHOLDER = object() _PLACEHOLDER = object()
# TODO should use 'KeyError' or 'Exception' as base
class RegistryItemNotFound(ValueError):
"""Raised when the item is not found in the keyring."""
class _Cache: class _Cache:
username = None username = None
def _get_ayon_appdirs(*args): def _get_ayon_appdirs(*args: str) -> str:
return os.path.join( return os.path.join(
platformdirs.user_data_dir("AYON", "Ynput"), platformdirs.user_data_dir("AYON", "Ynput"),
*args *args
) )
def get_ayon_appdirs(*args): def get_ayon_appdirs(*args: str) -> str:
"""Local app data directory of AYON client. """Local app data directory of AYON client.
Deprecated: Deprecated:
Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on
use-case. Deprecation added 24/08/09 (0.4.4-dev.1). a use-case. Deprecation added 24/08/09 (0.4.4-dev.1).
Args: Args:
*args (Iterable[str]): Subdirectories/files in local app data dir. *args (Iterable[str]): Subdirectories/files in the local app data dir.
Returns: Returns:
str: Path to directory/file in local app data dir. str: Path to directory/file in local app data dir.
@ -52,7 +58,7 @@ def get_ayon_appdirs(*args):
def get_launcher_storage_dir(*subdirs: str) -> str: def get_launcher_storage_dir(*subdirs: str) -> str:
"""Get storage directory for launcher. """Get a storage directory for launcher.
Storage directory is used for storing shims, addons, dependencies, etc. Storage directory is used for storing shims, addons, dependencies, etc.
@ -77,14 +83,14 @@ def get_launcher_storage_dir(*subdirs: str) -> str:
def get_launcher_local_dir(*subdirs: str) -> str: def get_launcher_local_dir(*subdirs: str) -> str:
"""Get local directory for launcher. """Get a local directory for launcher.
Local directory is used for storing machine or user specific data. Local directory is used for storing machine or user-specific data.
The location is user specific. The location is user-specific.
Note: Note:
This function should be called at least once on bootstrap. This function should be called at least once on the bootstrap.
Args: Args:
*subdirs (str): Subdirectories relative to local dir. *subdirs (str): Subdirectories relative to local dir.
@ -101,7 +107,7 @@ def get_launcher_local_dir(*subdirs: str) -> str:
def get_addons_resources_dir(addon_name: str, *args) -> str: def get_addons_resources_dir(addon_name: str, *args) -> str:
"""Get directory for storing resources for addons. """Get a directory for storing resources for addons.
Some addons might need to store ad-hoc resources that are not part of Some addons might need to store ad-hoc resources that are not part of
addon client package (e.g. because of size). Studio might define addon client package (e.g. because of size). Studio might define
@ -111,7 +117,7 @@ def get_addons_resources_dir(addon_name: str, *args) -> str:
Args: Args:
addon_name (str): Addon name. addon_name (str): Addon name.
*args (str): Subfolders in resources directory. *args (str): Subfolders in the resources directory.
Returns: Returns:
str: Path to resources directory. str: Path to resources directory.
@ -124,6 +130,10 @@ def get_addons_resources_dir(addon_name: str, *args) -> str:
return os.path.join(addons_resources_dir, addon_name, *args) return os.path.join(addons_resources_dir, addon_name, *args)
class _FakeException(Exception):
"""Placeholder exception used if real exception is not available."""
class AYONSecureRegistry: class AYONSecureRegistry:
"""Store information using keyring. """Store information using keyring.
@ -134,9 +144,10 @@ class AYONSecureRegistry:
identify which data were created by AYON. identify which data were created by AYON.
Args: Args:
name(str): Name of registry used as identifier for data. name(str): Name of registry used as the identifier for data.
""" """
def __init__(self, name): def __init__(self, name: str) -> None:
try: try:
import keyring import keyring
@ -152,13 +163,12 @@ class AYONSecureRegistry:
keyring.set_keyring(Windows.WinVaultKeyring()) keyring.set_keyring(Windows.WinVaultKeyring())
# Force "AYON" prefix # Force "AYON" prefix
self._name = "/".join(("AYON", name)) self._name = f"AYON/{name}"
def set_item(self, name, value): def set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None """Set sensitive item into the system's keyring.
"""Set sensitive item into system's keyring.
This uses `Keyring module`_ to save sensitive stuff into system's This uses `Keyring module`_ to save sensitive stuff into the system's
keyring. keyring.
Args: Args:
@ -172,22 +182,26 @@ class AYONSecureRegistry:
import keyring import keyring
keyring.set_password(self._name, name, value) keyring.set_password(self._name, name, value)
self.get_item.cache_clear()
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def get_item(self, name, default=_PLACEHOLDER): def get_item(
"""Get value of sensitive item from system's keyring. self, name: str, default: Any = _PLACEHOLDER
) -> Optional[str]:
"""Get value of sensitive item from the system's keyring.
See also `Keyring module`_ See also `Keyring module`_
Args: Args:
name (str): Name of the item. name (str): Name of the item.
default (Any): Default value if item is not available. default (Any): Default value if the item is not available.
Returns: Returns:
value (str): Value of the item. value (str): Value of the item.
Raises: Raises:
ValueError: If item doesn't exist and default is not defined. RegistryItemNotFound: If the item doesn't exist and default
is not defined.
.. _Keyring module: .. _Keyring module:
https://github.com/jaraco/keyring https://github.com/jaraco/keyring
@ -195,21 +209,29 @@ class AYONSecureRegistry:
""" """
import keyring import keyring
value = keyring.get_password(self._name, name) # Capture 'ItemNotFoundException' exception (on linux)
try:
from secretstorage.exceptions import ItemNotFoundException
except ImportError:
ItemNotFoundException = _FakeException
try:
value = keyring.get_password(self._name, name)
except ItemNotFoundException:
value = None
if value is not None: if value is not None:
return value return value
if default is not _PLACEHOLDER: if default is not _PLACEHOLDER:
return default return default
# NOTE Should raise `KeyError` raise RegistryItemNotFound(
raise ValueError( f"Item {self._name}:{name} not found in keyring."
"Item {}:{} does not exist in keyring.".format(self._name, name)
) )
def delete_item(self, name): def delete_item(self, name: str) -> None:
# type: (str) -> None """Delete value stored in the system's keyring.
"""Delete value stored in system's keyring.
See also `Keyring module`_ See also `Keyring module`_
@ -227,47 +249,38 @@ class AYONSecureRegistry:
class ASettingRegistry(ABC): class ASettingRegistry(ABC):
"""Abstract class defining structure of **SettingRegistry** class. """Abstract class to defining structure of registry class.
It is implementing methods to store secure items into keyring, otherwise
mechanism for storing common items must be implemented in abstract
methods.
Attributes:
_name (str): Registry names.
""" """
def __init__(self, name: str) -> None:
def __init__(self, name):
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
self._name = name self._name = name
self._items = {}
def set_item(self, name, value):
# type: (str, str) -> None
"""Set item to settings registry.
Args:
name (str): Name of the item.
value (str): Value of the item.
"""
self._set_item(name, value)
@abstractmethod @abstractmethod
def _set_item(self, name, value): def _get_item(self, name: str) -> Any:
# type: (str, str) -> None """Get item value from registry."""
# Implement it
pass
def __setitem__(self, name, value): @abstractmethod
self._items[name] = value def _set_item(self, name: str, value: str) -> None:
"""Set item value to registry."""
@abstractmethod
def _delete_item(self, name: str) -> None:
"""Delete item from registry."""
def __getitem__(self, name: str) -> Any:
return self._get_item(name)
def __setitem__(self, name: str, value: str) -> None:
self._set_item(name, value) self._set_item(name, value)
def get_item(self, name): def __delitem__(self, name: str) -> None:
# type: (str) -> str self._delete_item(name)
@property
def name(self) -> str:
return self._name
def get_item(self, name: str) -> str:
"""Get item from settings registry. """Get item from settings registry.
Args: Args:
@ -277,22 +290,22 @@ class ASettingRegistry(ABC):
value (str): Value of the item. value (str): Value of the item.
Raises: Raises:
ValueError: If item doesn't exist. RegistryItemNotFound: If the item doesn't exist.
""" """
return self._get_item(name) return self._get_item(name)
@abstractmethod def set_item(self, name: str, value: str) -> None:
def _get_item(self, name): """Set item to settings registry.
# type: (str) -> str
# Implement it
pass
def __getitem__(self, name): Args:
return self._get_item(name) name (str): Name of the item.
value (str): Value of the item.
def delete_item(self, name): """
# type: (str) -> None self._set_item(name, value)
def delete_item(self, name: str) -> None:
"""Delete item from settings registry. """Delete item from settings registry.
Args: Args:
@ -301,16 +314,6 @@ class ASettingRegistry(ABC):
""" """
self._delete_item(name) self._delete_item(name)
@abstractmethod
def _delete_item(self, name):
# type: (str) -> None
"""Delete item from settings."""
pass
def __delitem__(self, name):
del self._items[name]
self._delete_item(name)
class IniSettingRegistry(ASettingRegistry): class IniSettingRegistry(ASettingRegistry):
"""Class using :mod:`configparser`. """Class using :mod:`configparser`.
@ -318,20 +321,17 @@ class IniSettingRegistry(ASettingRegistry):
This class is using :mod:`configparser` (ini) files to store items. This class is using :mod:`configparser` (ini) files to store items.
""" """
def __init__(self, name: str, path: str) -> None:
def __init__(self, name, path): super().__init__(name)
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
# get registry file # get registry file
self._registry_file = os.path.join(path, "{}.ini".format(name)) self._registry_file = os.path.join(path, f"{name}.ini")
if not os.path.exists(self._registry_file): if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg) print("# Settings registry", cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg) print(f"# {now}", cfg)
def set_item_section(self, section, name, value): def set_item_section(self, section: str, name: str, value: str) -> None:
# type: (str, str, str) -> None
"""Set item to specific section of ini registry. """Set item to specific section of ini registry.
If section doesn't exists, it is created. If section doesn't exists, it is created.
@ -354,12 +354,10 @@ class IniSettingRegistry(ASettingRegistry):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
config.write(cfg) config.write(cfg)
def _set_item(self, name, value): def _set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None
self.set_item_section("MAIN", name, value) self.set_item_section("MAIN", name, value)
def set_item(self, name, value): def set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None
"""Set item to settings ini file. """Set item to settings ini file.
This saves item to ``DEFAULT`` section of ini as each item there This saves item to ``DEFAULT`` section of ini as each item there
@ -372,10 +370,9 @@ class IniSettingRegistry(ASettingRegistry):
""" """
# this does the some, overridden just for different docstring. # this does the some, overridden just for different docstring.
# we cast value to str as ini options values must be strings. # we cast value to str as ini options values must be strings.
super(IniSettingRegistry, self).set_item(name, str(value)) super().set_item(name, str(value))
def get_item(self, name): def get_item(self, name: str) -> str:
# type: (str) -> str
"""Gets item from settings ini file. """Gets item from settings ini file.
This gets settings from ``DEFAULT`` section of ini file as each item This gets settings from ``DEFAULT`` section of ini file as each item
@ -388,19 +385,18 @@ class IniSettingRegistry(ASettingRegistry):
str: Value of item. str: Value of item.
Raises: Raises:
ValueError: If value doesn't exist. RegistryItemNotFound: If value doesn't exist.
""" """
return super(IniSettingRegistry, self).get_item(name) return super().get_item(name)
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def get_item_from_section(self, section, name): def get_item_from_section(self, section: str, name: str) -> str:
# type: (str, str) -> str
"""Get item from section of ini file. """Get item from section of ini file.
This will read ini file and try to get item value from specified This will read ini file and try to get item value from specified
section. If that section or item doesn't exist, :exc:`ValueError` section. If that section or item doesn't exist,
is risen. :exc:`RegistryItemNotFound` is risen.
Args: Args:
section (str): Name of ini section. section (str): Name of ini section.
@ -410,7 +406,7 @@ class IniSettingRegistry(ASettingRegistry):
str: Item value. str: Item value.
Raises: Raises:
ValueError: If value doesn't exist. RegistryItemNotFound: If value doesn't exist.
""" """
config = configparser.ConfigParser() config = configparser.ConfigParser()
@ -418,16 +414,15 @@ class IniSettingRegistry(ASettingRegistry):
try: try:
value = config[section][name] value = config[section][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}:{}".format(section, name)) f"Registry doesn't contain value {section}:{name}"
)
return value return value
def _get_item(self, name): def _get_item(self, name: str) -> str:
# type: (str) -> str
return self.get_item_from_section("MAIN", name) return self.get_item_from_section("MAIN", name)
def delete_item_from_section(self, section, name): def delete_item_from_section(self, section: str, name: str) -> None:
# type: (str, str) -> None
"""Delete item from section in ini file. """Delete item from section in ini file.
Args: Args:
@ -435,7 +430,7 @@ class IniSettingRegistry(ASettingRegistry):
name (str): Name of the item. name (str): Name of the item.
Raises: Raises:
ValueError: If item doesn't exist. RegistryItemNotFound: If the item doesn't exist.
""" """
self.get_item_from_section.cache_clear() self.get_item_from_section.cache_clear()
@ -444,8 +439,9 @@ class IniSettingRegistry(ASettingRegistry):
try: try:
_ = config[section][name] _ = config[section][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}:{}".format(section, name)) f"Registry doesn't contain value {section}:{name}"
)
config.remove_option(section, name) config.remove_option(section, name)
# if section is empty, delete it # if section is empty, delete it
@ -461,29 +457,28 @@ class IniSettingRegistry(ASettingRegistry):
class JSONSettingRegistry(ASettingRegistry): class JSONSettingRegistry(ASettingRegistry):
"""Class using json file as storage.""" """Class using a json file as storage."""
def __init__(self, name, path): def __init__(self, name: str, path: str) -> None:
# type: (str, str) -> JSONSettingRegistry super().__init__(name)
super(JSONSettingRegistry, self).__init__(name) self._registry_file = os.path.join(path, f"{name}.json")
#: str: name of registry file
self._registry_file = os.path.join(path, "{}.json".format(name))
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = { header = {
"__metadata__": {"generated": now}, "__metadata__": {"generated": now},
"registry": {} "registry": {}
} }
if not os.path.exists(os.path.dirname(self._registry_file)): # Use 'os.path.dirname' in case someone uses slashes in 'name'
os.makedirs(os.path.dirname(self._registry_file), exist_ok=True) dirpath = os.path.dirname(self._registry_file)
if not os.path.exists(dirpath):
os.makedirs(dirpath, exist_ok=True)
if not os.path.exists(self._registry_file): if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
json.dump(header, cfg, indent=4) json.dump(header, cfg, indent=4)
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def _get_item(self, name): def _get_item(self, name: str) -> str:
# type: (str) -> object """Get item value from the registry.
"""Get item value from registry json.
Note: Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.get_item` See :meth:`ayon_core.lib.JSONSettingRegistry.get_item`
@ -494,29 +489,13 @@ class JSONSettingRegistry(ASettingRegistry):
try: try:
value = data["registry"][name] value = data["registry"][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}".format(name)) f"Registry doesn't contain value {name}"
)
return value return value
def get_item(self, name): def _set_item(self, name: str, value: str) -> None:
# type: (str) -> object """Set item value to the registry.
"""Get item value from registry json.
Args:
name (str): Name of the item.
Returns:
value of the item
Raises:
ValueError: If item is not found in registry file.
"""
return self._get_item(name)
def _set_item(self, name, value):
# type: (str, object) -> None
"""Set item value to registry json.
Note: Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.set_item` See :meth:`ayon_core.lib.JSONSettingRegistry.set_item`
@ -528,41 +507,39 @@ class JSONSettingRegistry(ASettingRegistry):
cfg.truncate(0) cfg.truncate(0)
cfg.seek(0) cfg.seek(0)
json.dump(data, cfg, indent=4) json.dump(data, cfg, indent=4)
def set_item(self, name, value):
# type: (str, object) -> None
"""Set item and its value into json registry file.
Args:
name (str): name of the item.
value (Any): value of the item.
"""
self._set_item(name, value)
def _delete_item(self, name):
# type: (str) -> None
self._get_item.cache_clear() self._get_item.cache_clear()
def _delete_item(self, name: str) -> None:
with open(self._registry_file, "r+") as cfg: with open(self._registry_file, "r+") as cfg:
data = json.load(cfg) data = json.load(cfg)
del data["registry"][name] del data["registry"][name]
cfg.truncate(0) cfg.truncate(0)
cfg.seek(0) cfg.seek(0)
json.dump(data, cfg, indent=4) json.dump(data, cfg, indent=4)
self._get_item.cache_clear()
class AYONSettingsRegistry(JSONSettingRegistry): class AYONSettingsRegistry(JSONSettingRegistry):
"""Class handling AYON general settings registry. """Class handling AYON general settings registry.
Args: Args:
name (Optional[str]): Name of the registry. name (Optional[str]): Name of the registry. Using 'None' or not
""" passing name is deprecated.
def __init__(self, name=None): """
def __init__(self, name: Optional[str] = None) -> None:
if not name: if not name:
name = "AYON_settings" name = "AYON_settings"
warnings.warn(
(
"Used 'AYONSettingsRegistry' without 'name' argument."
" The argument will be required in future versions."
),
DeprecationWarning,
stacklevel=2,
)
path = get_launcher_storage_dir() path = get_launcher_storage_dir()
super(AYONSettingsRegistry, self).__init__(name, path) super().__init__(name, path)
def get_local_site_id(): def get_local_site_id():

View file

@ -1403,7 +1403,12 @@ def _get_display_view_colorspace_name(config_path, display, view):
""" """
config = _get_ocio_config(config_path) config = _get_ocio_config(config_path)
return config.getDisplayViewColorSpaceName(display, view) colorspace = config.getDisplayViewColorSpaceName(display, view)
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
if colorspace == "<USE_DISPLAY_NAME>":
colorspace = display
return colorspace
def _get_ocio_config_colorspaces(config_path): def _get_ocio_config_colorspaces(config_path):

View file

@ -39,7 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
"blender", "blender",
"houdini", "houdini",
"max", "max",
"circuit", "batchdelivery",
] ]
settings_category = "core" settings_category = "core"

View file

@ -8,13 +8,7 @@ This module contains a unified plugin that handles:
from pprint import pformat from pprint import pformat
import opentimelineio as otio
import pyblish.api import pyblish.api
from ayon_core.pipeline.editorial import (
get_media_range_with_retimes,
otio_range_to_frame_range,
otio_range_with_handles,
)
def validate_otio_clip(instance, logger): def validate_otio_clip(instance, logger):
@ -74,6 +68,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
if not validate_otio_clip(instance, self.log): if not validate_otio_clip(instance, self.log):
return return
import opentimelineio as otio
otio_clip = instance.data["otioClip"] otio_clip = instance.data["otioClip"]
# Collect timeline ranges if workfile start frame is available # Collect timeline ranges if workfile start frame is available
@ -100,6 +96,11 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
def _collect_timeline_ranges(self, instance, otio_clip): def _collect_timeline_ranges(self, instance, otio_clip):
"""Collect basic timeline frame ranges.""" """Collect basic timeline frame ranges."""
from ayon_core.pipeline.editorial import (
otio_range_to_frame_range,
otio_range_with_handles,
)
workfile_start = instance.data["workfileFrameStart"] workfile_start = instance.data["workfileFrameStart"]
# Get timeline ranges # Get timeline ranges
@ -129,6 +130,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
def _collect_source_ranges(self, instance, otio_clip): def _collect_source_ranges(self, instance, otio_clip):
"""Collect source media frame ranges.""" """Collect source media frame ranges."""
import opentimelineio as otio
# Get source ranges # Get source ranges
otio_src_range = otio_clip.source_range otio_src_range = otio_clip.source_range
otio_available_range = otio_clip.available_range() otio_available_range = otio_clip.available_range()
@ -178,6 +181,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
def _collect_retimed_ranges(self, instance, otio_clip): def _collect_retimed_ranges(self, instance, otio_clip):
"""Handle retimed clip frame ranges.""" """Handle retimed clip frame ranges."""
from ayon_core.pipeline.editorial import get_media_range_with_retimes
retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0) retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0)
self.log.debug(f"Retimed attributes: {retimed_attributes}") self.log.debug(f"Retimed attributes: {retimed_attributes}")

View file

@ -55,7 +55,7 @@ class ExtractBurnin(publish.Extractor):
"max", "max",
"blender", "blender",
"unreal", "unreal",
"circuit", "batchdelivery",
] ]
settings_category = "core" settings_category = "core"

View file

@ -7,7 +7,6 @@ from ayon_core.lib import (
get_ffmpeg_tool_args, get_ffmpeg_tool_args,
run_subprocess run_subprocess
) )
from ayon_core.pipeline import editorial
class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
@ -159,6 +158,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
""" """
# Not all hosts can import this module. # Not all hosts can import this module.
import opentimelineio as otio import opentimelineio as otio
from ayon_core.pipeline.editorial import OTIO_EPSILON
output = [] output = []
# go trough all audio tracks # go trough all audio tracks
@ -177,7 +177,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# Avoid rounding issue on media available range. # Avoid rounding issue on media available range.
if clip_start.almost_equal( if clip_start.almost_equal(
conformed_av_start, conformed_av_start,
editorial.OTIO_EPSILON OTIO_EPSILON
): ):
conformed_av_start = clip_start conformed_av_start = clip_start

View file

@ -25,7 +25,6 @@ from ayon_core.lib import (
) )
from ayon_core.pipeline import ( from ayon_core.pipeline import (
KnownPublishError, KnownPublishError,
editorial,
publish, publish,
) )
@ -359,6 +358,7 @@ class ExtractOTIOReview(
import opentimelineio as otio import opentimelineio as otio
from ayon_core.pipeline.editorial import ( from ayon_core.pipeline.editorial import (
trim_media_range, trim_media_range,
OTIO_EPSILON,
) )
def _round_to_frame(rational_time): def _round_to_frame(rational_time):
@ -380,7 +380,7 @@ class ExtractOTIOReview(
# Avoid rounding issue on media available range. # Avoid rounding issue on media available range.
if start.almost_equal( if start.almost_equal(
avl_start, avl_start,
editorial.OTIO_EPSILON OTIO_EPSILON
): ):
avl_start = start avl_start = start
@ -406,7 +406,7 @@ class ExtractOTIOReview(
# Avoid rounding issue on media available range. # Avoid rounding issue on media available range.
if end_point.almost_equal( if end_point.almost_equal(
avl_end_point, avl_end_point,
editorial.OTIO_EPSILON OTIO_EPSILON
): ):
avl_end_point = end_point avl_end_point = end_point

View file

@ -161,7 +161,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
"aftereffects", "aftereffects",
"flame", "flame",
"unreal", "unreal",
"circuit", "batchdelivery",
"photoshop" "photoshop"
] ]

View file

@ -41,7 +41,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"photoshop", "photoshop",
"unreal", "unreal",
"houdini", "houdini",
"circuit", "batchdelivery",
] ]
settings_category = "core" settings_category = "core"
enabled = False enabled = False

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version.""" """Package declaring AYON addon 'core' version."""
__version__ = "1.5.0+dev" __version__ = "1.5.2+dev"

View file

@ -1,6 +1,6 @@
name = "core" name = "core"
title = "Core" title = "Core"
version = "1.5.0+dev" version = "1.5.2+dev"
client_dir = "ayon_core" client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry] [tool.poetry]
name = "ayon-core" name = "ayon-core"
version = "1.5.0+dev" version = "1.5.2+dev"
description = "" description = ""
authors = ["Ynput Team <team@ynput.io>"] authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md" readme = "README.md"