diff --git a/.gitignore b/.gitignore index 41389755f1..4e56d77392 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ dump.sql mypy.ini .github_changelog_generator + +# ignore mkdocs build +site/ diff --git a/client/ayon_core/hooks/pre_global_host_data.py b/client/ayon_core/hooks/pre_global_host_data.py index 12da6f12f8..23f725901c 100644 --- a/client/ayon_core/hooks/pre_global_host_data.py +++ b/client/ayon_core/hooks/pre_global_host_data.py @@ -1,12 +1,15 @@ 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.exceptions import ApplicationLaunchFailed from ayon_applications.utils import ( EnvironmentPrepData, prepare_app_environments, prepare_context_environments ) -from ayon_core.pipeline import Anatomy class GlobalHostDataHook(PreLaunchHook): @@ -67,9 +70,12 @@ class GlobalHostDataHook(PreLaunchHook): self.data["project_entity"] = project_entity # Anatomy - self.data["anatomy"] = Anatomy( - project_name, project_entity=project_entity - ) + try: + 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") if not folder_path: diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 03ed574081..92c3966e77 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -9,6 +9,7 @@ from .local_settings import ( AYONSettingsRegistry, get_launcher_local_dir, get_launcher_storage_dir, + get_addons_resources_dir, get_local_site_id, get_ayon_username, ) @@ -142,6 +143,7 @@ __all__ = [ "AYONSettingsRegistry", "get_launcher_local_dir", "get_launcher_storage_dir", + "get_addons_resources_dir", "get_local_site_id", "get_ayon_username", diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index eff0068f00..d994145d4b 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -96,6 +96,30 @@ def get_launcher_local_dir(*subdirs: str) -> str: return os.path.join(storage_dir, *subdirs) +def get_addons_resources_dir(addon_name: str, *args) -> str: + """Get 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 + dedicated directory to store them with 'AYON_ADDONS_RESOURCES_DIR' + environment variable. By default, is used 'addons_resources' in + launcher storage (might be shared across platforms). + + Args: + addon_name (str): Addon name. + *args (str): Subfolders in resources directory. + + Returns: + str: Path to resources directory. + + """ + addons_resources_dir = os.getenv("AYON_ADDONS_RESOURCES_DIR") + if not addons_resources_dir: + addons_resources_dir = get_launcher_storage_dir("addons_resources") + + return os.path.join(addons_resources_dir, addon_name, *args) + + class AYONSecureRegistry: """Store information using keyring. diff --git a/client/ayon_core/lib/python_module_tools.py b/client/ayon_core/lib/python_module_tools.py index d146e069a9..a6dc8031c7 100644 --- a/client/ayon_core/lib/python_module_tools.py +++ b/client/ayon_core/lib/python_module_tools.py @@ -1,6 +1,8 @@ +"""Tools for working with python modules and classes.""" import os import sys import types +from typing import Optional import importlib import inspect import logging @@ -8,13 +10,22 @@ import logging log = logging.getLogger(__name__) -def import_filepath(filepath, module_name=None): +def import_filepath( + filepath: str, + module_name: Optional[str] = None, + sys_module_name: Optional[str] = None) -> types.ModuleType: """Import python file as python module. Args: filepath (str): Path to python file. module_name (str): Name of loaded module. Only for Python 3. By default is filled with filename of filepath. + sys_module_name (str): Name of module in `sys.modules` where to store + loaded module. By default is None so module is not added to + `sys.modules`. + + Todo (antirotor): We should add the module to the sys.modules always but + we need to be careful about it and test it properly. """ if module_name is None: @@ -28,6 +39,9 @@ def import_filepath(filepath, module_name=None): module_loader = importlib.machinery.SourceFileLoader( module_name, filepath ) + # only add to sys.modules if requested + if sys_module_name: + sys.modules[sys_module_name] = module module_loader.exec_module(module) return module @@ -126,7 +140,8 @@ def classes_from_module(superclass, module): return classes -def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): +def import_module_from_dirpath( + dirpath, folder_name, dst_module_name=None): """Import passed directory as a python module. Imported module can be assigned as a child attribute of already loaded @@ -193,7 +208,7 @@ def is_func_signature_supported(func, *args, **kwargs): Notes: This does NOT check if the function would work with passed arguments only if they can be passed in. If function have *args, **kwargs - in paramaters, this will always return 'True'. + in parameters, this will always return 'True'. Example: >>> def my_function(my_number): diff --git a/client/ayon_core/pipeline/anatomy/__init__.py b/client/ayon_core/pipeline/anatomy/__init__.py index 336d09ccaa..7000f51495 100644 --- a/client/ayon_core/pipeline/anatomy/__init__.py +++ b/client/ayon_core/pipeline/anatomy/__init__.py @@ -1,5 +1,6 @@ from .exceptions import ( ProjectNotSet, + RootMissingEnv, RootCombinationError, TemplateMissingKey, AnatomyTemplateUnsolved, @@ -9,6 +10,7 @@ from .anatomy import Anatomy __all__ = ( "ProjectNotSet", + "RootMissingEnv", "RootCombinationError", "TemplateMissingKey", "AnatomyTemplateUnsolved", diff --git a/client/ayon_core/pipeline/anatomy/exceptions.py b/client/ayon_core/pipeline/anatomy/exceptions.py index 39f116baf0..24df0e3046 100644 --- a/client/ayon_core/pipeline/anatomy/exceptions.py +++ b/client/ayon_core/pipeline/anatomy/exceptions.py @@ -5,6 +5,11 @@ class ProjectNotSet(Exception): """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): """This exception is raised when templates has combined root types.""" diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 2773559d49..bd09a9fe51 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -2,9 +2,11 @@ import os import platform import numbers -from ayon_core.lib import Logger +from ayon_core.lib import Logger, StringTemplate from ayon_core.lib.path_templates import FormatObject +from .exceptions import RootMissingEnv + class RootItem(FormatObject): """Represents one item or roots. @@ -21,18 +23,36 @@ class RootItem(FormatObject): multi root setup otherwise None value is expected. """ def __init__(self, parent, root_raw_data, name): - super(RootItem, self).__init__() + super().__init__() self._log = None - lowered_platform_keys = {} - for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value + lowered_platform_keys = { + key.lower(): value + for key, value in root_raw_data.items() + } self.raw_data = lowered_platform_keys self.cleaned_data = self._clean_roots(lowered_platform_keys) self.name = name self.parent = parent 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) def __format__(self, *args, **kwargs): @@ -105,10 +125,10 @@ class RootItem(FormatObject): def _clean_roots(self, raw_data): """Clean all values of raw root item values.""" - cleaned = {} - for key, value in raw_data.items(): - cleaned[key] = self._clean_root(value) - return cleaned + return { + key: self._clean_root(value) + for key, value in raw_data.items() + } def path_remapper(self, path, dst_platform=None, src_platform=None): """Remap path for specific platform. diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 24557234f4..6ac6685647 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -839,7 +839,7 @@ class CreateContext: publish_attributes.update(output) for plugin in self.plugins_with_defs: - attr_defs = plugin.get_attr_defs_for_context (self) + attr_defs = plugin.get_attr_defs_for_context(self) if not attr_defs: continue self._publish_attributes.set_publish_plugin_attr_defs( @@ -1259,50 +1259,6 @@ class CreateContext: with self._bulk_context("add", sender) as bulk_info: yield bulk_info - # Set publish attributes before bulk context is exited - for instance in bulk_info.get_data(): - publish_attributes = instance.publish_attributes - # Prepare publish plugin attributes and set it on instance - for plugin in self.plugins_with_defs: - try: - if is_func_signature_supported( - plugin.convert_attribute_values, self, instance - ): - plugin.convert_attribute_values(self, instance) - - elif plugin.__instanceEnabled__: - output = plugin.convert_attribute_values( - publish_attributes - ) - if output: - publish_attributes.update(output) - - except Exception: - self.log.error( - "Failed to convert attribute values of" - f" plugin '{plugin.__name__}'", - exc_info=True - ) - - for plugin in self.plugins_with_defs: - attr_defs = None - try: - attr_defs = plugin.get_attr_defs_for_instance( - self, instance - ) - except Exception: - self.log.error( - "Failed to get attribute definitions" - f" from plugin '{plugin.__name__}'.", - exc_info=True - ) - - if not attr_defs: - continue - instance.set_publish_plugin_attr_defs( - plugin.__name__, attr_defs - ) - @contextmanager def bulk_instances_collection(self, sender=None): """DEPRECATED use 'bulk_add_instances' instead.""" @@ -2251,6 +2207,50 @@ class CreateContext: if not instances_to_validate: return + # Set publish attributes before bulk callbacks are triggered + for instance in instances_to_validate: + publish_attributes = instance.publish_attributes + # Prepare publish plugin attributes and set it on instance + for plugin in self.plugins_with_defs: + try: + if is_func_signature_supported( + plugin.convert_attribute_values, self, instance + ): + plugin.convert_attribute_values(self, instance) + + elif plugin.__instanceEnabled__: + output = plugin.convert_attribute_values( + publish_attributes + ) + if output: + publish_attributes.update(output) + + except Exception: + self.log.error( + "Failed to convert attribute values of" + f" plugin '{plugin.__name__}'", + exc_info=True + ) + + for plugin in self.plugins_with_defs: + attr_defs = None + try: + attr_defs = plugin.get_attr_defs_for_instance( + self, instance + ) + except Exception: + self.log.error( + "Failed to get attribute definitions" + f" from plugin '{plugin.__name__}'.", + exc_info=True + ) + + if not attr_defs: + continue + instance.set_publish_plugin_attr_defs( + plugin.__name__, attr_defs + ) + # Cache folder and task entities for all instances at once self.get_instances_context_info(instances_to_validate) @@ -2303,10 +2303,16 @@ class CreateContext: for plugin_name, plugin_value in item_changes.pop( "publish_attributes" ).items(): + if plugin_value is None: + current_publish[plugin_name] = None + continue plugin_changes = current_publish.setdefault( 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) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 17bb85b720..6b45a5c610 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -160,29 +160,26 @@ class AttributeValues: return self._attr_defs_by_key.get(key, default) 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 - + changes = self._update(value) if changes: self._parent.attribute_value_changed(self._key, changes) def pop(self, key, default=None): - 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) - 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}) + value, changes = self._pop(key, default) + if changes: + self._parent.attribute_value_changed(self._key, changes) 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): self._data = {} @@ -228,6 +225,29 @@ class AttributeValues: 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): """Creator specific attribute values of an instance.""" @@ -270,6 +290,23 @@ class PublishAttributes: def __getitem__(self, 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): return key in self._data diff --git a/client/ayon_core/pipeline/project_folders.py b/client/ayon_core/pipeline/project_folders.py index 902b969457..def2af9ba1 100644 --- a/client/ayon_core/pipeline/project_folders.py +++ b/client/ayon_core/pipeline/project_folders.py @@ -1,6 +1,8 @@ +from __future__ import annotations import os import re import json +from typing import Any, Union from ayon_core.settings import get_project_settings from ayon_core.lib import Logger @@ -9,7 +11,7 @@ from .anatomy import Anatomy from .template_data import get_project_template_data -def concatenate_splitted_paths(split_paths, anatomy): +def concatenate_splitted_paths(split_paths, anatomy: Anatomy): log = Logger.get_logger("concatenate_splitted_paths") pattern_array = re.compile(r"\[.*\]") output = [] @@ -47,7 +49,7 @@ def concatenate_splitted_paths(split_paths, anatomy): return output -def fill_paths(path_list, anatomy): +def fill_paths(path_list: list[str], anatomy: Anatomy): format_data = get_project_template_data(project_name=anatomy.project_name) format_data["root"] = anatomy.roots filled_paths = [] @@ -59,7 +61,7 @@ def fill_paths(path_list, anatomy): return filled_paths -def create_project_folders(project_name, basic_paths=None): +def create_project_folders(project_name: str, basic_paths=None): log = Logger.get_logger("create_project_folders") anatomy = Anatomy(project_name) if basic_paths is None: @@ -80,8 +82,19 @@ def create_project_folders(project_name, basic_paths=None): os.makedirs(path) -def _list_path_items(folder_structure): +def _list_path_items( + folder_structure: Union[dict[str, Any], list[str]]): output = [] + + # Allow leaf folders of the `project_folder_structure` to use a list of + # strings instead of a dictionary of keys with empty values. + if isinstance(folder_structure, list): + if not all(isinstance(item, str) for item in folder_structure): + raise ValueError( + f"List items must all be strings. Got: {folder_structure}") + return [[path] for path in folder_structure] + + # Process key, value as key for folder names and value its subfolders for key, value in folder_structure.items(): if not value: output.append(key) @@ -99,7 +112,7 @@ def _list_path_items(folder_structure): return output -def get_project_basic_paths(project_name): +def get_project_basic_paths(project_name: str): project_settings = get_project_settings(project_name) folder_structure = ( project_settings["core"]["project_folder_structure"] diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index cc5f67c74b..49ecab2221 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -1,3 +1,5 @@ +"""Library functions for publishing.""" +from __future__ import annotations import os import sys import inspect @@ -12,8 +14,8 @@ import pyblish.plugin import pyblish.api from ayon_core.lib import ( - Logger, import_filepath, + Logger, filter_profiles, ) from ayon_core.settings import get_project_settings @@ -163,7 +165,7 @@ class HelpContent: def load_help_content_from_filepath(filepath): """Load help content from xml file. - Xml file may containt errors and warnings. + Xml file may contain errors and warnings. """ errors = {} warnings = {} @@ -208,8 +210,9 @@ def load_help_content_from_plugin(plugin): return load_help_content_from_filepath(filepath) -def publish_plugins_discover(paths=None): - """Find and return available pyblish plug-ins +def publish_plugins_discover( + paths: Optional[list[str]] = None) -> DiscoverResult: + """Find and return available pyblish plug-ins. Overridden function from `pyblish` module to be able to collect crashed files and reason of their crash. @@ -252,17 +255,14 @@ def publish_plugins_discover(paths=None): continue try: - module = import_filepath(abspath, mod_name) + module = import_filepath( + abspath, mod_name, sys_module_name=mod_name) - # Store reference to original module, to avoid - # garbage collection from collecting it's global - # imports, such as `import os`. - sys.modules[abspath] = module - - except Exception as err: + except Exception as err: # noqa: BLE001 + # we need broad exception to catch all possible errors. result.crashed_file_paths[abspath] = sys.exc_info() - log.debug("Skipped: \"%s\" (%s)", mod_name, err) + log.debug('Skipped: "%s" (%s)', mod_name, err) continue for plugin in pyblish.plugin.plugins_from_module(module): @@ -280,9 +280,8 @@ def publish_plugins_discover(paths=None): continue plugin_names.append(plugin.__name__) - - plugin.__module__ = module.__file__ - key = "{0}.{1}".format(plugin.__module__, plugin.__name__) + plugin.__file__ = module.__file__ + key = f"{module.__file__}.{plugin.__name__}" plugins[key] = plugin # Include plug-ins from registration. @@ -361,7 +360,7 @@ def get_plugin_settings(plugin, project_settings, log, category=None): # Settings category determined from path # - usually path is './/plugins/publish/' # - category can be host name of addon name ('maya', 'deadline', ...) - filepath = os.path.normpath(inspect.getsourcefile(plugin)) + filepath = os.path.normpath(inspect.getfile(plugin)) split_path = filepath.rsplit(os.path.sep, 5) if len(split_path) < 4: @@ -427,7 +426,7 @@ def filter_pyblish_plugins(plugins): log = Logger.get_logger("filter_pyblish_plugins") # TODO: Don't use host from 'pyblish.api' but from defined host by us. - # - kept becau on farm is probably used host 'shell' which propably + # - kept because on farm is probably used host 'shell' which probably # affect how settings are applied there host_name = pyblish.api.current_host() project_name = os.environ.get("AYON_PROJECT_NAME") @@ -529,7 +528,7 @@ def filter_instances_for_context_plugin(plugin, context): Args: plugin (pyblish.api.Plugin): Plugin with filters. - context (pyblish.api.Context): Pyblish context with insances. + context (pyblish.api.Context): Pyblish context with instances. Returns: Iterator[pyblish.lib.Instance]: Iteration of valid instances. diff --git a/client/ayon_core/pipeline/thumbnails.py b/client/ayon_core/pipeline/thumbnails.py index 401d95f273..658372888f 100644 --- a/client/ayon_core/pipeline/thumbnails.py +++ b/client/ayon_core/pipeline/thumbnails.py @@ -226,11 +226,26 @@ class _CacheItems: 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. + 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: 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. Returns: @@ -251,7 +266,7 @@ def get_thumbnail_path(project_name, thumbnail_id): # 'get_thumbnail_by_id' did not return output of # 'ServerAPI' method. 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: return _CacheItems.thumbnails_cache.store_thumbnail( diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index c1633e414e..57c69ef2b2 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -39,6 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin): "blender", "houdini", "max", + "circuit", ] audio_product_name = "audioMain" diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 8e8764fc33..3f7c2f4cba 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -54,7 +54,8 @@ class ExtractBurnin(publish.Extractor): "houdini", "max", "blender", - "unreal" + "unreal", + "circuit", ] optional = True diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 7c38b0453b..df87abba91 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -91,7 +91,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "webpublisher", "aftereffects", "flame", - "unreal" + "unreal", + "circuit", ] # Supported extensions @@ -196,7 +197,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ).format(repre_name)) continue - input_ext = repre["ext"] + input_ext = repre["ext"].lower() if input_ext.startswith("."): input_ext = input_ext[1:] diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index da429c1cd2..7062a0a591 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -39,7 +39,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "nuke", "aftereffects", "unreal", - "houdini" + "houdini", + "circuit", ] enabled = False diff --git a/client/ayon_core/plugins/publish/integrate_attach_reviewable.py b/client/ayon_core/plugins/publish/integrate_attach_reviewable.py new file mode 100644 index 0000000000..b98d8d28fe --- /dev/null +++ b/client/ayon_core/plugins/publish/integrate_attach_reviewable.py @@ -0,0 +1,138 @@ +import copy +import pyblish.api +from typing import List + +from ayon_core.lib import EnumDef +from ayon_core.pipeline import OptionalPyblishPluginMixin + + +class AttachReviewables( + pyblish.api.InstancePlugin, OptionalPyblishPluginMixin +): + """Attach reviewable to other instances + + This pre-integrator plugin allows instances to be 'attached to' other + instances by moving all its representations over to the other instance. + Even though this technically could work for any representation the current + intent is to use for reviewables only, like e.g. `review` or `render` + product type. + + When the reviewable is attached to another instance, the instance itself + will not be published as a separate entity. Instead, the representations + will be copied/moved to the instances it is attached to. + """ + + families = ["render", "review"] + order = pyblish.api.IntegratorOrder - 0.499 + label = "Attach reviewables" + + settings_category = "core" + + def process(self, instance): + # TODO: Support farm. + # If instance is being submitted to the farm we should pass through + # the 'attached reviewables' metadata to the farm job + # TODO: Reviewable frame range and resolutions + # Because we are attaching the data to another instance, how do we + # correctly propagate the resolution + frame rate to the other + # instance? Do we even need to? + # TODO: If this were to attach 'renders' to another instance that would + # mean there wouldn't necessarily be a render publish separate as a + # result. Is that correct expected behavior? + attr_values = self.get_attr_values_from_data(instance.data) + attach_to = attr_values.get("attach", []) + if not attach_to: + self.log.debug( + "Reviewable is not set to attach to another instance." + ) + return + + attach_instances: List[pyblish.api.Instance] = [] + for attach_instance_id in attach_to: + # Find the `pyblish.api.Instance` matching the `CreatedInstance.id` + # in the `attach_to` list + attach_instance = next( + ( + _inst + for _inst in instance.context + if _inst.data.get("instance_id") == attach_instance_id + ), + None, + ) + if attach_instance is None: + continue + + # Skip inactive instances + if not attach_instance.data.get("active", True): + continue + + # For now do not support attaching to 'farm' instances until we + # can pass the 'attaching' on to the farm jobs. + if attach_instance.data.get("farm"): + self.log.warning( + "Attaching to farm instances is not supported yet." + ) + continue + + attach_instances.append(attach_instance) + + instances_names = ", ".join( + instance.name for instance in attach_instances + ) + self.log.info( + f"Attaching reviewable to other instances: {instances_names}" + ) + + # Copy the representations of this reviewable instance to the other + # instance + representations = instance.data.get("representations", []) + for attach_instance in attach_instances: + self.log.info(f"Attaching to {attach_instance.name}") + attach_instance.data.setdefault("representations", []).extend( + copy.deepcopy(representations) + ) + + # Delete representations on the reviewable instance itself + for repre in representations: + self.log.debug( + "Marking representation as deleted because it was " + f"attached to other instances instead: {repre}" + ) + repre.setdefault("tags", []).append("delete") + + # Stop integrator from trying to integrate this instance + if attach_to: + instance.data["integrate"] = False + + @classmethod + def get_attr_defs_for_instance(cls, create_context, instance): + # TODO: Check if instance is actually a 'reviewable' + # Filtering of instance, if needed, can be customized + if not cls.instance_matches_plugin_families(instance): + return [] + + items = [] + for other_instance in create_context.instances: + if other_instance == instance: + continue + + # Do not allow attaching to other reviewable instances + if other_instance.data["productType"] in cls.families: + continue + + items.append( + { + "label": other_instance.label, + "value": str(other_instance.id), + } + ) + + return [ + EnumDef( + "attach", + label="Attach reviewable", + multiselection=True, + items=items, + tooltip="Attach this reviewable to another instance", + ) + ] diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index edff8471b0..891eb80960 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -227,6 +227,9 @@ class HierarchyModel(object): self._tasks_by_id = NestedCacheItem( levels=2, default_factory=dict, lifetime=self.lifetime) + self._entity_ids_by_assignee = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) + self._folders_refreshing = set() self._tasks_refreshing = set() self._controller = controller @@ -238,6 +241,8 @@ class HierarchyModel(object): self._task_items.reset() self._tasks_by_id.reset() + self._entity_ids_by_assignee.reset() + def refresh_project(self, project_name): """Force to refresh folder items for a project. @@ -461,6 +466,54 @@ class HierarchyModel(object): output = self.get_task_entities(project_name, {task_id}) return output[task_id] + def get_entity_ids_for_assignees( + self, project_name: str, assignees: list[str] + ): + folder_ids = set() + task_ids = set() + output = { + "folder_ids": folder_ids, + "task_ids": task_ids, + } + assignees = set(assignees) + for assignee in tuple(assignees): + cache = self._entity_ids_by_assignee[project_name][assignee] + if cache.is_valid: + assignees.discard(assignee) + assignee_data = cache.get_data() + folder_ids.update(assignee_data["folder_ids"]) + task_ids.update(assignee_data["task_ids"]) + + if not assignees: + return output + + tasks = ayon_api.get_tasks( + project_name, + assignees_all=assignees, + fields={"id", "folderId", "assignees"}, + ) + tasks_assignee = {} + for task in tasks: + folder_ids.add(task["folderId"]) + task_ids.add(task["id"]) + for assignee in task["assignees"]: + tasks_assignee.setdefault(assignee, []).append(task) + + for assignee, tasks in tasks_assignee.items(): + cache = self._entity_ids_by_assignee[project_name][assignee] + assignee_folder_ids = set() + assignee_task_ids = set() + assignee_data = { + "folder_ids": assignee_folder_ids, + "task_ids": assignee_task_ids, + } + for task in tasks: + assignee_folder_ids.add(task["folderId"]) + assignee_task_ids.add(task["id"]) + cache.update_data(assignee_data) + + return output + @contextlib.contextmanager def _folder_refresh_event_manager(self, project_name, sender): self._folders_refreshing.add(project_name) diff --git a/client/ayon_core/tools/common_models/thumbnails.py b/client/ayon_core/tools/common_models/thumbnails.py index 2fa1e36e5c..5111d0be28 100644 --- a/client/ayon_core/tools/common_models/thumbnails.py +++ b/client/ayon_core/tools/common_models/thumbnails.py @@ -21,8 +21,50 @@ class ThumbnailsModel: self._folders_cache.reset() self._versions_cache.reset() - def get_thumbnail_path(self, project_name, thumbnail_id): - return self._get_thumbnail_path(project_name, thumbnail_id) + def get_thumbnail_paths( + 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): project_cache = self._folders_cache[project_name] @@ -56,7 +98,13 @@ class ThumbnailsModel: output[version_id] = cache.get_data() 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: return None @@ -64,7 +112,12 @@ class ThumbnailsModel: if thumbnail_id in project_cache: 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 return filepath diff --git a/client/ayon_core/tools/launcher/abstract.py b/client/ayon_core/tools/launcher/abstract.py index 63ba4cd717..ea0842f24d 100644 --- a/client/ayon_core/tools/launcher/abstract.py +++ b/client/ayon_core/tools/launcher/abstract.py @@ -160,8 +160,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[FolderItem]: Minimum possible information needed for visualisation of folder hierarchy. - """ + """ pass @abstractmethod @@ -180,8 +180,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[TaskItem]: Minimum possible information needed for visualisation of tasks. - """ + """ pass @abstractmethod @@ -190,8 +190,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected project name. - """ + """ pass @abstractmethod @@ -200,8 +200,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected folder id. - """ + """ pass @abstractmethod @@ -210,8 +210,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected task id. - """ + """ pass @abstractmethod @@ -220,8 +220,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: Union[str, None]: Selected task name. - """ + """ pass @abstractmethod @@ -238,8 +238,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: dict[str, Union[str, None]]: Selected context. - """ + """ pass @abstractmethod @@ -249,8 +249,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Args: project_name (Union[str, None]): Project nameor None if no project is selected. - """ + """ pass @abstractmethod @@ -260,8 +260,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Args: folder_id (Union[str, None]): Folder id or None if no folder is selected. - """ + """ pass @abstractmethod @@ -273,8 +273,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): is selected. task_name (Union[str, None]): Task name or None if no task is selected. - """ + """ pass # Actions @@ -290,8 +290,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Returns: list[ActionItem]: List of action items that should be shown for given context. - """ + """ pass @abstractmethod @@ -303,8 +303,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. action_id (str): Action identifier. - """ + """ pass @abstractmethod @@ -317,10 +317,10 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): project_name (Union[str, None]): Project name. folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. - action_id (Iterable[str]): Action identifiers. + action_ids (Iterable[str]): Action identifiers. enabled (bool): New value of force not open workfile. - """ + """ pass @abstractmethod @@ -340,5 +340,17 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): Triggers 'controller.refresh.actions.started' event at the beginning and 'controller.refresh.actions.finished' at the end. """ - + pass + + @abstractmethod + def get_my_tasks_entity_ids(self, project_name: str): + """Get entity ids for my tasks. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Union[list[str]]]: Folder and task ids. + + """ pass diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index d60e4747e3..45cb2b7945 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,4 +1,4 @@ -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem from ayon_core.settings import get_project_settings from ayon_core.tools.common_models import ProjectsModel, HierarchyModel @@ -6,6 +6,8 @@ from ayon_core.tools.common_models import ProjectsModel, HierarchyModel from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend from .models import LauncherSelectionModel, ActionsModel +NOT_SET = object() + class BaseLauncherController( AbstractLauncherFrontEnd, AbstractLauncherBackend @@ -15,6 +17,8 @@ class BaseLauncherController( self._event_system = None self._log = None + self._username = NOT_SET + self._selection_model = LauncherSelectionModel(self) self._projects_model = ProjectsModel(self) self._hierarchy_model = HierarchyModel(self) @@ -168,5 +172,19 @@ class BaseLauncherController( self._emit_event("controller.refresh.actions.finished") + def get_my_tasks_entity_ids(self, project_name: str): + username = self._get_my_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + + def _get_my_username(self): + if self._username is NOT_SET: + self._username = get_ayon_username() + return self._username + def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index ad48e8ac77..7c34989947 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -5,17 +5,17 @@ from ayon_core.tools.utils import ( PlaceholderLineEdit, SquareButton, RefreshButton, -) -from ayon_core.tools.utils import ( ProjectsCombobox, FoldersWidget, TasksWidget, + NiceCheckbox, ) +from ayon_core.tools.utils.lib import checkstate_int_to_enum class HierarchyPage(QtWidgets.QWidget): def __init__(self, controller, parent): - super(HierarchyPage, self).__init__(parent) + super().__init__(parent) # Header header_widget = QtWidgets.QWidget(self) @@ -43,23 +43,36 @@ class HierarchyPage(QtWidgets.QWidget): ) content_body.setOrientation(QtCore.Qt.Horizontal) - # - Folders widget with filter - folders_wrapper = QtWidgets.QWidget(content_body) + # - filters + filters_widget = QtWidgets.QWidget(self) - folders_filter_text = PlaceholderLineEdit(folders_wrapper) + folders_filter_text = PlaceholderLineEdit(filters_widget) folders_filter_text.setPlaceholderText("Filter folders...") - folders_widget = FoldersWidget(controller, folders_wrapper) + my_tasks_tooltip = ( + "Filter folders and task to only those you are assigned to." + ) + my_tasks_label = QtWidgets.QLabel("My tasks", filters_widget) + my_tasks_label.setToolTip(my_tasks_tooltip) - folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper) - folders_wrapper_layout.setContentsMargins(0, 0, 0, 0) - folders_wrapper_layout.addWidget(folders_filter_text, 0) - folders_wrapper_layout.addWidget(folders_widget, 1) + my_tasks_checkbox = NiceCheckbox(filters_widget) + my_tasks_checkbox.setChecked(False) + my_tasks_checkbox.setToolTip(my_tasks_tooltip) + + filters_layout = QtWidgets.QHBoxLayout(filters_widget) + filters_layout.setContentsMargins(0, 0, 0, 0) + filters_layout.addWidget(folders_filter_text, 1) + filters_layout.addWidget(my_tasks_label, 0) + filters_layout.addWidget(my_tasks_checkbox, 0) + + # - Folders widget + folders_widget = FoldersWidget(controller, content_body) + folders_widget.set_header_visible(True) # - Tasks widget tasks_widget = TasksWidget(controller, content_body) - content_body.addWidget(folders_wrapper) + content_body.addWidget(folders_widget) content_body.addWidget(tasks_widget) content_body.setStretchFactor(0, 100) content_body.setStretchFactor(1, 65) @@ -67,20 +80,27 @@ class HierarchyPage(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(header_widget, 0) + main_layout.addWidget(filters_widget, 0) main_layout.addWidget(content_body, 1) btn_back.clicked.connect(self._on_back_clicked) refresh_btn.clicked.connect(self._on_refresh_clicked) folders_filter_text.textChanged.connect(self._on_filter_text_changed) + my_tasks_checkbox.stateChanged.connect( + self._on_my_tasks_checkbox_state_changed + ) self._is_visible = False self._controller = controller self._btn_back = btn_back self._projects_combobox = projects_combobox + self._my_tasks_checkbox = my_tasks_checkbox self._folders_widget = folders_widget self._tasks_widget = tasks_widget + self._project_name = None + # Post init projects_combobox.set_listen_to_selection_change(self._is_visible) @@ -91,10 +111,14 @@ class HierarchyPage(QtWidgets.QWidget): self._projects_combobox.set_listen_to_selection_change(visible) if visible and project_name: self._projects_combobox.set_selection(project_name) + self._project_name = project_name def refresh(self): self._folders_widget.refresh() self._tasks_widget.refresh() + self._on_my_tasks_checkbox_state_changed( + self._my_tasks_checkbox.checkState() + ) def _on_back_clicked(self): self._controller.set_selected_project(None) @@ -104,3 +128,16 @@ class HierarchyPage(QtWidgets.QWidget): def _on_filter_text_changed(self, text): self._folders_widget.set_name_filter(text) + + def _on_my_tasks_checkbox_state_changed(self, state): + folder_ids = None + task_ids = None + state = checkstate_int_to_enum(state) + if state == QtCore.Qt.Checked: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) diff --git a/client/ayon_core/tools/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py index 2d52a73c38..aa336108ed 100644 --- a/client/ayon_core/tools/launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -17,7 +17,7 @@ class LauncherWindow(QtWidgets.QWidget): page_side_anim_interval = 250 def __init__(self, controller=None, parent=None): - super(LauncherWindow, self).__init__(parent) + super().__init__(parent) if controller is None: controller = BaseLauncherController() @@ -153,14 +153,14 @@ class LauncherWindow(QtWidgets.QWidget): self.resize(520, 740) def showEvent(self, event): - super(LauncherWindow, self).showEvent(event) + super().showEvent(event) self._window_is_active = True if not self._actions_refresh_timer.isActive(): self._actions_refresh_timer.start() self._controller.refresh() def closeEvent(self, event): - super(LauncherWindow, self).closeEvent(event) + super().closeEvent(event) self._window_is_active = False self._actions_refresh_timer.stop() @@ -176,7 +176,7 @@ class LauncherWindow(QtWidgets.QWidget): self._on_actions_refresh_timeout() self._actions_refresh_timer.start() - super(LauncherWindow, self).changeEvent(event) + super().changeEvent(event) def _on_actions_refresh_timeout(self): # Stop timer if widget is not visible diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 26b476de1f..d0d7cd430b 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -733,7 +733,12 @@ class FrontendLoaderController(_BaseLoaderController): pass @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. This method should get a path to a thumbnail based on thumbnail id. @@ -742,10 +747,11 @@ class FrontendLoaderController(_BaseLoaderController): Args: project_name (str): Project name. - thumbnail_id (str): Thumbnail id. + entity_type (str): Entity type. + entity_ids (set[str]): Entity ids. Returns: - Union[str, None]: Thumbnail path or None if not found. + dict[str, Union[str, None]]: Thumbnail path by entity id. """ pass diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 7959a63edb..b3a80b34d4 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -259,9 +259,14 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name, version_ids ) - def get_thumbnail_path(self, project_name, thumbnail_id): - return self._thumbnails_model.get_thumbnail_path( - project_name, thumbnail_id + def get_thumbnail_paths( + self, + 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): diff --git a/client/ayon_core/tools/loader/ui/product_types_combo.py b/client/ayon_core/tools/loader/ui/product_types_combo.py index 91fa52b0e9..525f1cae1b 100644 --- a/client/ayon_core/tools/loader/ui/product_types_combo.py +++ b/client/ayon_core/tools/loader/ui/product_types_combo.py @@ -1,3 +1,4 @@ +from __future__ import annotations from qtpy import QtGui, QtCore from ._multicombobox import ( diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index b846484c39..3d2e15c630 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -501,38 +501,29 @@ class LoaderWindow(QtWidgets.QWidget): self._update_thumbnails() def _update_thumbnails(self): + # TODO make this threaded and show loading animation while running project_name = self._selected_project_name - thumbnail_ids = set() + entity_type = None + entity_ids = set() if self._selected_version_ids: - thumbnail_id_by_entity_id = ( - self._controller.get_version_thumbnail_ids( - project_name, - self._selected_version_ids - ) - ) - thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + entity_ids = set(self._selected_version_ids) + entity_type = "version" elif self._selected_folder_ids: - thumbnail_id_by_entity_id = ( - self._controller.get_folder_thumbnail_ids( - project_name, - self._selected_folder_ids - ) - ) - thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + entity_ids = set(self._selected_folder_ids) + entity_type = "folder" - thumbnail_ids.discard(None) - - if not thumbnail_ids: - self._thumbnails_widget.set_current_thumbnails(None) - 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_path_by_entity_id = self._controller.get_thumbnail_paths( + project_name, entity_type, entity_ids + ) + thumbnail_paths = set(thumbnail_path_by_entity_id.values()) 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): self._refresh_handler.set_project_refreshed() diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index b3b5cebb0e..7b71dd087c 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -1,4 +1,6 @@ +from __future__ import annotations import collections +from typing import Optional from qtpy import QtWidgets, QtGui, QtCore @@ -33,7 +35,10 @@ class FoldersQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersQtModel, self).__init__() + super().__init__() + + self.setColumnCount(1) + self.setHeaderData(0, QtCore.Qt.Horizontal, "Folders") self._controller = controller self._items_by_id = {} @@ -334,6 +339,29 @@ class FoldersQtModel(QtGui.QStandardItemModel): self.refreshed.emit() +class FoldersProxyModel(RecursiveSortFilterProxyModel): + def __init__(self): + super().__init__() + + self._folder_ids_filter = None + + def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): + if self._folder_ids_filter == folder_ids: + return + self._folder_ids_filter = folder_ids + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent_index): + if self._folder_ids_filter is not None: + if not self._folder_ids_filter: + return False + source_index = self.sourceModel().index(row, 0, parent_index) + folder_id = source_index.data(FOLDER_ID_ROLE) + if folder_id not in self._folder_ids_filter: + return False + return super().filterAcceptsRow(row, parent_index) + + class FoldersWidget(QtWidgets.QWidget): """Folders widget. @@ -369,13 +397,13 @@ class FoldersWidget(QtWidgets.QWidget): refreshed = QtCore.Signal() def __init__(self, controller, parent, handle_expected_selection=False): - super(FoldersWidget, self).__init__(parent) + super().__init__(parent) folders_view = TreeView(self) folders_view.setHeaderHidden(True) folders_model = FoldersQtModel(controller) - folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model = FoldersProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -446,6 +474,18 @@ class FoldersWidget(QtWidgets.QWidget): if name: self._folders_view.expandAll() + def set_folder_ids_filter(self, folder_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + folder_ids (list[str]): The list of folder ids. + + """ + self._folders_proxy_model.set_folder_ids_filter(folder_ids) + + def set_header_visible(self, visible: bool): + self._folders_view.setHeaderHidden(not visible) + def refresh(self): """Refresh folders model. diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index fd361493ab..88d8a6c9f5 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -286,6 +286,7 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): self._sort_by_type = True # Disable case sensitivity self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) def _type_sort(self, l_index, r_index): if not self._sort_by_type: diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index 30846e6cda..9118611c23 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -1,3 +1,6 @@ +from __future__ import annotations +from typing import Optional + from qtpy import QtWidgets, QtGui, QtCore from ayon_core.style import ( @@ -343,6 +346,29 @@ class TasksQtModel(QtGui.QStandardItemModel): return self._has_content +class TasksProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self): + super().__init__() + + self._task_ids_filter: Optional[set[str]] = None + + def set_task_ids_filter(self, task_ids: Optional[set[str]]): + if self._task_ids_filter == task_ids: + return + self._task_ids_filter = task_ids + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent_index): + if self._task_ids_filter is not None: + if not self._task_ids_filter: + return False + source_index = self.sourceModel().index(row, 0, parent_index) + task_id = source_index.data(ITEM_ID_ROLE) + if task_id is not None and task_id not in self._task_ids_filter: + return False + return super().filterAcceptsRow(row, parent_index) + + class TasksWidget(QtWidgets.QWidget): """Tasks widget. @@ -364,7 +390,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view.setIndentation(0) tasks_model = TasksQtModel(controller) - tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model = TasksProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) @@ -490,6 +516,15 @@ class TasksWidget(QtWidgets.QWidget): ) return True + def set_task_ids_filter(self, task_ids: Optional[list[str]]): + """Set filter of folder ids. + + Args: + task_ids (list[str]): The list of folder ids. + + """ + self._tasks_proxy_model.set_task_ids_filter(task_ids) + def _on_tasks_refresh_finished(self, event): """Tasks were refreshed in controller. diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 1074b6d4fb..0cd6d68ab3 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -1,4 +1,5 @@ import logging +import math from typing import Optional, List, Set, Any from qtpy import QtWidgets, QtCore, QtGui @@ -410,10 +411,12 @@ class ExpandingTextEdit(QtWidgets.QTextEdit): document = self.document().clone() document.setTextWidth(document_width) - return margins.top() + document.size().height() + margins.bottom() + return math.ceil( + margins.top() + document.size().height() + margins.bottom() + ) def sizeHint(self): - width = super(ExpandingTextEdit, self).sizeHint().width() + width = super().sizeHint().width() return QtCore.QSize(width, self.heightForWidth(width)) diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index b78e987032..152ca33d99 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -1016,6 +1016,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): workdir, filename, template_key, + artist_note, ): """Save current state of workfile to workarea. @@ -1040,6 +1041,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): workdir, filename, template_key, + artist_note, ): """Action to copy published workfile representation to workarea. @@ -1054,12 +1056,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): workdir (str): Workarea directory. filename (str): Workarea filename. template_key (str): Template key. + artist_note (str): Artist note. """ pass @abstractmethod - def duplicate_workfile(self, src_filepath, workdir, filename): + def duplicate_workfile(self, src_filepath, workdir, filename, artist_note): """Duplicate workfile. Workfiles is not opened when done. @@ -1068,6 +1071,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): src_filepath (str): Source workfile path. workdir (str): Destination workdir. filename (str): Destination filename. + artist_note (str): Artist note. """ pass diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index ca97015eea..3a7459da0c 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -554,6 +554,7 @@ class BaseWorkfileController( workdir, filename, template_key, + artist_note, ): self._emit_event("save_as.started") @@ -565,6 +566,7 @@ class BaseWorkfileController( workdir, filename, template_key, + artist_note=artist_note, ) except Exception: failed = True @@ -584,6 +586,7 @@ class BaseWorkfileController( workdir, filename, template_key, + artist_note, ): self._emit_event("copy_representation.started") @@ -595,6 +598,7 @@ class BaseWorkfileController( workdir, filename, template_key, + artist_note, src_filepath=representation_filepath ) except Exception: @@ -608,7 +612,7 @@ class BaseWorkfileController( {"failed": failed}, ) - def duplicate_workfile(self, src_filepath, workdir, filename): + def duplicate_workfile(self, src_filepath, workdir, filename, artist_note): self._emit_event("workfile_duplicate.started") failed = False @@ -701,11 +705,12 @@ class BaseWorkfileController( def _save_as_workfile( self, - folder_id, - task_id, - workdir, - filename, - template_key, + folder_id: str, + task_id: str, + workdir: str, + filename: str, + template_key: str, + artist_note: str, src_filepath=None, ): # Trigger before save event @@ -748,7 +753,11 @@ class BaseWorkfileController( self._host_save_workfile(dst_filepath) # Make sure workfile info exists - self.save_workfile_info(folder_id, task_name, dst_filepath, None) + if not artist_note: + artist_note = None + self.save_workfile_info( + folder_id, task_name, dst_filepath, note=artist_note + ) # Create extra folders create_workdir_extra_folders( diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget.py b/client/ayon_core/tools/workfiles/widgets/files_widget.py index dbe5966c31..f0b74f4289 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget.py @@ -213,7 +213,8 @@ class FilesWidget(QtWidgets.QWidget): self._controller.duplicate_workfile( filepath, result["workdir"], - result["filename"] + result["filename"], + artist_note=result["artist_note"] ) def _on_workarea_browse_clicked(self): @@ -261,6 +262,7 @@ class FilesWidget(QtWidgets.QWidget): result["workdir"], result["filename"], result["template_key"], + artist_note=result["artist_note"] ) def _on_workarea_path_changed(self, event): @@ -313,6 +315,7 @@ class FilesWidget(QtWidgets.QWidget): result["workdir"], result["filename"], result["template_key"], + artist_note=result["artist_note"] ) def _on_save_as_request(self): diff --git a/client/ayon_core/tools/workfiles/widgets/save_as_dialog.py b/client/ayon_core/tools/workfiles/widgets/save_as_dialog.py index 77dac1198a..bddff816fe 100644 --- a/client/ayon_core/tools/workfiles/widgets/save_as_dialog.py +++ b/client/ayon_core/tools/workfiles/widgets/save_as_dialog.py @@ -1,6 +1,6 @@ from qtpy import QtWidgets, QtCore -from ayon_core.tools.utils import PlaceholderLineEdit +from ayon_core.tools.utils import PlaceholderLineEdit, PlaceholderPlainTextEdit class SubversionLineEdit(QtWidgets.QWidget): @@ -143,6 +143,11 @@ class SaveAsDialog(QtWidgets.QDialog): version_layout.addWidget(version_input) version_layout.addWidget(last_version_check) + # Artist note widget + artist_note_input = PlaceholderPlainTextEdit(inputs_widget) + artist_note_input.setPlaceholderText( + "Provide a note about this workfile.") + # Preview widget preview_widget = QtWidgets.QLabel("Preview filename", inputs_widget) preview_widget.setWordWrap(True) @@ -161,6 +166,7 @@ class SaveAsDialog(QtWidgets.QDialog): subversion_label = QtWidgets.QLabel("Subversion:", inputs_widget) extension_label = QtWidgets.QLabel("Extension:", inputs_widget) preview_label = QtWidgets.QLabel("Preview:", inputs_widget) + artist_note_label = QtWidgets.QLabel("Artist Note:", inputs_widget) # Build inputs inputs_layout = QtWidgets.QGridLayout(inputs_widget) @@ -172,6 +178,8 @@ class SaveAsDialog(QtWidgets.QDialog): inputs_layout.addWidget(extension_combobox, 2, 1) inputs_layout.addWidget(preview_label, 3, 0) inputs_layout.addWidget(preview_widget, 3, 1) + inputs_layout.addWidget(artist_note_label, 4, 0, 1, 2) + inputs_layout.addWidget(artist_note_input, 5, 0, 1, 2) # Build layout main_layout = QtWidgets.QVBoxLayout(self) @@ -206,11 +214,13 @@ class SaveAsDialog(QtWidgets.QDialog): self._extension_combobox = extension_combobox self._subversion_input = subversion_input self._preview_widget = preview_widget + self._artist_note_input = artist_note_input self._version_label = version_label self._subversion_label = subversion_label self._extension_label = extension_label self._preview_label = preview_label + self._artist_note_label = artist_note_label # Post init setup @@ -322,6 +332,7 @@ class SaveAsDialog(QtWidgets.QDialog): "folder_id": self._folder_id, "task_id": self._task_id, "template_key": self._template_key, + "artist_note": self._artist_note_input.toPlainText(), } self.close() diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index de5c199428..962ec487a7 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.1.5+dev" +__version__ = "1.1.7+dev" diff --git a/docs/css/custom.css b/docs/css/custom.css new file mode 100644 index 0000000000..28461e6eca --- /dev/null +++ b/docs/css/custom.css @@ -0,0 +1,12 @@ +[data-md-color-scheme="slate"] { + /* simple slate overrides */ + --md-primary-fg-color: hsl(155, 49%, 50%); + --md-accent-fg-color: rgb(93, 200, 156); + --md-typeset-a-color: hsl(155, 49%, 45%) !important; +} +[data-md-color-scheme="default"] { + /* simple default overrides */ + --md-primary-fg-color: hsl(155, 49%, 50%); + --md-accent-fg-color: rgb(93, 200, 156); + --md-typeset-a-color: hsl(155, 49%, 45%) !important; +} diff --git a/docs/img/ay-symbol-blackw-full.png b/docs/img/ay-symbol-blackw-full.png new file mode 100644 index 0000000000..5edda784cc Binary files /dev/null and b/docs/img/ay-symbol-blackw-full.png differ diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000000..62b656e501 Binary files /dev/null and b/docs/img/favicon.ico differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..612c7a5e0d --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000000..f409d45232 --- /dev/null +++ b/docs/license.md @@ -0,0 +1 @@ +--8<-- "LICENSE" diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000..8e4c2663bc --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,71 @@ +site_name: ayon-core +repo_url: https://github.com/ynput/ayon-core + +nav: + - Home: index.md + - License: license.md + +theme: + name: material + palette: + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/toggle-switch-off-outline + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/toggle-switch + name: Switch to dark mode + logo: img/ay-symbol-blackw-full.png + favicon: img/favicon.ico + features: + - navigation.sections + - navigation.path + - navigation.prune + +extra: + version: + provider: mike + +extra_css: [css/custom.css] + +markdown_extensions: + - mdx_gh_links + - pymdownx.snippets + +plugins: + - search + - offline + - mkdocs-autoapi: + autoapi_dir: ./ + autoapi_add_nav_entry: Reference + autoapi_ignore: + - .* + - docs/**/* + - tests/**/* + - tools/**/* + - stubs/**/* # mocha fix + - ./**/pythonrc.py # houdini fix + - .*/**/* + - ./*.py + - mkdocstrings: + handlers: + python: + paths: + - ./ + - client/* + - server/* + - services/* + - minify: + minify_html: true + minify_js: true + minify_css: true + htmlmin_opts: + remove_comments: true + cache_safe: true + - mike + +hooks: + - mkdocs_hooks.py diff --git a/mkdocs_hooks.py b/mkdocs_hooks.py new file mode 100644 index 0000000000..1faa1954f9 --- /dev/null +++ b/mkdocs_hooks.py @@ -0,0 +1,191 @@ +import os +from pathlib import Path +from shutil import rmtree +import json +import glob +import logging + +TMP_FILE = "./missing_init_files.json" +NFILES = [] + +# ----------------------------------------------------------------------------- + + +class ColorFormatter(logging.Formatter): + grey = "\x1b[38;20m" + green = "\x1b[32;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + fmt = ( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s " # noqa + "(%(filename)s:%(lineno)d)" + ) + + FORMATS = { + logging.DEBUG: grey + fmt + reset, + logging.INFO: green + fmt + reset, + logging.WARNING: yellow + fmt + reset, + logging.ERROR: red + fmt + reset, + logging.CRITICAL: bold_red + fmt + reset, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +ch = logging.StreamHandler() +ch.setFormatter(ColorFormatter()) + +logging.basicConfig( + level=logging.INFO, + handlers=[ch], +) + + +# ----------------------------------------------------------------------------- + + +def create_init_file(dirpath, msg): + global NFILES + ini_file = f"{dirpath}/__init__.py" + Path(ini_file).touch() + NFILES.append(ini_file) + logging.info(f"{msg}: created '{ini_file}'") + + +def create_parent_init_files(dirpath: str, rootpath: str, msg: str): + parent_path = dirpath + while parent_path != rootpath: + parent_path = os.path.dirname(parent_path) + parent_init = os.path.join(parent_path, "__init__.py") + if not os.path.exists(parent_init): + create_init_file(parent_path, msg) + else: + break + + +def add_missing_init_files(*roots, msg=""): + """ + This function takes in one or more root directories as arguments and scans + them for Python files without an `__init__.py` file. It generates a JSON + file named `missing_init_files.json` containing the paths of these files. + + Args: + *roots: Variable number of root directories to scan. + + Returns: + None + """ + + for root in roots: + if not os.path.exists(root): + continue + rootpath = os.path.abspath(root) + for dirpath, dirs, files in os.walk(rootpath): + if "__init__.py" in files: + continue + + if "." in dirpath: + continue + + if not glob.glob(os.path.join(dirpath, "*.py")): + continue + + create_init_file(dirpath, msg) + create_parent_init_files(dirpath, rootpath, msg) + + with open(TMP_FILE, "w") as f: + json.dump(NFILES, f) + + +def remove_missing_init_files(msg=""): + """ + This function removes temporary `__init__.py` files created in the + `add_missing_init_files()` function. It reads the paths of these files from + a JSON file named `missing_init_files.json`. + + Args: + None + + Returns: + None + """ + global NFILES + nfiles = [] + if os.path.exists(TMP_FILE): + with open(TMP_FILE, "r") as f: + nfiles = json.load(f) + else: + nfiles = NFILES + + for file in nfiles: + Path(file).unlink() + logging.info(f"{msg}: removed {file}") + + os.remove(TMP_FILE) + NFILES = [] + + +def remove_pychache_dirs(msg=""): + """ + This function walks the current directory and removes all existing + '__pycache__' directories. + + Args: + msg: An optional message to display during the removal process. + + Returns: + None + """ + nremoved = 0 + + for dirpath, dirs, files in os.walk("."): + if "__pycache__" in dirs: + pydir = Path(f"{dirpath}/__pycache__") + rmtree(pydir) + nremoved += 1 + logging.info(f"{msg}: removed '{pydir}'") + + if not nremoved: + logging.info(f"{msg}: no __pycache__ dirs found") + + +# mkdocs hooks ---------------------------------------------------------------- + + +def on_startup(command, dirty): + remove_pychache_dirs(msg="HOOK - on_startup") + + +def on_pre_build(config): + """ + This function is called before the MkDocs build process begins. It adds + temporary `__init__.py` files to directories that do not contain one, to + make sure mkdocs doesn't ignore them. + """ + try: + add_missing_init_files( + "client", + "server", + "services", + msg="HOOK - on_pre_build", + ) + except BaseException as e: + logging.error(e) + remove_missing_init_files( + msg="HOOK - on_post_build: cleaning up on error !" + ) + raise + + +def on_post_build(config): + """ + This function is called after the MkDocs build process ends. It removes + temporary `__init__.py` files that were added in the `on_pre_build()` + function. + """ + remove_missing_init_files(msg="HOOK - on_post_build") diff --git a/package.py b/package.py index 250b77fe52..fe870315cf 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.5+dev" +version = "1.1.7+dev" client_dir = "ayon_core" diff --git a/poetry.lock b/poetry.lock index 2d040a5f91..96e1dc0f4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,6 +49,40 @@ appdirs = ">=1,<2" requests = ">=2.27.1" Unidecode = ">=1.3.0" +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "backrefs" +version = "5.8" +description = "A wrapper around re and regex that adds additional back references." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, + {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, + {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, + {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, + {file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"}, + {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, +] + +[package.extras] +extras = ["regex"] + [[package]] name = "certifi" version = "2025.1.31" @@ -175,6 +209,21 @@ files = [ {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "clique" version = "2.0.0" @@ -217,12 +266,22 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "csscompressor" +version = "0.9.5" +description = "A python port of YUI CSS Compressor" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05"}, +] + [[package]] name = "distlib" version = "0.3.9" @@ -267,6 +326,50 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "1.6.2" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee"}, + {file = "griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[[package]] +name = "htmlmin2" +version = "0.1.13" +description = "An HTML Minifier" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2"}, +] + [[package]] name = "identify" version = "2.6.7" @@ -297,6 +400,53 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.6.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, + {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -309,6 +459,405 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsmin" +version = "3.0.1" +description = "JavaScript minifier." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc"}, +] + +[[package]] +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-checklist" +version = "0.4.4" +description = "Python Markdown extension for task lists with checkboxes" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "markdown-checklist-0.4.4.tar.gz", hash = "sha256:69c93850798b1e01cdc6fcd4a80592d941f669f6451bbf69c71a4ffd1142f849"}, +] + +[package.dependencies] +markdown = "*" + +[package.extras] +coverage = ["coverage", "figleaf"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdx-gh-links" +version = "0.4" +description = "An extension to Python-Markdown which adds support for shorthand links to GitHub users, repositories, issues and commits." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099"}, + {file = "mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686"}, +] + +[package.dependencies] +markdown = ">=3.0.0" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mike" +version = "2.1.3" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a"}, + {file = "mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810"}, +] + +[package.dependencies] +importlib-metadata = "*" +importlib-resources = "*" +jinja2 = ">=2.7" +mkdocs = ">=1.0" +pyparsing = ">=3.0" +pyyaml = ">=5.1" +pyyaml-env-tag = "*" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autoapi" +version = "0.4.0" +description = "MkDocs plugin providing automatic API reference generation" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mkdocs_autoapi-0.4.0-py3-none-any.whl", hash = "sha256:ce2b5e8b4d1df37bd1273a6bce1dded152107e2280cedbfa085cea10edf8531b"}, + {file = "mkdocs_autoapi-0.4.0.tar.gz", hash = "sha256:409c2da8eb297ef51381b066744ac1cdf846220bcc779bdba680fbc5a080df1e"}, +] + +[package.dependencies] +mkdocs = ">=1.4.0" +mkdocstrings = ">=0.19.0" + +[package.extras] +python = ["mkdocstrings[python] (>=0.19.0)"] +python-legacy = ["mkdocstrings[python-legacy] (>=0.19.0)"] +vba = ["mkdocstrings-vba (>=0.0.10)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f"}, + {file = "mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.6.9" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_material-9.6.9-py3-none-any.whl", hash = "sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1"}, + {file = "mkdocs_material-9.6.9.tar.gz", hash = "sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +backrefs = ">=5.7.post1,<6.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocs-minify-plugin" +version = "0.8.0" +description = "An MkDocs plugin to minify HTML, JS or CSS files prior to being written to disk" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d"}, + {file = "mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6"}, +] + +[package.dependencies] +csscompressor = ">=0.9.5" +htmlmin2 = ">=0.1.13" +jsmin = ">=3.0.1" +mkdocs = ">=1.4.1" + +[[package]] +name = "mkdocstrings" +version = "0.29.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d"}, + {file = "mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.6" +mkdocs-autorefs = ">=1.4" +pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=1.16.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.8" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings_python-1.16.8-py3-none-any.whl", hash = "sha256:211b7aaf776cd45578ecb531e5ad0d3a35a8be9101a6bfa10de38a69af9d8fd8"}, + {file = "mkdocstrings_python-1.16.8.tar.gz", hash = "sha256:9453ccae69be103810c1cf6435ce71c8f714ae37fef4d87d16aa92a7c800fe1d"}, +] + +[package.dependencies] +griffe = ">=1.6.2" +mkdocs-autorefs = ">=1.4" +mkdocstrings = ">=0.28.3" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mkdocstrings-shell" +version = "1.0.3" +description = "A shell scripts/libraries handler for mkdocstrings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings_shell-1.0.3-py3-none-any.whl", hash = "sha256:b23ebe43d06c9c19a541548f34d42ee4e4324ae06423eba8a9136e295c67f345"}, + {file = "mkdocstrings_shell-1.0.3.tar.gz", hash = "sha256:3bdea6a1e794a5d0e15d461f33b92e0b9f3b9a1e2c33671d9a2b7d83c761096a"}, +] + +[package.dependencies] +mkdocstrings = ">=0.28.3" +shellman = ">=1.0.2" + [[package]] name = "mock" version = "5.1.0" @@ -400,6 +949,34 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -464,6 +1041,55 @@ files = [ {file = "pyblish_base-1.8.12-py2.py3-none-any.whl", hash = "sha256:2cbe956bfbd4175a2d7d22b344cd345800f4d4437153434ab658fc12646a11e8"}, ] +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.14.3" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, + {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + +[[package]] +name = "pyparsing" +version = "3.2.3" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.3.4" @@ -505,6 +1131,21 @@ pytest = ">=8.3.2" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pyyaml" version = "6.0.2" @@ -568,6 +1209,21 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "requests" version = "2.32.3" @@ -629,6 +1285,35 @@ files = [ {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, ] +[[package]] +name = "shellman" +version = "1.0.2" +description = "Write documentation in comments and render it with templates." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "shellman-1.0.2-py3-none-any.whl", hash = "sha256:f8c960fd2d3785e195f86fcd8f110a8d51a950e759d82c14a5af0bd71b918b3c"}, + {file = "shellman-1.0.2.tar.gz", hash = "sha256:48cba79d6415c0d013ad4dfd2205ed81b0e468795d1886dcda943ac78eaffd38"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +jinja2 = ">=3" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -671,6 +1356,30 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "unidecode" version = "1.3.8" @@ -701,6 +1410,21 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + [[package]] name = "virtualenv" version = "20.29.2" @@ -722,7 +1446,70 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.1" python-versions = ">=3.9.1,<3.10" -content-hash = "0a399d239c49db714c1166c20286fdd5cd62faf12e45ab85833c4d6ea7a04a2a" +content-hash = "24b6215b9c20a4f64f844d3deb121618aef510b1c5ee54242e50305db6c0c4f4" diff --git a/pyproject.toml b/pyproject.toml index 94badd2f1a..86df6535aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.5+dev" +version = "1.1.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md" @@ -29,6 +29,17 @@ attrs = "^25.0.0" pyblish-base = "^1.8.7" clique = "^2.0.0" opentimelineio = "^0.17.0" +tomlkit = "^0.13.2" +requests = "^2.32.3" +mkdocs-material = "^9.6.7" +mkdocs-autoapi = "^0.4.0" +mkdocstrings-python = "^1.16.2" +mkdocs-minify-plugin = "^0.8.0" +markdown-checklist = "^0.4.4" +mdx-gh-links = "^0.4" +pymdown-extensions = "^10.14.3" +mike = "^2.1.3" +mkdocstrings-shell = "^1.0.2" [tool.ruff] diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index c9c66e65d9..39a9c028f9 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -12,6 +12,10 @@ from ayon_server.settings import ( from ayon_server.types import ColorRGBA_uint8 +class EnabledModel(BaseSettingsModel): + enabled: bool = SettingsField(True) + + class ValidateBaseModel(BaseSettingsModel): _isGroup = True enabled: bool = SettingsField(True) @@ -1026,6 +1030,17 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=IntegrateHeroVersionModel, title="Integrate Hero Version" ) + AttachReviewables: EnabledModel = SettingsField( + default_factory=EnabledModel, + title="Attach Reviewables", + description=( + "When enabled, expose an 'Attach Reviewables' attribute on review" + " and render instances in the publisher to allow including the" + " media to be attached to another instance.\n\n" + "If a reviewable is attached to another instance it will not be " + "published as a render/review product of its own." + ) + ) CleanUp: CleanUpModel = SettingsField( default_factory=CleanUpModel, title="Clean Up" @@ -1410,6 +1425,9 @@ DEFAULT_PUBLISH_VALUES = { ], "use_hardlinks": False }, + "AttachReviewables": { + "enabled": True, + }, "CleanUp": { "paterns": [], # codespell:ignore paterns "remove_temp_renders": False