Merge branch 'develop' into feature/909-define-basic-trait-type-using-dataclasses

This commit is contained in:
Ondřej Samohel 2025-04-09 11:12:28 +02:00 committed by GitHub
commit 51d7cc16af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 242 additions and 77 deletions

View file

@ -1,12 +1,15 @@
from ayon_api import get_project, get_folder_by_path, get_task_by_name from ayon_api import get_project, get_folder_by_path, get_task_by_name
from ayon_core.pipeline import Anatomy
from ayon_core.pipeline.anatomy import RootMissingEnv
from ayon_applications import PreLaunchHook from ayon_applications import PreLaunchHook
from ayon_applications.exceptions import ApplicationLaunchFailed
from ayon_applications.utils import ( from ayon_applications.utils import (
EnvironmentPrepData, EnvironmentPrepData,
prepare_app_environments, prepare_app_environments,
prepare_context_environments prepare_context_environments
) )
from ayon_core.pipeline import Anatomy
class GlobalHostDataHook(PreLaunchHook): class GlobalHostDataHook(PreLaunchHook):
@ -67,9 +70,12 @@ class GlobalHostDataHook(PreLaunchHook):
self.data["project_entity"] = project_entity self.data["project_entity"] = project_entity
# Anatomy # Anatomy
self.data["anatomy"] = Anatomy( try:
project_name, project_entity=project_entity self.data["anatomy"] = Anatomy(
) project_name, project_entity=project_entity
)
except RootMissingEnv as exc:
raise ApplicationLaunchFailed(str(exc))
folder_path = self.data.get("folder_path") folder_path = self.data.get("folder_path")
if not folder_path: if not folder_path:

View file

@ -1,5 +1,6 @@
from .exceptions import ( from .exceptions import (
ProjectNotSet, ProjectNotSet,
RootMissingEnv,
RootCombinationError, RootCombinationError,
TemplateMissingKey, TemplateMissingKey,
AnatomyTemplateUnsolved, AnatomyTemplateUnsolved,
@ -9,6 +10,7 @@ from .anatomy import Anatomy
__all__ = ( __all__ = (
"ProjectNotSet", "ProjectNotSet",
"RootMissingEnv",
"RootCombinationError", "RootCombinationError",
"TemplateMissingKey", "TemplateMissingKey",
"AnatomyTemplateUnsolved", "AnatomyTemplateUnsolved",

View file

@ -5,6 +5,11 @@ class ProjectNotSet(Exception):
"""Exception raised when is created Anatomy without project name.""" """Exception raised when is created Anatomy without project name."""
class RootMissingEnv(KeyError):
"""Raised when root requires environment variables which is not filled."""
pass
class RootCombinationError(Exception): class RootCombinationError(Exception):
"""This exception is raised when templates has combined root types.""" """This exception is raised when templates has combined root types."""

View file

@ -2,9 +2,11 @@ import os
import platform import platform
import numbers import numbers
from ayon_core.lib import Logger from ayon_core.lib import Logger, StringTemplate
from ayon_core.lib.path_templates import FormatObject from ayon_core.lib.path_templates import FormatObject
from .exceptions import RootMissingEnv
class RootItem(FormatObject): class RootItem(FormatObject):
"""Represents one item or roots. """Represents one item or roots.
@ -21,18 +23,36 @@ class RootItem(FormatObject):
multi root setup otherwise None value is expected. multi root setup otherwise None value is expected.
""" """
def __init__(self, parent, root_raw_data, name): def __init__(self, parent, root_raw_data, name):
super(RootItem, self).__init__() super().__init__()
self._log = None self._log = None
lowered_platform_keys = {} lowered_platform_keys = {
for key, value in root_raw_data.items(): key.lower(): value
lowered_platform_keys[key.lower()] = value for key, value in root_raw_data.items()
}
self.raw_data = lowered_platform_keys self.raw_data = lowered_platform_keys
self.cleaned_data = self._clean_roots(lowered_platform_keys) self.cleaned_data = self._clean_roots(lowered_platform_keys)
self.name = name self.name = name
self.parent = parent self.parent = parent
self.available_platforms = set(lowered_platform_keys.keys()) self.available_platforms = set(lowered_platform_keys.keys())
self.value = lowered_platform_keys.get(platform.system().lower())
current_platform = platform.system().lower()
# WARNING: Using environment variables in roots is not considered
# as production safe. Some features may not work as expected, for
# example USD resolver or site sync.
try:
self.value = lowered_platform_keys[current_platform].format_map(
os.environ
)
except KeyError:
result = StringTemplate(self.value).format(os.environ.copy())
is_are = "is" if len(result.missing_keys) == 1 else "are"
missing_keys = ", ".join(result.missing_keys)
raise RootMissingEnv(
f"Root \"{name}\" requires environment variable/s"
f" {missing_keys} which {is_are} not available."
)
self.clean_value = self._clean_root(self.value) self.clean_value = self._clean_root(self.value)
def __format__(self, *args, **kwargs): def __format__(self, *args, **kwargs):
@ -105,10 +125,10 @@ class RootItem(FormatObject):
def _clean_roots(self, raw_data): def _clean_roots(self, raw_data):
"""Clean all values of raw root item values.""" """Clean all values of raw root item values."""
cleaned = {} return {
for key, value in raw_data.items(): key: self._clean_root(value)
cleaned[key] = self._clean_root(value) for key, value in raw_data.items()
return cleaned }
def path_remapper(self, path, dst_platform=None, src_platform=None): def path_remapper(self, path, dst_platform=None, src_platform=None):
"""Remap path for specific platform. """Remap path for specific platform.

View file

@ -27,7 +27,8 @@ from .workfile import (
get_workdir, get_workdir,
get_custom_workfile_template_by_string_context, get_custom_workfile_template_by_string_context,
get_workfile_template_key_from_context, get_workfile_template_key_from_context,
get_last_workfile get_last_workfile,
MissingWorkdirError,
) )
from . import ( from . import (
register_loader_plugin_path, register_loader_plugin_path,
@ -251,7 +252,7 @@ def uninstall_host():
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
deregister_loader_plugin_path(LOAD_PATH) deregister_loader_plugin_path(LOAD_PATH)
deregister_inventory_action_path(INVENTORY_PATH) deregister_inventory_action_path(INVENTORY_PATH)
log.info("Global plug-ins unregistred") log.info("Global plug-ins unregistered")
deregister_host() deregister_host()
@ -617,7 +618,18 @@ def version_up_current_workfile():
last_workfile_path = get_last_workfile( last_workfile_path = get_last_workfile(
work_root, file_template, data, extensions, True work_root, file_template, data, extensions, True
) )
new_workfile_path = version_up(last_workfile_path) # `get_last_workfile` will return the first expected file version
# if no files exist yet. In that case, if they do not exist we will
# want to save v001
new_workfile_path = last_workfile_path
if os.path.exists(new_workfile_path): if os.path.exists(new_workfile_path):
new_workfile_path = version_up(new_workfile_path) new_workfile_path = version_up(new_workfile_path)
# Raise an error if the parent folder doesn't exist as `host.save_workfile`
# is not supposed/able to create missing folders.
parent_folder = os.path.dirname(new_workfile_path)
if not os.path.exists(parent_folder):
raise MissingWorkdirError(
f"Work area directory '{parent_folder}' does not exist.")
host.save_workfile(new_workfile_path) host.save_workfile(new_workfile_path)

View file

@ -2303,10 +2303,16 @@ class CreateContext:
for plugin_name, plugin_value in item_changes.pop( for plugin_name, plugin_value in item_changes.pop(
"publish_attributes" "publish_attributes"
).items(): ).items():
if plugin_value is None:
current_publish[plugin_name] = None
continue
plugin_changes = current_publish.setdefault( plugin_changes = current_publish.setdefault(
plugin_name, {} plugin_name, {}
) )
plugin_changes.update(plugin_value) if plugin_changes is None:
current_publish[plugin_name] = plugin_value
else:
plugin_changes.update(plugin_value)
item_values.update(item_changes) item_values.update(item_changes)

View file

@ -160,29 +160,26 @@ class AttributeValues:
return self._attr_defs_by_key.get(key, default) return self._attr_defs_by_key.get(key, default)
def update(self, value): def update(self, value):
changes = {} changes = self._update(value)
for _key, _value in dict(value).items():
if _key in self._data and self._data.get(_key) == _value:
continue
self._data[_key] = _value
changes[_key] = _value
if changes: if changes:
self._parent.attribute_value_changed(self._key, changes) self._parent.attribute_value_changed(self._key, changes)
def pop(self, key, default=None): def pop(self, key, default=None):
has_key = key in self._data value, changes = self._pop(key, default)
value = self._data.pop(key, default) if changes:
# Remove attribute definition if is 'UnknownDef' self._parent.attribute_value_changed(self._key, changes)
# - gives option to get rid of unknown values
attr_def = self._attr_defs_by_key.get(key)
if isinstance(attr_def, UnknownDef):
self._attr_defs_by_key.pop(key)
self._attr_defs.remove(attr_def)
elif has_key:
self._parent.attribute_value_changed(self._key, {key: None})
return value return value
def set_value(self, value):
pop_keys = set(value.keys()) - set(self._data.keys())
changes = self._update(value)
for key in pop_keys:
_, key_changes = self._pop(key, None)
changes.update(key_changes)
if changes:
self._parent.attribute_value_changed(self._key, changes)
def reset_values(self): def reset_values(self):
self._data = {} self._data = {}
@ -228,6 +225,29 @@ class AttributeValues:
return serialize_attr_defs(self._attr_defs) return serialize_attr_defs(self._attr_defs)
def _update(self, value):
changes = {}
for key, value in dict(value).items():
if key in self._data and self._data.get(key) == value:
continue
self._data[key] = value
changes[key] = value
return changes
def _pop(self, key, default):
has_key = key in self._data
value = self._data.pop(key, default)
# Remove attribute definition if is 'UnknownDef'
# - gives option to get rid of unknown values
attr_def = self._attr_defs_by_key.get(key)
changes = {}
if isinstance(attr_def, UnknownDef):
self._attr_defs_by_key.pop(key)
self._attr_defs.remove(attr_def)
elif has_key:
changes[key] = None
return value, changes
class CreatorAttributeValues(AttributeValues): class CreatorAttributeValues(AttributeValues):
"""Creator specific attribute values of an instance.""" """Creator specific attribute values of an instance."""
@ -270,6 +290,23 @@ class PublishAttributes:
def __getitem__(self, key): def __getitem__(self, key):
return self._data[key] return self._data[key]
def __setitem__(self, key, value):
"""Set value for plugin.
Args:
key (str): Plugin name.
value (dict[str, Any]): Value to set.
"""
current_value = self._data.get(key)
if isinstance(current_value, PublishAttributeValues):
current_value.set_value(value)
else:
self._data[key] = value
def __delitem__(self, key):
self.pop(key)
def __contains__(self, key): def __contains__(self, key):
return key in self._data return key in self._data

View file

@ -226,11 +226,26 @@ class _CacheItems:
thumbnails_cache = ThumbnailsCache() thumbnails_cache = ThumbnailsCache()
def get_thumbnail_path(project_name, thumbnail_id): def get_thumbnail_path(
project_name: str,
entity_type: str,
entity_id: str,
thumbnail_id: str
):
"""Get path to thumbnail image. """Get path to thumbnail image.
Thumbnail is cached by thumbnail id but is received using entity type and
entity id.
Notes:
Function 'get_thumbnail_by_id' can't be used because does not work
for artists. The endpoint can't validate artist permissions.
Args: Args:
project_name (str): Project where thumbnail belongs to. project_name (str): Project where thumbnail belongs to.
entity_type (str): Entity type "folder", "task", "version"
and "workfile".
entity_id (str): Entity id.
thumbnail_id (Union[str, None]): Thumbnail id. thumbnail_id (Union[str, None]): Thumbnail id.
Returns: Returns:
@ -251,7 +266,7 @@ def get_thumbnail_path(project_name, thumbnail_id):
# 'get_thumbnail_by_id' did not return output of # 'get_thumbnail_by_id' did not return output of
# 'ServerAPI' method. # 'ServerAPI' method.
con = ayon_api.get_server_api_connection() con = ayon_api.get_server_api_connection()
result = con.get_thumbnail_by_id(project_name, thumbnail_id) result = con.get_thumbnail(project_name, entity_type, entity_id)
if result is not None and result.is_valid: if result is not None and result.is_valid:
return _CacheItems.thumbnails_cache.store_thumbnail( return _CacheItems.thumbnails_cache.store_thumbnail(

View file

@ -16,6 +16,7 @@ from .path_resolving import (
from .utils import ( from .utils import (
should_use_last_workfile_on_launch, should_use_last_workfile_on_launch,
should_open_workfiles_tool_on_launch, should_open_workfiles_tool_on_launch,
MissingWorkdirError,
) )
from .build_workfile import BuildWorkfile from .build_workfile import BuildWorkfile
@ -46,6 +47,7 @@ __all__ = (
"should_use_last_workfile_on_launch", "should_use_last_workfile_on_launch",
"should_open_workfiles_tool_on_launch", "should_open_workfiles_tool_on_launch",
"MissingWorkdirError",
"BuildWorkfile", "BuildWorkfile",

View file

@ -2,6 +2,11 @@ from ayon_core.lib import filter_profiles
from ayon_core.settings import get_project_settings from ayon_core.settings import get_project_settings
class MissingWorkdirError(Exception):
"""Raised when accessing a work directory not found on disk."""
pass
def should_use_last_workfile_on_launch( def should_use_last_workfile_on_launch(
project_name, project_name,
host_name, host_name,

View file

@ -21,8 +21,50 @@ class ThumbnailsModel:
self._folders_cache.reset() self._folders_cache.reset()
self._versions_cache.reset() self._versions_cache.reset()
def get_thumbnail_path(self, project_name, thumbnail_id): def get_thumbnail_paths(
return self._get_thumbnail_path(project_name, thumbnail_id) self,
project_name,
entity_type,
entity_ids,
):
thumbnail_paths = set()
if not project_name or not entity_type or not entity_ids:
return thumbnail_paths
thumbnail_id_by_entity_id = {}
if entity_type == "folder":
thumbnail_id_by_entity_id = self.get_folder_thumbnail_ids(
project_name, entity_ids
)
elif entity_type == "version":
thumbnail_id_by_entity_id = self.get_version_thumbnail_ids(
project_name, entity_ids
)
if not thumbnail_id_by_entity_id:
return thumbnail_paths
entity_ids_by_thumbnail_id = collections.defaultdict(set)
for entity_id, thumbnail_id in thumbnail_id_by_entity_id.items():
if not thumbnail_id:
continue
entity_ids_by_thumbnail_id[thumbnail_id].add(entity_id)
output = {
entity_id: None
for entity_id in entity_ids
}
for thumbnail_id, entity_ids in entity_ids_by_thumbnail_id.items():
thumbnail_path = self._get_thumbnail_path(
project_name, entity_type, next(iter(entity_ids)), thumbnail_id
)
if not thumbnail_path:
continue
for entity_id in entity_ids:
output[entity_id] = thumbnail_path
return output
def get_folder_thumbnail_ids(self, project_name, folder_ids): def get_folder_thumbnail_ids(self, project_name, folder_ids):
project_cache = self._folders_cache[project_name] project_cache = self._folders_cache[project_name]
@ -56,7 +98,13 @@ class ThumbnailsModel:
output[version_id] = cache.get_data() output[version_id] = cache.get_data()
return output return output
def _get_thumbnail_path(self, project_name, thumbnail_id): def _get_thumbnail_path(
self,
project_name,
entity_type,
entity_id,
thumbnail_id
):
if not thumbnail_id: if not thumbnail_id:
return None return None
@ -64,7 +112,12 @@ class ThumbnailsModel:
if thumbnail_id in project_cache: if thumbnail_id in project_cache:
return project_cache[thumbnail_id] return project_cache[thumbnail_id]
filepath = get_thumbnail_path(project_name, thumbnail_id) filepath = get_thumbnail_path(
project_name,
entity_type,
entity_id,
thumbnail_id
)
project_cache[thumbnail_id] = filepath project_cache[thumbnail_id] = filepath
return filepath return filepath

View file

@ -733,7 +733,12 @@ class FrontendLoaderController(_BaseLoaderController):
pass pass
@abstractmethod @abstractmethod
def get_thumbnail_path(self, project_name, thumbnail_id): def get_thumbnail_paths(
self,
project_name,
entity_type,
entity_ids
):
"""Get thumbnail path for thumbnail id. """Get thumbnail path for thumbnail id.
This method should get a path to a thumbnail based on thumbnail id. This method should get a path to a thumbnail based on thumbnail id.
@ -742,10 +747,11 @@ class FrontendLoaderController(_BaseLoaderController):
Args: Args:
project_name (str): Project name. project_name (str): Project name.
thumbnail_id (str): Thumbnail id. entity_type (str): Entity type.
entity_ids (set[str]): Entity ids.
Returns: Returns:
Union[str, None]: Thumbnail path or None if not found. dict[str, Union[str, None]]: Thumbnail path by entity id.
""" """
pass pass

View file

@ -259,9 +259,14 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
project_name, version_ids project_name, version_ids
) )
def get_thumbnail_path(self, project_name, thumbnail_id): def get_thumbnail_paths(
return self._thumbnails_model.get_thumbnail_path( self,
project_name, thumbnail_id project_name,
entity_type,
entity_ids,
):
return self._thumbnails_model.get_thumbnail_paths(
project_name, entity_type, entity_ids
) )
def change_products_group(self, project_name, product_ids, group_name): def change_products_group(self, project_name, product_ids, group_name):

View file

@ -501,38 +501,29 @@ class LoaderWindow(QtWidgets.QWidget):
self._update_thumbnails() self._update_thumbnails()
def _update_thumbnails(self): def _update_thumbnails(self):
# TODO make this threaded and show loading animation while running
project_name = self._selected_project_name project_name = self._selected_project_name
thumbnail_ids = set() entity_type = None
entity_ids = set()
if self._selected_version_ids: if self._selected_version_ids:
thumbnail_id_by_entity_id = ( entity_ids = set(self._selected_version_ids)
self._controller.get_version_thumbnail_ids( entity_type = "version"
project_name,
self._selected_version_ids
)
)
thumbnail_ids = set(thumbnail_id_by_entity_id.values())
elif self._selected_folder_ids: elif self._selected_folder_ids:
thumbnail_id_by_entity_id = ( entity_ids = set(self._selected_folder_ids)
self._controller.get_folder_thumbnail_ids( entity_type = "folder"
project_name,
self._selected_folder_ids
)
)
thumbnail_ids = set(thumbnail_id_by_entity_id.values())
thumbnail_ids.discard(None) thumbnail_path_by_entity_id = self._controller.get_thumbnail_paths(
project_name, entity_type, entity_ids
if not thumbnail_ids: )
self._thumbnails_widget.set_current_thumbnails(None) thumbnail_paths = set(thumbnail_path_by_entity_id.values())
return
thumbnail_paths = set()
for thumbnail_id in thumbnail_ids:
thumbnail_path = self._controller.get_thumbnail_path(
project_name, thumbnail_id)
thumbnail_paths.add(thumbnail_path)
thumbnail_paths.discard(None) thumbnail_paths.discard(None)
self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths)
if thumbnail_paths:
self._thumbnails_widget.set_current_thumbnail_paths(
thumbnail_paths
)
else:
self._thumbnails_widget.set_current_thumbnails(None)
def _on_projects_refresh(self): def _on_projects_refresh(self):
self._refresh_handler.set_project_refreshed() self._refresh_handler.set_project_refreshed()

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.1.6+dev" __version__ = "1.1.7+dev"

View file

@ -1,6 +1,6 @@
name = "core" name = "core"
title = "Core" title = "Core"
version = "1.1.6+dev" version = "1.1.7+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.1.6+dev" version = "1.1.7+dev"
description = "" description = ""
authors = ["Ynput Team <team@ynput.io>"] authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md" readme = "README.md"