Merge branch 'develop' into enhancement/AY-8004_template-build-using-linked-folders

This commit is contained in:
Ondřej Samohel 2025-07-31 15:58:06 +02:00 committed by GitHub
commit e6092f4c2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 172 additions and 166 deletions

View file

@ -33,6 +33,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"cinema4d",
"silhouette",
"gaffer",
"loki",
}
launch_types = {LaunchTypes.local}

View file

@ -24,6 +24,7 @@ class OCIOEnvHook(PreLaunchHook):
"cinema4d",
"silhouette",
"gaffer",
"loki",
}
launch_types = set()

View file

@ -8,6 +8,7 @@ import warnings
from datetime import datetime
from abc import ABC, abstractmethod
from functools import lru_cache
from typing import Optional, Any
import platformdirs
import ayon_api
@ -15,26 +16,31 @@ import ayon_api
_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:
username = None
def _get_ayon_appdirs(*args):
def _get_ayon_appdirs(*args: str) -> str:
return os.path.join(
platformdirs.user_data_dir("AYON", "Ynput"),
*args
)
def get_ayon_appdirs(*args):
def get_ayon_appdirs(*args: str) -> str:
"""Local app data directory of AYON client.
Deprecated:
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 (Iterable[str]): Subdirectories/files in local app data dir.
*args (Iterable[str]): Subdirectories/files in the local app data dir.
Returns:
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:
"""Get storage directory for launcher.
"""Get a storage directory for launcher.
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:
"""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:
This function should be called at least once on bootstrap.
This function should be called at least once on the bootstrap.
Args:
*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:
"""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
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:
addon_name (str): Addon name.
*args (str): Subfolders in resources directory.
*args (str): Subfolders in the resources directory.
Returns:
str: Path to resources directory.
@ -134,9 +140,10 @@ class AYONSecureRegistry:
identify which data were created by AYON.
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:
import keyring
@ -152,13 +159,12 @@ class AYONSecureRegistry:
keyring.set_keyring(Windows.WinVaultKeyring())
# Force "AYON" prefix
self._name = "/".join(("AYON", name))
self._name = f"AYON/{name}"
def set_item(self, name, value):
# type: (str, str) -> None
"""Set sensitive item into system's keyring.
def set_item(self, name: str, value: str) -> None:
"""Set sensitive item into the 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.
Args:
@ -172,22 +178,26 @@ class AYONSecureRegistry:
import keyring
keyring.set_password(self._name, name, value)
self.get_item.cache_clear()
@lru_cache(maxsize=32)
def get_item(self, name, default=_PLACEHOLDER):
"""Get value of sensitive item from system's keyring.
def get_item(
self, name: str, default: Any = _PLACEHOLDER
) -> Optional[str]:
"""Get value of sensitive item from the system's keyring.
See also `Keyring module`_
Args:
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:
value (str): Value of the item.
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:
https://github.com/jaraco/keyring
@ -202,14 +212,12 @@ class AYONSecureRegistry:
if default is not _PLACEHOLDER:
return default
# NOTE Should raise `KeyError`
raise ValueError(
"Item {}:{} does not exist in keyring.".format(self._name, name)
raise RegistryItemNotFound(
f"Item {self._name}:{name} not found in keyring."
)
def delete_item(self, name):
# type: (str) -> None
"""Delete value stored in system's keyring.
def delete_item(self, name: str) -> None:
"""Delete value stored in the system's keyring.
See also `Keyring module`_
@ -227,47 +235,38 @@ class AYONSecureRegistry:
class ASettingRegistry(ABC):
"""Abstract class defining structure of **SettingRegistry** 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.
"""Abstract class to defining structure of registry class.
"""
def __init__(self, name):
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
def __init__(self, name: str) -> None:
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
def _set_item(self, name, value):
# type: (str, str) -> None
# Implement it
pass
def _get_item(self, name: str) -> Any:
"""Get item value from registry."""
def __setitem__(self, name, value):
self._items[name] = value
@abstractmethod
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)
def get_item(self, name):
# type: (str) -> str
def __delitem__(self, name: str) -> None:
self._delete_item(name)
@property
def name(self) -> str:
return self._name
def get_item(self, name: str) -> str:
"""Get item from settings registry.
Args:
@ -277,22 +276,22 @@ class ASettingRegistry(ABC):
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist.
RegistryItemNotFound: If the item doesn't exist.
"""
return self._get_item(name)
@abstractmethod
def _get_item(self, name):
# type: (str) -> str
# Implement it
pass
def set_item(self, name: str, value: str) -> None:
"""Set item to settings registry.
def __getitem__(self, name):
return self._get_item(name)
Args:
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.
Args:
@ -301,16 +300,6 @@ class ASettingRegistry(ABC):
"""
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 using :mod:`configparser`.
@ -318,20 +307,17 @@ class IniSettingRegistry(ASettingRegistry):
This class is using :mod:`configparser` (ini) files to store items.
"""
def __init__(self, name, path):
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
def __init__(self, name: str, path: str) -> None:
super().__init__(name)
# 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):
with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg)
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):
# type: (str, str, str) -> None
def set_item_section(self, section: str, name: str, value: str) -> None:
"""Set item to specific section of ini registry.
If section doesn't exists, it is created.
@ -354,12 +340,10 @@ class IniSettingRegistry(ASettingRegistry):
with open(self._registry_file, mode="w") as cfg:
config.write(cfg)
def _set_item(self, name, value):
# type: (str, str) -> None
def _set_item(self, name: str, value: str) -> None:
self.set_item_section("MAIN", name, value)
def set_item(self, name, value):
# type: (str, str) -> None
def set_item(self, name: str, value: str) -> None:
"""Set item to settings ini file.
This saves item to ``DEFAULT`` section of ini as each item there
@ -372,10 +356,9 @@ class IniSettingRegistry(ASettingRegistry):
"""
# this does the some, overridden just for different docstring.
# 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):
# type: (str) -> str
def get_item(self, name: str) -> str:
"""Gets item from settings ini file.
This gets settings from ``DEFAULT`` section of ini file as each item
@ -388,19 +371,18 @@ class IniSettingRegistry(ASettingRegistry):
str: Value of item.
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)
def get_item_from_section(self, section, name):
# type: (str, str) -> str
def get_item_from_section(self, section: str, name: str) -> str:
"""Get item from section of ini file.
This will read ini file and try to get item value from specified
section. If that section or item doesn't exist, :exc:`ValueError`
is risen.
section. If that section or item doesn't exist,
:exc:`RegistryItemNotFound` is risen.
Args:
section (str): Name of ini section.
@ -410,7 +392,7 @@ class IniSettingRegistry(ASettingRegistry):
str: Item value.
Raises:
ValueError: If value doesn't exist.
RegistryItemNotFound: If value doesn't exist.
"""
config = configparser.ConfigParser()
@ -418,16 +400,15 @@ class IniSettingRegistry(ASettingRegistry):
try:
value = config[section][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}:{}".format(section, name))
raise RegistryItemNotFound(
f"Registry doesn't contain value {section}:{name}"
)
return value
def _get_item(self, name):
# type: (str) -> str
def _get_item(self, name: str) -> str:
return self.get_item_from_section("MAIN", name)
def delete_item_from_section(self, section, name):
# type: (str, str) -> None
def delete_item_from_section(self, section: str, name: str) -> None:
"""Delete item from section in ini file.
Args:
@ -435,7 +416,7 @@ class IniSettingRegistry(ASettingRegistry):
name (str): Name of the item.
Raises:
ValueError: If item doesn't exist.
RegistryItemNotFound: If the item doesn't exist.
"""
self.get_item_from_section.cache_clear()
@ -444,8 +425,9 @@ class IniSettingRegistry(ASettingRegistry):
try:
_ = config[section][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}:{}".format(section, name))
raise RegistryItemNotFound(
f"Registry doesn't contain value {section}:{name}"
)
config.remove_option(section, name)
# if section is empty, delete it
@ -461,29 +443,28 @@ class IniSettingRegistry(ASettingRegistry):
class JSONSettingRegistry(ASettingRegistry):
"""Class using json file as storage."""
"""Class using a json file as storage."""
def __init__(self, name, path):
# type: (str, str) -> JSONSettingRegistry
super(JSONSettingRegistry, self).__init__(name)
#: str: name of registry file
self._registry_file = os.path.join(path, "{}.json".format(name))
def __init__(self, name: str, path: str) -> None:
super().__init__(name)
self._registry_file = os.path.join(path, f"{name}.json")
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = {
"__metadata__": {"generated": now},
"registry": {}
}
if not os.path.exists(os.path.dirname(self._registry_file)):
os.makedirs(os.path.dirname(self._registry_file), exist_ok=True)
# Use 'os.path.dirname' in case someone uses slashes in 'name'
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):
with open(self._registry_file, mode="w") as cfg:
json.dump(header, cfg, indent=4)
@lru_cache(maxsize=32)
def _get_item(self, name):
# type: (str) -> object
"""Get item value from registry json.
def _get_item(self, name: str) -> str:
"""Get item value from the registry.
Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.get_item`
@ -494,29 +475,13 @@ class JSONSettingRegistry(ASettingRegistry):
try:
value = data["registry"][name]
except KeyError:
raise ValueError(
"Registry doesn't contain value {}".format(name))
raise RegistryItemNotFound(
f"Registry doesn't contain value {name}"
)
return value
def get_item(self, name):
# type: (str) -> object
"""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.
def _set_item(self, name: str, value: str) -> None:
"""Set item value to the registry.
Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.set_item`
@ -528,41 +493,39 @@ class JSONSettingRegistry(ASettingRegistry):
cfg.truncate(0)
cfg.seek(0)
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()
def _delete_item(self, name: str) -> None:
with open(self._registry_file, "r+") as cfg:
data = json.load(cfg)
del data["registry"][name]
cfg.truncate(0)
cfg.seek(0)
json.dump(data, cfg, indent=4)
self._get_item.cache_clear()
class AYONSettingsRegistry(JSONSettingRegistry):
"""Class handling AYON general settings registry.
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:
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()
super(AYONSettingsRegistry, self).__init__(name, path)
super().__init__(name, path)
def get_local_site_id():

View file

@ -361,12 +361,18 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
# Use project settings based on a category name
if category:
try:
return (
output = (
project_settings
[category]
["publish"]
[plugin.__name__]
)
warnings.warn(
"Please fill 'settings_category'"
f" for plugin '{plugin.__name__}'.",
DeprecationWarning
)
return output
except KeyError:
pass
@ -391,12 +397,18 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
category_from_file = "core"
try:
return (
output = (
project_settings
[category_from_file]
[plugin_kind]
[plugin.__name__]
)
warnings.warn(
"Please fill 'settings_category'"
f" for plugin '{plugin.__name__}'.",
DeprecationWarning
)
return output
except KeyError:
pass
return {}

View file

@ -38,6 +38,8 @@ class CleanUp(pyblish.api.InstancePlugin):
"webpublisher",
"shell"
]
settings_category = "core"
exclude_families = ["clip"]
optional = True
active = True

View file

@ -13,6 +13,8 @@ class CleanUpFarm(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder + 11
label = "Clean Up Farm"
settings_category = "core"
enabled = True
# Keep "filesequence" for backwards compatibility of older jobs

View file

@ -46,6 +46,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder + 0.49
label = "Collect Anatomy Instance data"
settings_category = "core"
follow_workfile_version = False
def process(self, context):

View file

@ -41,6 +41,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
"max",
"circuit",
]
settings_category = "core"
audio_product_name = "audioMain"

View file

@ -23,6 +23,7 @@ class CollectFramesFixDef(
targets = ["local"]
hosts = ["nuke"]
families = ["render", "prerender"]
settings_category = "core"
rewrite_version_enable = False

View file

@ -12,9 +12,10 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
"""
order = pyblish.api.CollectorOrder
label = 'Collect Scene Version'
label = "Collect Scene Version"
# configurable in Settings
hosts = ["*"]
settings_category = "core"
# in some cases of headless publishing (for example webpublisher using PS)
# you want to ignore version from name and let integrate use next version

View file

@ -57,6 +57,7 @@ class ExtractBurnin(publish.Extractor):
"unreal",
"circuit",
]
settings_category = "core"
optional = True

View file

@ -55,6 +55,8 @@ class ExtractOIIOTranscode(publish.Extractor):
label = "Transcode color spaces"
order = pyblish.api.ExtractorOrder + 0.019
settings_category = "core"
optional = True
# Supported extensions

View file

@ -165,6 +165,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
"photoshop"
]
settings_category = "core"
# Supported extensions
image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"}
video_exts = {"mov", "mp4"}

View file

@ -43,6 +43,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"houdini",
"circuit",
]
settings_category = "core"
enabled = False
integrate_thumbnail = False

View file

@ -256,6 +256,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
label = "Collect USD Layer Contributions (Asset/Shot)"
families = ["usd"]
enabled = True
settings_category = "core"
# A contribution defines a contribution into a (department) layer which
# will get layered into the target product, usually the asset or shot.
@ -633,6 +634,8 @@ class ExtractUSDLayerContribution(publish.Extractor):
label = "Extract USD Layer Contributions (Asset/Shot)"
order = pyblish.api.ExtractorOrder + 0.45
settings_category = "core"
use_ayon_entity_uri = False
def process(self, instance):
@ -795,6 +798,8 @@ class ExtractUSDAssetContribution(publish.Extractor):
label = "Extract USD Asset/Shot Contributions"
order = ExtractUSDLayerContribution.order + 0.01
settings_category = "core"
use_ayon_entity_uri = False
def process(self, instance):

View file

@ -61,6 +61,8 @@ class IntegrateHeroVersion(
# Must happen after IntegrateNew
order = pyblish.api.IntegratorOrder + 0.1
settings_category = "core"
optional = True
active = True

View file

@ -24,6 +24,8 @@ class IntegrateProductGroup(pyblish.api.InstancePlugin):
order = pyblish.api.IntegratorOrder - 0.1
label = "Product Group"
settings_category = "core"
# Attributes set by settings
product_grouping_profiles = None

View file

@ -22,6 +22,8 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
label = "Override Integrate Thumbnail Representations"
order = pyblish.api.IntegratorOrder - 0.1
settings_category = "core"
integrate_profiles = []
def process(self, instance):

View file

@ -31,6 +31,7 @@ class ValidateOutdatedContainers(
label = "Validate Outdated Containers"
order = pyblish.api.ValidatorOrder
settings_category = "core"
optional = True
actions = [ShowInventory]

View file

@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
label = "Validate File Saved"
order = pyblish.api.ValidatorOrder - 0.1
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
"cinema4d", "silhouette", "gaffer", "blender"]
"cinema4d", "silhouette", "gaffer", "blender", "loki"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context):

View file

@ -14,6 +14,8 @@ class ValidateIntent(pyblish.api.ContextPlugin):
order = pyblish.api.ValidatorOrder
label = "Validate Intent"
settings_category = "core"
enabled = False
# Can be modified by settings

View file

@ -17,6 +17,7 @@ class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
order = pyblish.api.ValidatorOrder
label = "Validate Version"
settings_category = "core"
optional = False
active = True