diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 95c8647d45..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "client/ayon_core/hosts/unreal/integration"] - path = client/ayon_core/hosts/unreal/integration - url = https://github.com/ynput/ayon-unreal-plugin.git diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index 88c27db154..e1c04ea0d6 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -35,14 +35,14 @@ AYON addons should contain separated logic of specific kind of implementation, s - addon has more logic when used in a tray - it is possible that addon can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_addons` + - `tray_init` - initialization triggered after `initialize` when used in `TrayAddonsManager` and before `connect_with_addons` - `tray_menu` - add actions to tray widget's menu that represent the addon - `tray_start` - start of addon's login in tray - addon is initialized and connected with other addons - `tray_exit` - addon's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - order of calling is based on implementation this order is how it works with `TrayAddonsManager` - it is recommended to import and use GUI implementation only in these methods -- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` +- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayAddonsManager` to True after `tray_init` - if addon has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index d49358b0d2..b9ecff4233 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -8,7 +8,6 @@ import inspect import logging import threading import collections - from uuid import uuid4 from abc import ABCMeta, abstractmethod @@ -29,32 +28,45 @@ from .interfaces import ( ) # Files that will be always ignored on addons import -IGNORED_FILENAMES = ( +IGNORED_FILENAMES = { "__pycache__", -) +} # Files ignored on addons import from "./ayon_core/modules" -IGNORED_DEFAULT_FILENAMES = ( +IGNORED_DEFAULT_FILENAMES = { "__init__.py", "base.py", "interfaces.py", "click_wrap.py", - "example_addons", - "default_modules", -) -IGNORED_HOSTS_IN_AYON = { - "flame", - "harmony", } -IGNORED_MODULES_IN_AYON = set() # When addon was moved from ayon-core codebase # - this is used to log the missing addon MOVED_ADDON_MILESTONE_VERSIONS = { + "aftereffects": VersionInfo(0, 2, 0), "applications": VersionInfo(0, 2, 0), + "blender": VersionInfo(0, 2, 0), + "celaction": VersionInfo(0, 2, 0), "clockify": VersionInfo(0, 2, 0), + "deadline": VersionInfo(0, 2, 0), + "flame": VersionInfo(0, 2, 0), + "fusion": VersionInfo(0, 2, 0), + "harmony": VersionInfo(0, 2, 0), + "hiero": VersionInfo(0, 2, 0), + "max": VersionInfo(0, 2, 0), + "photoshop": VersionInfo(0, 2, 0), + "timers_manager": VersionInfo(0, 2, 0), + "traypublisher": VersionInfo(0, 2, 0), "tvpaint": VersionInfo(0, 2, 0), + "maya": VersionInfo(0, 2, 0), + "nuke": VersionInfo(0, 2, 0), + "resolve": VersionInfo(0, 2, 0), + "royalrender": VersionInfo(0, 2, 0), + "substancepainter": VersionInfo(0, 2, 0), + "houdini": VersionInfo(0, 3, 0), + "unreal": VersionInfo(0, 2, 0), } + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing AYON addons. @@ -399,95 +411,59 @@ def _load_addons_in_core( ): # Add current directory at first place # - has small differences in import logic - hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts") modules_dir = os.path.join(AYON_CORE_ROOT, "modules") + if not os.path.exists(modules_dir): + log.warning( + f"Could not find path when loading AYON addons \"{modules_dir}\"" + ) + return - ignored_host_names = set(IGNORED_HOSTS_IN_AYON) - ignored_module_dir_filenames = ( - set(IGNORED_DEFAULT_FILENAMES) - | IGNORED_MODULES_IN_AYON - ) + ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES - for dirpath in {hosts_dir, modules_dir}: - if not os.path.exists(dirpath): - log.warning(( - "Could not find path when loading AYON addons \"{}\"" - ).format(dirpath)) + for filename in os.listdir(modules_dir): + # Ignore filenames + if filename in ignored_filenames: continue - is_in_modules_dir = dirpath == modules_dir - if is_in_modules_dir: - ignored_filenames = ignored_module_dir_filenames - else: - ignored_filenames = ignored_host_names + fullpath = os.path.join(modules_dir, filename) + basename, ext = os.path.splitext(filename) - for filename in os.listdir(dirpath): - # Ignore filenames - if filename in IGNORED_FILENAMES or filename in ignored_filenames: + if basename in ignore_addon_names: + continue + + # Validations + if os.path.isdir(fullpath): + # Check existence of init file + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + log.debug(( + "Addon directory does not contain __init__.py" + f" file {fullpath}" + )) continue - fullpath = os.path.join(dirpath, filename) - basename, ext = os.path.splitext(filename) + elif ext != ".py": + continue - if basename in ignore_addon_names: - continue + # TODO add more logic how to define if folder is addon or not + # - check manifest and content of manifest + try: + # Don't import dynamically current directory modules + new_import_str = f"{modules_key}.{basename}" - # Validations - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Addon directory does not contain __init__.py" - " file {}" - ).format(fullpath)) - continue + import_str = f"ayon_core.modules.{basename}" + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) - elif ext not in (".py", ): - continue - - # TODO add more logic how to define if folder is addon or not - # - check manifest and content of manifest - try: - # Don't import dynamically current directory modules - new_import_str = "{}.{}".format(modules_key, basename) - if is_in_modules_dir: - import_str = "ayon_core.modules.{}".format(basename) - default_module = __import__(import_str, fromlist=("", )) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - else: - import_str = "ayon_core.hosts.{}".format(basename) - # Until all hosts are converted to be able use them as - # modules is this error check needed - try: - default_module = __import__( - import_str, fromlist=("", ) - ) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - except Exception: - log.warning( - "Failed to import host folder {}".format(basename), - exc_info=True - ) - - except Exception: - if is_in_modules_dir: - msg = "Failed to import in-core addon '{}'.".format( - basename - ) - else: - msg = "Failed to import addon '{}'.".format(fullpath) - log.error(msg, exc_info=True) + except Exception: + log.error( + f"Failed to import in-core addon '{basename}'.", + exc_info=True + ) def _load_addons(): - # Support to use 'openpype' imports - sys.modules["openpype"] = sys.modules["ayon_core"] - # Key under which will be modules imported in `sys.modules` modules_key = "openpype_modules" @@ -540,6 +516,9 @@ class AYONAddon(object): enabled = True _id = None + # Temporary variable for 'version' property + _missing_version_warned = False + def __init__(self, manager, settings): self.manager = manager @@ -570,6 +549,26 @@ class AYONAddon(object): pass + @property + def version(self): + """Addon version. + + Todo: + Should be abstract property (required). Introduced in + ayon-core 0.3.3 . + + Returns: + str: Addon version as semver compatible string. + + """ + if not self.__class__._missing_version_warned: + self.__class__._missing_version_warned = True + print( + f"DEV WARNING: Addon '{self.name}' does not have" + f" defined version." + ) + return "0.0.0" + def initialize(self, settings): """Initialization of addon attributes. @@ -685,6 +684,30 @@ class OpenPypeAddOn(OpenPypeModule): enabled = True +class _AddonReportInfo: + def __init__( + self, class_name, name, version, report_value_by_label + ): + self.class_name = class_name + self.name = name + self.version = version + self.report_value_by_label = report_value_by_label + + @classmethod + def from_addon(cls, addon, report): + class_name = addon.__class__.__name__ + report_value_by_label = { + label: reported.get(class_name) + for label, reported in report.items() + } + return cls( + addon.__class__.__name__, + addon.name, + addon.version, + report_value_by_label + ) + + class AddonsManager: """Manager of addons that helps to load and prepare them to work. @@ -861,10 +884,6 @@ class AddonsManager: name_alias = getattr(addon, "openpype_alias", None) if name_alias: aliased_names.append((name_alias, addon)) - enabled_str = "X" - if not addon.enabled: - enabled_str = " " - self.log.debug("[{}] {}".format(enabled_str, name)) now = time.time() report[addon.__class__.__name__] = now - prev_start_time @@ -876,6 +895,13 @@ class AddonsManager: exc_info=True ) + for addon_name in sorted(self._addons_by_name.keys()): + addon = self._addons_by_name[addon_name] + enabled_str = "X" if addon.enabled else " " + self.log.debug( + f"[{enabled_str}] {addon.name} ({addon.version})" + ) + for item in aliased_names: name_alias, addon = item if name_alias not in self._addons_by_name: @@ -1164,39 +1190,55 @@ class AddonsManager: available_col_names |= set(addon_names.keys()) # Prepare ordered dictionary for columns - cols = collections.OrderedDict() - # Add addon names to first columnt - cols["Addon name"] = list(sorted( - addon.__class__.__name__ + addons_info = [ + _AddonReportInfo.from_addon(addon, self._report) for addon in self.addons if addon.__class__.__name__ in available_col_names - )) + ] + addons_info.sort(key=lambda x: x.name) + + addon_name_rows = [ + addon_info.name + for addon_info in addons_info + ] + addon_version_rows = [ + addon_info.version + for addon_info in addons_info + ] + # Add total key (as last addon) - cols["Addon name"].append(self._report_total_key) + addon_name_rows.append(self._report_total_key) + addon_version_rows.append(f"({len(addons_info)})") + + cols = collections.OrderedDict() + # Add addon names to first columnt + cols["Addon name"] = addon_name_rows + cols["Version"] = addon_version_rows # Add columns from report + total_by_addon = { + row: 0 + for row in addon_name_rows + } for label in self._report.keys(): - cols[label] = [] - - total_addon_times = {} - for addon_name in cols["Addon name"]: - total_addon_times[addon_name] = 0 - - for label, reported in self._report.items(): - for addon_name in cols["Addon name"]: - col_time = reported.get(addon_name) - if col_time is None: - cols[label].append("N/A") + rows = [] + col_total = 0 + for addon_info in addons_info: + value = addon_info.report_value_by_label.get(label) + if value is None: + rows.append("N/A") continue - cols[label].append("{:.3f}".format(col_time)) - total_addon_times[addon_name] += col_time - + rows.append("{:.3f}".format(value)) + total_by_addon[addon_info.name] += value + col_total += value + total_by_addon[self._report_total_key] += col_total + rows.append("{:.3f}".format(col_total)) + cols[label] = rows # Add to also total column that should sum the row - cols[self._report_total_key] = [] - for addon_name in cols["Addon name"]: - cols[self._report_total_key].append( - "{:.3f}".format(total_addon_times[addon_name]) - ) + cols[self._report_total_key] = [ + "{:.3f}".format(total_by_addon[addon_name]) + for addon_name in cols["Addon name"] + ] # Prepare column widths and total row count # - column width is by @@ -1323,7 +1365,7 @@ class TrayAddonsManager(AddonsManager): self.doubleclick_callback = None def add_doubleclick_callback(self, addon, callback): - """Register doubleclick callbacks on tray icon. + """Register double-click callbacks on tray icon. Currently, there is no way how to determine which is launched. Name of callback can be defined with `doubleclick_callback` attribute. diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index bd47dc1aac..60cf5624b0 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -268,7 +268,7 @@ def main(*args, **kwargs): os.path.join(AYON_CORE_ROOT, "tools"), # add common AYON vendor # (common for multiple Python interpreter versions) - os.path.join(AYON_CORE_ROOT, "vendor", "python", "common") + os.path.join(AYON_CORE_ROOT, "vendor", "python") ] for path in additional_paths: if path not in split_paths: diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 0fb18be687..35b7e294de 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -36,7 +36,7 @@ class Commands: log.warning( "Failed to add cli command for module \"{}\"".format( addon.name - ) + ), exc_info=True ) return click_func @@ -64,9 +64,10 @@ class Commands: get_global_context, ) - # Register target and host + import ayon_api import pyblish.util + # Register target and host if not isinstance(path, str): raise RuntimeError("Path to JSON must be a string.") @@ -86,6 +87,19 @@ class Commands: log = Logger.get_logger("CLI-publish") + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + install_ayon_plugins() manager = AddonsManager() diff --git a/client/ayon_core/hosts/blender/__init__.py b/client/ayon_core/hosts/blender/__init__.py deleted file mode 100644 index 2a6603606a..0000000000 --- a/client/ayon_core/hosts/blender/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import BlenderAddon - - -__all__ = ( - "BlenderAddon", -) diff --git a/client/ayon_core/hosts/flame/__init__.py b/client/ayon_core/hosts/flame/__init__.py deleted file mode 100644 index b45f107747..0000000000 --- a/client/ayon_core/hosts/flame/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .addon import ( - HOST_DIR, - FlameAddon, -) - - -__all__ = ( - "HOST_DIR", - "FlameAddon", -) diff --git a/client/ayon_core/hosts/hiero/__init__.py b/client/ayon_core/hosts/hiero/__init__.py deleted file mode 100644 index e6744d5aec..0000000000 --- a/client/ayon_core/hosts/hiero/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .addon import ( - HIERO_ROOT_DIR, - HieroAddon, -) - - -__all__ = ( - "HIERO_ROOT_DIR", - "HieroAddon", -) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_remove_marked.py b/client/ayon_core/hosts/maya/plugins/publish/collect_remove_marked.py deleted file mode 100644 index 69e69f6630..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_remove_marked.py +++ /dev/null @@ -1,24 +0,0 @@ -import pyblish.api - - -class CollectRemoveMarked(pyblish.api.ContextPlugin): - """Remove marked data - - Remove instances that have 'remove' in their instance.data - - """ - - order = pyblish.api.CollectorOrder + 0.499 - label = 'Remove Marked Instances' - - def process(self, context): - - self.log.debug(context) - # make ftrack publishable - instances_to_remove = [] - for instance in context: - if instance.data.get('remove'): - instances_to_remove.append(instance) - - for instance in instances_to_remove: - context.remove(instance) diff --git a/client/ayon_core/hosts/nuke/vendor/google/protobuf/util/__init__.py b/client/ayon_core/hosts/nuke/vendor/google/protobuf/util/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/hosts/resolve/__init__.py b/client/ayon_core/hosts/resolve/__init__.py deleted file mode 100644 index b4a994bbaa..0000000000 --- a/client/ayon_core/hosts/resolve/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import ResolveAddon - - -__all__ = ( - "ResolveAddon", -) diff --git a/client/ayon_core/hosts/resolve/otio/__init__.py b/client/ayon_core/hosts/resolve/otio/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/hosts/unreal/README.md b/client/ayon_core/hosts/unreal/README.md deleted file mode 100644 index d131105659..0000000000 --- a/client/ayon_core/hosts/unreal/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Unreal Integration - -Supported Unreal Engine version is 4.26+ (mainly because of major Python changes done there). - -### Project naming -Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are -invalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` -will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. -Longer names will issue warning in Unreal Editor that there might be possible side effects. diff --git a/client/ayon_core/hosts/unreal/__init__.py b/client/ayon_core/hosts/unreal/__init__.py deleted file mode 100644 index 42dd8f0ac4..0000000000 --- a/client/ayon_core/hosts/unreal/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import UnrealAddon - - -__all__ = ( - "UnrealAddon", -) diff --git a/client/ayon_core/hosts/unreal/addon.py b/client/ayon_core/hosts/unreal/addon.py deleted file mode 100644 index c65490bd8c..0000000000 --- a/client/ayon_core/hosts/unreal/addon.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -import re -from ayon_core.addon import AYONAddon, IHostAddon - -UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class UnrealAddon(AYONAddon, IHostAddon): - name = "unreal" - host_name = "unreal" - - def get_global_environments(self): - return { - "AYON_UNREAL_ROOT": UNREAL_ROOT_DIR, - } - - def add_implementation_envs(self, env, app): - """Modify environments to contain all required for implementation.""" - # Set AYON_UNREAL_PLUGIN required for Unreal implementation - # Imports are in this method for Python 2 compatiblity of an addon - from pathlib import Path - - from .lib import get_compatible_integration - - from ayon_core.tools.utils import show_message_dialog - - pattern = re.compile(r'^\d+-\d+$') - - if not pattern.match(app.name): - msg = ( - "Unreal application key in the settings must be in format" - "'5-0' or '5-1'" - ) - show_message_dialog( - parent=None, - title="Unreal application name format", - message=msg, - level="critical") - raise ValueError(msg) - - ue_version = app.name.replace("-", ".") - unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" - ) - if not Path(unreal_plugin_path).exists(): - compatible_versions = get_compatible_integration( - ue_version, Path(UNREAL_ROOT_DIR) / "integration" - ) - if compatible_versions: - unreal_plugin_path = compatible_versions[-1] / "Ayon" - unreal_plugin_path = unreal_plugin_path.as_posix() - - if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: - env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path - - # Set default environments if are not set via settings - defaults = { - "AYON_LOG_NO_COLORS": "1", - "UE_PYTHONPATH": os.environ.get("PYTHONPATH", ""), - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - def get_launch_hook_paths(self, app): - if app.host_name != self.host_name: - return [] - return [ - os.path.join(UNREAL_ROOT_DIR, "hooks") - ] - - def get_workfile_extensions(self): - return [".uproject"] diff --git a/client/ayon_core/hosts/unreal/api/__init__.py b/client/ayon_core/hosts/unreal/api/__init__.py deleted file mode 100644 index 7e7f839f27..0000000000 --- a/client/ayon_core/hosts/unreal/api/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unreal Editor Ayon host API.""" - -from .plugin import ( - UnrealActorCreator, - UnrealAssetCreator, - Loader -) - -from .pipeline import ( - install, - uninstall, - ls, - publish, - containerise, - show_creator, - show_loader, - show_publisher, - show_manager, - show_experimental_tools, - show_tools_dialog, - show_tools_popup, - instantiate, - UnrealHost, - set_sequence_hierarchy, - generate_sequence, - maintained_selection -) - -__all__ = [ - "UnrealActorCreator", - "UnrealAssetCreator", - "Loader", - "install", - "uninstall", - "ls", - "publish", - "containerise", - "show_creator", - "show_loader", - "show_publisher", - "show_manager", - "show_experimental_tools", - "show_tools_dialog", - "show_tools_popup", - "instantiate", - "UnrealHost", - "set_sequence_hierarchy", - "generate_sequence", - "maintained_selection" -] diff --git a/client/ayon_core/hosts/unreal/api/helpers.py b/client/ayon_core/hosts/unreal/api/helpers.py deleted file mode 100644 index e9ab3fb4c5..0000000000 --- a/client/ayon_core/hosts/unreal/api/helpers.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal # noqa - - -class AyonUnrealException(Exception): - pass - - -@unreal.uclass() -class AyonHelpers(unreal.AyonLib): - """Class wrapping some useful functions for Ayon. - - This class is extending native BP class in Ayon Integration Plugin. - - """ - - @unreal.ufunction(params=[str, unreal.LinearColor, bool]) - def set_folder_color(self, path: str, color: unreal.LinearColor) -> None: - """Set color on folder in Content Browser. - - This method sets color on folder in Content Browser. Unfortunately - there is no way to refresh Content Browser so new color isn't applied - immediately. They are saved to config file and appears correctly - only after Editor is restarted. - - Args: - path (str): Path to folder - color (:class:`unreal.LinearColor`): Color of the folder - - Example: - - AyonHelpers().set_folder_color( - "/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0) - ) - - Note: - This will take effect only after Editor is restarted. I couldn't - find a way to refresh it. Also, this saves the color definition - into the project config, binding this path with color. So if you - delete this path and later re-create, it will set this color - again. - - """ - self.c_set_folder_color(path, color, False) diff --git a/client/ayon_core/hosts/unreal/api/pipeline.py b/client/ayon_core/hosts/unreal/api/pipeline.py deleted file mode 100644 index a60564d5b0..0000000000 --- a/client/ayon_core/hosts/unreal/api/pipeline.py +++ /dev/null @@ -1,804 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import json -import logging -from typing import List -from contextlib import contextmanager -import time - -import semver -import pyblish.api -import ayon_api - -from ayon_core.pipeline import ( - register_loader_plugin_path, - register_creator_plugin_path, - register_inventory_action_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, - deregister_inventory_action_path, - AYON_CONTAINER_ID, - get_current_project_name, -) -from ayon_core.tools.utils import host_tools -import ayon_core.hosts.unreal -from ayon_core.host import HostBase, ILoadHost, IPublishHost - -import unreal # noqa - -# Rename to Ayon once parent module renames -logger = logging.getLogger("ayon_core.hosts.unreal") - -AYON_CONTAINERS = "AyonContainers" -AYON_ASSET_DIR = "/Game/Ayon/Assets" -CONTEXT_CONTAINER = "Ayon/context.json" -UNREAL_VERSION = semver.VersionInfo( - *os.getenv("AYON_UNREAL_VERSION").split(".") -) - -HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.unreal.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - - -class UnrealHost(HostBase, ILoadHost, IPublishHost): - """Unreal host implementation. - - For some time this class will re-use functions from module based - implementation for backwards compatibility of older unreal projects. - """ - - name = "unreal" - - def install(self): - install() - - def get_containers(self): - return ls() - - @staticmethod - def show_tools_popup(): - """Show tools popup with actions leading to show other tools.""" - show_tools_popup() - - @staticmethod - def show_tools_dialog(): - """Show tools dialog with actions leading to show other tools.""" - show_tools_dialog() - - def update_context_data(self, data, changes): - content_path = unreal.Paths.project_content_dir() - op_ctx = content_path + CONTEXT_CONTAINER - attempts = 3 - for i in range(attempts): - try: - with open(op_ctx, "w+") as f: - json.dump(data, f) - break - except IOError as e: - if i == attempts - 1: - raise Exception( - "Failed to write context data. Aborting.") from e - unreal.log_warning("Failed to write context data. Retrying...") - i += 1 - time.sleep(3) - continue - - def get_context_data(self): - content_path = unreal.Paths.project_content_dir() - op_ctx = content_path + CONTEXT_CONTAINER - if not os.path.isfile(op_ctx): - return {} - with open(op_ctx, "r") as fp: - data = json.load(fp) - return data - - -def install(): - """Install Unreal configuration for AYON.""" - print("-=" * 40) - logo = '''. -. - · - │ - ·∙/ - ·-∙•∙-· - / \\ /∙· / \\ - ∙ \\ │ / ∙ - \\ \\ · / / - \\\\ ∙ ∙ // - \\\\/ \\// - ___ - │ │ - │ │ - │ │ - │___│ - -· - - ·-─═─-∙ A Y O N ∙-─═─-· - by YNPUT -. -''' - print(logo) - print("installing Ayon for Unreal ...") - print("-=" * 40) - logger.info("installing Ayon for Unreal") - pyblish.api.register_host("unreal") - pyblish.api.register_plugin_path(str(PUBLISH_PATH)) - register_loader_plugin_path(str(LOAD_PATH)) - register_creator_plugin_path(str(CREATE_PATH)) - register_inventory_action_path(str(INVENTORY_PATH)) - _register_callbacks() - _register_events() - - -def uninstall(): - """Uninstall Unreal configuration for Ayon.""" - pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) - deregister_loader_plugin_path(str(LOAD_PATH)) - deregister_creator_plugin_path(str(CREATE_PATH)) - deregister_inventory_action_path(str(INVENTORY_PATH)) - - -def _register_callbacks(): - """ - TODO: Implement callbacks if supported by UE - """ - pass - - -def _register_events(): - """ - TODO: Implement callbacks if supported by UE - """ - pass - - -def ls(): - """List all containers. - - List all found in *Content Manager* of Unreal and return - metadata from them. Adding `objectName` to set. - - """ - ar = unreal.AssetRegistryHelpers.get_asset_registry() - # UE 5.1 changed how class name is specified - class_name = ["/Script/Ayon", "AyonAssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AyonAssetContainer" # noqa - ayon_containers = ar.get_assets_by_class(class_name, True) - - # get_asset_by_class returns AssetData. To get all metadata we need to - # load asset. get_tag_values() work only on metadata registered in - # Asset Registry Project settings (and there is no way to set it with - # python short of editing ini configuration file). - for asset_data in ayon_containers: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - yield cast_map_to_str_dict(data) - - -def ls_inst(): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - # UE 5.1 changed how class name is specified - class_name = [ - "/Script/Ayon", - "AyonPublishInstance" - ] if ( - UNREAL_VERSION.major == 5 - and UNREAL_VERSION.minor > 0 - ) else "AyonPublishInstance" # noqa - instances = ar.get_assets_by_class(class_name, True) - - # get_asset_by_class returns AssetData. To get all metadata we need to - # load asset. get_tag_values() work only on metadata registered in - # Asset Registry Project settings (and there is no way to set it with - # python short of editing ini configuration file). - for asset_data in instances: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - yield cast_map_to_str_dict(data) - - -def parse_container(container): - """To get data from container, AyonAssetContainer must be loaded. - - Args: - container(str): path to container - - Returns: - dict: metadata stored on container - """ - asset = unreal.EditorAssetLibrary.load_asset(container) - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset.get_name() - data = cast_map_to_str_dict(data) - - return data - - -def publish(): - """Shorthand to publish from within host.""" - import pyblish.util - - return pyblish.util.publish() - - -def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): - """Bundles *nodes* (assets) into a *container* and add metadata to it. - - Unreal doesn't support *groups* of assets that you can add metadata to. - But it does support folders that helps to organize asset. Unfortunately - those folders are just that - you cannot add any additional information - to them. Ayon Integration Plugin is providing way out - Implementing - `AssetContainer` Blueprint class. This class when added to folder can - handle metadata on it using standard - :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and - :func:`unreal.EditorAssetLibrary.get_metadata_tag_values()`. It also - stores and monitor all changes in assets in path where it resides. List of - those assets is available as `assets` property. - - This is list of strings starting with asset type and ending with its path: - `Material /Game/Ayon/Test/TestMaterial.TestMaterial` - - """ - # 1 - create directory for container - root = "/Game" - container_name = f"{name}{suffix}" - new_name = move_assets_to_path(root, container_name, nodes) - - # 2 - create Asset Container there - path = f"{root}/{new_name}" - create_container(container=container_name, path=path) - - namespace = path - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "name": new_name, - "namespace": namespace, - "loader": str(loader), - "representation": context["representation"]["id"], - } - # 3 - imprint data - imprint(f"{path}/{container_name}", data) - return path - - -def instantiate(root, name, data, assets=None, suffix="_INS"): - """Bundles *nodes* into *container*. - - Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `AyonPublishInstance` class asset is - created and imprinted with metadata. - - This can then be collected for publishing by Pyblish for example. - - Args: - root (str): root path where to create instance container - name (str): name of the container - data (dict): data to imprint on container - assets (list of str): list of asset paths to include in publish - instance - suffix (str): suffix string to append to instance name - - """ - container_name = f"{name}{suffix}" - - # if we specify assets, create new folder and move them there. If not, - # just create empty folder - if assets: - new_name = move_assets_to_path(root, container_name, assets) - else: - new_name = create_folder(root, name) - - path = f"{root}/{new_name}" - create_publish_instance(instance=container_name, path=path) - - imprint(f"{path}/{container_name}", data) - - -def imprint(node, data): - loaded_asset = unreal.EditorAssetLibrary.load_asset(node) - for key, value in data.items(): - # Support values evaluated at imprint - if callable(value): - value = value() - # Unreal doesn't support NoneType in metadata values - if value is None: - value = "" - unreal.EditorAssetLibrary.set_metadata_tag( - loaded_asset, key, str(value) - ) - - with unreal.ScopedEditorTransaction("Ayon containerising"): - unreal.EditorAssetLibrary.save_asset(node) - - -def show_tools_popup(): - """Show popup with tools. - - Popup will disappear on click or losing focus. - """ - from ayon_core.hosts.unreal.api import tools_ui - - tools_ui.show_tools_popup() - - -def show_tools_dialog(): - """Show dialog with tools. - - Dialog will stay visible. - """ - from ayon_core.hosts.unreal.api import tools_ui - - tools_ui.show_tools_dialog() - - -def show_creator(): - host_tools.show_creator() - - -def show_loader(): - host_tools.show_loader(use_context=True) - - -def show_publisher(): - host_tools.show_publish() - - -def show_manager(): - host_tools.show_scene_inventory() - - -def show_experimental_tools(): - host_tools.show_experimental_tools_dialog() - - -def create_folder(root: str, name: str) -> str: - """Create new folder. - - If folder exists, append number at the end and try again, incrementing - if needed. - - Args: - root (str): path root - name (str): folder name - - Returns: - str: folder name - - Example: - >>> create_folder("/Game/Foo") - /Game/Foo - >>> create_folder("/Game/Foo") - /Game/Foo1 - - """ - eal = unreal.EditorAssetLibrary - index = 1 - while True: - if eal.does_directory_exist(f"{root}/{name}"): - name = f"{name}{index}" - index += 1 - else: - eal.make_directory(f"{root}/{name}") - break - - return name - - -def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: - """Moving (renaming) list of asset paths to new destination. - - Args: - root (str): root of the path (eg. `/Game`) - name (str): name of destination directory (eg. `Foo` ) - assets (list of str): list of asset paths - - Returns: - str: folder name - - Example: - This will get paths of all assets under `/Game/Test` and move them - to `/Game/NewTest`. If `/Game/NewTest` already exists, then resulting - path will be `/Game/NewTest1` - - >>> assets = unreal.EditorAssetLibrary.list_assets("/Game/Test") - >>> move_assets_to_path("/Game", "NewTest", assets) - NewTest - - """ - eal = unreal.EditorAssetLibrary - name = create_folder(root, name) - - unreal.log(assets) - for asset in assets: - loaded = eal.load_asset(asset) - eal.rename_asset(asset, f"{root}/{name}/{loaded.get_name()}") - - return name - - -def create_container(container: str, path: str) -> unreal.Object: - """Helper function to create Asset Container class on given path. - - This Asset Class helps to mark given path as Container - and enable asset version control on it. - - Args: - container (str): Asset Container name - path (str): Path where to create Asset Container. This path should - point into container folder - - Returns: - :class:`unreal.Object`: instance of created asset - - Example: - - create_container( - "/Game/modelingFooCharacter_CON", - "modelingFooCharacter_CON" - ) - - """ - factory = unreal.AyonAssetContainerFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - - return tools.create_asset(container, path, None, factory) - - -def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create Ayon Publish Instance on given path. - - This behaves similarly as :func:`create_ayon_container`. - - Args: - path (str): Path where to create Publish Instance. - This path should point into container folder - instance (str): Publish Instance name - - Returns: - :class:`unreal.Object`: instance of created asset - - Example: - - create_publish_instance( - "/Game/modelingFooCharacter_INST", - "modelingFooCharacter_INST" - ) - - """ - factory = unreal.AyonPublishInstanceFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - return tools.create_asset(instance, path, None, factory) - - -def cast_map_to_str_dict(umap) -> dict: - """Cast Unreal Map to dict. - - Helper function to cast Unreal Map object to plain old python - dict. This will also cast values and keys to str. Useful for - metadata dicts. - - Args: - umap: Unreal Map object - - Returns: - dict - - """ - return {str(key): str(value) for (key, value) in umap.items()} - - -def get_subsequences(sequence: unreal.LevelSequence): - """Get list of subsequences from sequence. - - Args: - sequence (unreal.LevelSequence): Sequence - - Returns: - list(unreal.LevelSequence): List of subsequences - - """ - tracks = sequence.get_master_tracks() - subscene_track = next( - ( - t - for t in tracks - if t.get_class() == unreal.MovieSceneSubTrack.static_class() - ), - None, - ) - if subscene_track is not None and subscene_track.get_sections(): - return subscene_track.get_sections() - return [] - - -def set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths -): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - - -def generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = get_current_project_name() - # TODO Fix this does not return folder path - folder_path = h_dir.split('/')[-1], - folder_entity = ayon_api.get_folder_by_path( - project_name, - folder_path, - fields={"id", "attrib.fps"} - ) - - start_frames = [] - end_frames = [] - - elements = list(ayon_api.get_folders( - project_name, - parent_ids=[folder_entity["id"]], - fields={"id", "attrib.clipIn", "attrib.clipOut"} - )) - for e in elements: - start_frames.append(e["attrib"].get("clipIn")) - end_frames.append(e["attrib"].get("clipOut")) - - elements.extend(ayon_api.get_folders( - project_name, - parent_ids=[e["id"]], - fields={"id", "attrib.clipIn", "attrib.clipOut"} - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - fps = folder_entity["attrib"].get("fps") - - sequence.set_display_rate( - unreal.FrameRate(fps, 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - sequence.set_work_range_start(min_frame / fps) - sequence.set_work_range_end(max_frame / fps) - sequence.set_view_range_start(min_frame / fps) - sequence.set_view_range_end(max_frame / fps) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - - -def _get_comps_and_assets( - component_class, asset_class, old_assets, new_assets, selected -): - eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) - - components = [] - if selected: - sel_actors = eas.get_selected_level_actors() - for actor in sel_actors: - comps = actor.get_components_by_class(component_class) - components.extend(comps) - else: - comps = eas.get_all_level_actors_components() - components = [ - c for c in comps if isinstance(c, component_class) - ] - - # Get all the static meshes among the old assets in a dictionary with - # the name as key - selected_old_assets = {} - for a in old_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, asset_class): - selected_old_assets[asset.get_name()] = asset - - # Get all the static meshes among the new assets in a dictionary with - # the name as key - selected_new_assets = {} - for a in new_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, asset_class): - selected_new_assets[asset.get_name()] = asset - - return components, selected_old_assets, selected_new_assets - - -def replace_static_mesh_actors(old_assets, new_assets, selected): - smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - - static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( - unreal.StaticMeshComponent, - unreal.StaticMesh, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_meshes.items(): - new_mesh = new_meshes.get(old_name) - - if not new_mesh: - continue - - smes.replace_mesh_components_meshes( - static_mesh_comps, old_mesh, new_mesh) - - -def replace_skeletal_mesh_actors(old_assets, new_assets, selected): - skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( - unreal.SkeletalMeshComponent, - unreal.SkeletalMesh, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_meshes.items(): - new_mesh = new_meshes.get(old_name) - - if not new_mesh: - continue - - for comp in skeletal_mesh_comps: - if comp.get_skeletal_mesh_asset() == old_mesh: - comp.set_skeletal_mesh_asset(new_mesh) - - -def replace_geometry_cache_actors(old_assets, new_assets, selected): - geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets( - unreal.GeometryCacheComponent, - unreal.GeometryCache, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_caches.items(): - new_mesh = new_caches.get(old_name) - - if not new_mesh: - continue - - for comp in geometry_cache_comps: - if comp.get_editor_property("geometry_cache") == old_mesh: - comp.set_geometry_cache(new_mesh) - - -def delete_asset_if_unused(container, asset_content): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - references = set() - - for asset_path in asset_content: - asset = ar.get_asset_by_object_path(asset_path) - refs = ar.get_referencers( - asset.package_name, - unreal.AssetRegistryDependencyOptions( - include_soft_package_references=False, - include_hard_package_references=True, - include_searchable_names=False, - include_soft_management_references=False, - include_hard_management_references=False - )) - if not refs: - continue - references = references.union(set(refs)) - - # Filter out references that are in the Temp folder - cleaned_references = { - ref for ref in references if not str(ref).startswith("/Temp/")} - - # Check which of the references are Levels - for ref in cleaned_references: - loaded_asset = unreal.EditorAssetLibrary.load_asset(ref) - if isinstance(loaded_asset, unreal.World): - # If there is at least a level, we can stop, we don't want to - # delete the container - return - - unreal.log("Previous version unused, deleting...") - - # No levels, delete the asset - unreal.EditorAssetLibrary.delete_directory(container["namespace"]) - - -@contextmanager -def maintained_selection(): - """Stub to be either implemented or replaced. - - This is needed for old publisher implementation, but - it is not supported (yet) in UE. - """ - try: - yield - finally: - pass diff --git a/client/ayon_core/hosts/unreal/api/plugin.py b/client/ayon_core/hosts/unreal/api/plugin.py deleted file mode 100644 index f31c7c46b9..0000000000 --- a/client/ayon_core/hosts/unreal/api/plugin.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -import ast -import collections -import sys -import six -from abc import ( - ABC, - ABCMeta, -) - -import unreal - -from .pipeline import ( - create_publish_instance, - imprint, - ls_inst, - UNREAL_VERSION -) -from ayon_core.lib import ( - BoolDef, - UILabelDef -) -from ayon_core.pipeline import ( - Creator, - LoaderPlugin, - CreatorError, - CreatedInstance -) - - -@six.add_metaclass(ABCMeta) -class UnrealBaseCreator(Creator): - """Base class for Unreal creator plugins.""" - root = "/Game/Ayon/AyonPublishInstances" - suffix = "_INS" - - @staticmethod - def cache_instance_data(shared_data): - """Cache instances for Creators to shared data. - - Create `unreal_cached_instances` key when needed in shared data and - fill it with all collected instances from the scene under its - respective creator identifiers. - - If legacy instances are detected in the scene, create - `unreal_cached_legacy_instances` there and fill it with - all legacy products under family as a key. - - Args: - Dict[str, Any]: Shared data. - - """ - if "unreal_cached_instances" in shared_data: - return - - unreal_cached_instances = collections.defaultdict(list) - unreal_cached_legacy_instances = collections.defaultdict(list) - for instance in ls_inst(): - creator_id = instance.get("creator_identifier") - if creator_id: - unreal_cached_instances[creator_id].append(instance) - else: - family = instance.get("family") - unreal_cached_legacy_instances[family].append(instance) - - shared_data["unreal_cached_instances"] = unreal_cached_instances - shared_data["unreal_cached_legacy_instances"] = ( - unreal_cached_legacy_instances - ) - - def create(self, product_name, instance_data, pre_create_data): - try: - instance_name = f"{product_name}{self.suffix}" - pub_instance = create_publish_instance(instance_name, self.root) - - instance_data["productName"] = product_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - - instance = CreatedInstance( - self.product_type, - product_name, - instance_data, - self) - self._add_instance_to_context(instance) - - pub_instance.set_editor_property('add_external_assets', True) - assets = pub_instance.get_editor_property('asset_data_external') - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for member in pre_create_data.get("members", []): - obj = ar.get_asset_by_object_path(member).get_asset() - assets.add(obj) - - imprint(f"{self.root}/{instance_name}", instance.data_to_store()) - - return instance - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def collect_instances(self): - # cache instances if missing - self.cache_instance_data(self.collection_shared_data) - for instance in self.collection_shared_data[ - "unreal_cached_instances"].get(self.identifier, []): - # Unreal saves metadata as string, so we need to convert it back - instance['creator_attributes'] = ast.literal_eval( - instance.get('creator_attributes', '{}')) - instance['publish_attributes'] = ast.literal_eval( - instance.get('publish_attributes', '{}')) - created_instance = CreatedInstance.from_existing(instance, self) - self._add_instance_to_context(created_instance) - - def update_instances(self, update_list): - for created_inst, changes in update_list: - instance_node = created_inst.get("instance_path", "") - - if not instance_node: - unreal.log_warning( - f"Instance node not found for {created_inst}") - continue - - new_values = { - key: changes[key].new_value - for key in changes.changed_keys - } - imprint( - instance_node, - new_values - ) - - def remove_instances(self, instances): - for instance in instances: - instance_node = instance.data.get("instance_path", "") - if instance_node: - unreal.EditorAssetLibrary.delete_asset(instance_node) - - self._remove_instance_from_context(instance) - - -@six.add_metaclass(ABCMeta) -class UnrealAssetCreator(UnrealBaseCreator): - """Base class for Unreal creator plugins based on assets.""" - - def create(self, product_name, instance_data, pre_create_data): - """Create instance of the asset. - - Args: - product_name (str): Name of the product. - instance_data (dict): Data for the instance. - pre_create_data (dict): Data for the instance. - - Returns: - CreatedInstance: Created instance. - """ - try: - # Check if instance data has members, filled by the plugin. - # If not, use selection. - if not pre_create_data.get("members"): - pre_create_data["members"] = [] - - if pre_create_data.get("use_selection"): - utilib = unreal.EditorUtilityLibrary - sel_objects = utilib.get_selected_assets() - pre_create_data["members"] = [ - a.get_path_name() for a in sel_objects] - - super(UnrealAssetCreator, self).create( - product_name, - instance_data, - pre_create_data) - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def get_pre_create_attr_defs(self): - return [ - BoolDef("use_selection", label="Use selection", default=True) - ] - - -@six.add_metaclass(ABCMeta) -class UnrealActorCreator(UnrealBaseCreator): - """Base class for Unreal creator plugins based on actors.""" - - def create(self, product_name, instance_data, pre_create_data): - """Create instance of the asset. - - Args: - product_name (str): Name of the product. - instance_data (dict): Data for the instance. - pre_create_data (dict): Data for the instance. - - Returns: - CreatedInstance: Created instance. - """ - try: - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - - # Check if the level is saved - if world.get_path_name().startswith("/Temp/"): - raise CreatorError( - "Level must be saved before creating instances.") - - # Check if instance data has members, filled by the plugin. - # If not, use selection. - if not instance_data.get("members"): - actor_subsystem = unreal.EditorActorSubsystem() - sel_actors = actor_subsystem.get_selected_level_actors() - selection = [a.get_path_name() for a in sel_actors] - - instance_data["members"] = selection - - instance_data["level"] = world.get_path_name() - - super(UnrealActorCreator, self).create( - product_name, - instance_data, - pre_create_data) - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef("Select actors to create instance from them.") - ] - - -class Loader(LoaderPlugin, ABC): - """This serves as skeleton for future Ayon specific functionality""" - pass diff --git a/client/ayon_core/hosts/unreal/api/rendering.py b/client/ayon_core/hosts/unreal/api/rendering.py deleted file mode 100644 index 395513aefa..0000000000 --- a/client/ayon_core/hosts/unreal/api/rendering.py +++ /dev/null @@ -1,180 +0,0 @@ -import os - -import unreal - -from ayon_core.settings import get_project_settings -from ayon_core.pipeline import Anatomy -from ayon_core.hosts.unreal.api import pipeline -from ayon_core.tools.utils import show_message_dialog - - -queue = None -executor = None - - -def _queue_finish_callback(exec, success): - unreal.log("Render completed. Success: " + str(success)) - - # Delete our reference so we don't keep it alive. - global executor - global queue - del executor - del queue - - -def _job_finish_callback(job, success): - # You can make any edits you want to the editor world here, and the world - # will be duplicated when the next render happens. Make sure you undo your - # edits in OnQueueFinishedCallback if you don't want to leak state changes - # into the editor world. - unreal.log("Individual job completed.") - - -def start_rendering(): - """ - Start the rendering process. - """ - unreal.log("Starting rendering...") - - # Get selected sequences - assets = unreal.EditorUtilityLibrary.get_selected_assets() - - if not assets: - show_message_dialog( - title="No assets selected", - message="No assets selected. Select a render instance.", - level="warning") - raise RuntimeError( - "No assets selected. You need to select a render instance.") - - # instances = pipeline.ls_inst() - instances = [ - a for a in assets - if a.get_class().get_name() == "AyonPublishInstance"] - - inst_data = [] - - for i in instances: - data = pipeline.parse_container(i.get_path_name()) - if data["productType"] == "render": - inst_data.append(data) - - try: - project = os.environ.get("AYON_PROJECT_NAME") - anatomy = Anatomy(project) - root = anatomy.roots['renders'] - except Exception as e: - raise Exception( - "Could not find render root in anatomy settings.") from e - - render_dir = f"{root}/{project}" - - # subsystem = unreal.get_editor_subsystem( - # unreal.MoviePipelineQueueSubsystem) - # queue = subsystem.get_queue() - global queue - queue = unreal.MoviePipelineQueue() - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - data = get_project_settings(project) - config = None - config_path = str(data.get("unreal").get("render_config_path")) - if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path): - unreal.log("Found saved render configuration") - config = ar.get_asset_by_object_path(config_path).get_asset() - - for i in inst_data: - sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() - - sequences = [{ - "sequence": sequence, - "output": f"{i['output']}", - "frame_range": ( - int(float(i["frameStart"])), - int(float(i["frameEnd"])) + 1) - }] - render_list = [] - - # Get all the sequences to render. If there are subsequences, - # add them and their frame ranges to the render list. We also - # use the names for the output paths. - for seq in sequences: - subscenes = pipeline.get_subsequences(seq.get('sequence')) - - if subscenes: - for sub_seq in subscenes: - sequences.append({ - "sequence": sub_seq.get_sequence(), - "output": (f"{seq.get('output')}/" - f"{sub_seq.get_sequence().get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), sub_seq.get_end_frame()) - }) - else: - # Avoid rendering camera sequences - if "_camera" not in seq.get('sequence').get_name(): - render_list.append(seq) - - # Create the rendering jobs and add them to the queue. - for render_setting in render_list: - job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) - job.sequence = unreal.SoftObjectPath(i["master_sequence"]) - job.map = unreal.SoftObjectPath(i["master_level"]) - job.author = "Ayon" - - # If we have a saved configuration, copy it to the job. - if config: - job.get_configuration().copy_from(config) - - # User data could be used to pass data to the job, that can be - # read in the job's OnJobFinished callback. We could, - # for instance, pass the AyonPublishInstance's path to the job. - # job.user_data = "" - - output_dir = render_setting.get('output') - shot_name = render_setting.get('sequence').get_name() - - settings = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineOutputSetting) - settings.output_resolution = unreal.IntPoint(1920, 1080) - settings.custom_start_frame = render_setting.get("frame_range")[0] - settings.custom_end_frame = render_setting.get("frame_range")[1] - settings.use_custom_playback_range = True - settings.file_name_format = f"{shot_name}" + ".{frame_number}" - settings.output_directory.path = f"{render_dir}/{output_dir}" - - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - - render_format = data.get("unreal").get("render_format", "png") - - if render_format == "png": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_PNG) - elif render_format == "exr": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_EXR) - elif render_format == "jpg": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_JPG) - elif render_format == "bmp": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_BMP) - - # If there are jobs in the queue, start the rendering process. - if queue.get_jobs(): - global executor - executor = unreal.MoviePipelinePIEExecutor() - - preroll_frames = data.get("unreal").get("preroll_frames", 0) - - settings = unreal.MoviePipelinePIEExecutorSettings() - settings.set_editor_property( - "initial_delay_frame_count", preroll_frames) - - executor.on_executor_finished_delegate.add_callable_unique( - _queue_finish_callback) - executor.on_individual_job_finished_delegate.add_callable_unique( - _job_finish_callback) # Only available on PIE Executor - executor.execute(queue) diff --git a/client/ayon_core/hosts/unreal/api/tools_ui.py b/client/ayon_core/hosts/unreal/api/tools_ui.py deleted file mode 100644 index efae5bb702..0000000000 --- a/client/ayon_core/hosts/unreal/api/tools_ui.py +++ /dev/null @@ -1,162 +0,0 @@ -import sys -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core import ( - resources, - style -) -from ayon_core.tools.utils import host_tools -from ayon_core.tools.utils.lib import qt_app_context -from ayon_core.hosts.unreal.api import rendering - - -class ToolsBtnsWidget(QtWidgets.QWidget): - """Widget containing buttons which are clickable.""" - tool_required = QtCore.Signal(str) - - def __init__(self, parent=None): - super(ToolsBtnsWidget, self).__init__(parent) - - load_btn = QtWidgets.QPushButton("Load...", self) - publish_btn = QtWidgets.QPushButton("Publisher...", self) - manage_btn = QtWidgets.QPushButton("Manage...", self) - render_btn = QtWidgets.QPushButton("Render...", self) - experimental_tools_btn = QtWidgets.QPushButton( - "Experimental tools...", self - ) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(load_btn, 0) - layout.addWidget(publish_btn, 0) - layout.addWidget(manage_btn, 0) - layout.addWidget(render_btn, 0) - layout.addWidget(experimental_tools_btn, 0) - layout.addStretch(1) - - load_btn.clicked.connect(self._on_load) - publish_btn.clicked.connect(self._on_publish) - manage_btn.clicked.connect(self._on_manage) - render_btn.clicked.connect(self._on_render) - experimental_tools_btn.clicked.connect(self._on_experimental) - - def _on_create(self): - self.tool_required.emit("creator") - - def _on_load(self): - self.tool_required.emit("loader") - - def _on_publish(self): - self.tool_required.emit("publisher") - - def _on_manage(self): - self.tool_required.emit("sceneinventory") - - def _on_render(self): - rendering.start_rendering() - - def _on_experimental(self): - self.tool_required.emit("experimental_tools") - - -class ToolsDialog(QtWidgets.QDialog): - """Dialog with tool buttons that will stay opened until user close it.""" - def __init__(self, *args, **kwargs): - super(ToolsDialog, self).__init__(*args, **kwargs) - - self.setWindowTitle("Ayon tools") - icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) - self.setWindowIcon(icon) - - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - tools_widget = ToolsBtnsWidget(self) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(tools_widget) - - tools_widget.tool_required.connect(self._on_tool_require) - self._tools_widget = tools_widget - - self._first_show = True - - def sizeHint(self): - result = super(ToolsDialog, self).sizeHint() - result.setWidth(result.width() * 2) - return result - - def showEvent(self, event): - super(ToolsDialog, self).showEvent(event) - if self._first_show: - self.setStyleSheet(style.load_stylesheet()) - self._first_show = False - - def _on_tool_require(self, tool_name): - host_tools.show_tool_by_name(tool_name, parent=self) - - -class ToolsPopup(ToolsDialog): - """Popup with tool buttons that will close when loose focus.""" - def __init__(self, *args, **kwargs): - super(ToolsPopup, self).__init__(*args, **kwargs) - - self.setWindowFlags( - QtCore.Qt.FramelessWindowHint - | QtCore.Qt.Popup - ) - - def showEvent(self, event): - super(ToolsPopup, self).showEvent(event) - app = QtWidgets.QApplication.instance() - app.processEvents() - pos = QtGui.QCursor.pos() - self.move(pos) - - -class WindowCache: - """Cached objects and methods to be used in global scope.""" - _dialog = None - _popup = None - _first_show = True - - @classmethod - def _before_show(cls): - """Create QApplication if does not exist yet.""" - if not cls._first_show: - return - - cls._first_show = False - if not QtWidgets.QApplication.instance(): - QtWidgets.QApplication(sys.argv) - - @classmethod - def show_popup(cls): - cls._before_show() - with qt_app_context(): - if cls._popup is None: - cls._popup = ToolsPopup() - - cls._popup.show() - - @classmethod - def show_dialog(cls): - cls._before_show() - with qt_app_context(): - if cls._dialog is None: - cls._dialog = ToolsDialog() - - cls._dialog.show() - cls._dialog.raise_() - cls._dialog.activateWindow() - - -def show_tools_popup(): - WindowCache.show_popup() - - -def show_tools_dialog(): - WindowCache.show_dialog() diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py deleted file mode 100644 index e38591f65d..0000000000 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- -"""Hook to launch Unreal and prepare projects.""" -import os -import copy -import shutil -import tempfile -from pathlib import Path - -from qtpy import QtCore - -from ayon_core import resources -from ayon_applications import ( - PreLaunchHook, - ApplicationLaunchFailed, - LaunchTypes, -) -from ayon_core.pipeline.workfile import get_workfile_template_key -import ayon_core.hosts.unreal.lib as unreal_lib -from ayon_core.hosts.unreal.ue_workers import ( - UEProjectGenerationWorker, - UEPluginInstallWorker -) -from ayon_core.hosts.unreal.ui import SplashScreen - - -class UnrealPrelaunchHook(PreLaunchHook): - """Hook to handle launching Unreal. - - This hook will check if current workfile path has Unreal - project inside. IF not, it initializes it, and finally it pass - path to the project by environment variable to Unreal launcher - shell script. - - """ - app_groups = {"unreal"} - launch_types = {LaunchTypes.local} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.signature = f"( {self.__class__.__name__} )" - - def _get_work_filename(self): - # Use last workfile if was found - if self.data.get("last_workfile_path"): - last_workfile = Path(self.data.get("last_workfile_path")) - if last_workfile and last_workfile.exists(): - return last_workfile.name - - # Prepare data for fill data and for getting workfile template key - anatomy = self.data["anatomy"] - project_entity = self.data["project_entity"] - - # Use already prepared workdir data - workdir_data = copy.deepcopy(self.data["workdir_data"]) - task_type = workdir_data.get("task", {}).get("type") - - # QUESTION raise exception if version is part of filename template? - workdir_data["version"] = 1 - workdir_data["ext"] = "uproject" - - # Get workfile template key for current context - workfile_template_key = get_workfile_template_key( - project_entity["name"], - task_type, - self.host_name, - ) - # Fill templates - template_obj = anatomy.get_template_item( - "work", workfile_template_key, "file" - ) - - # Return filename - return template_obj.format_strict(workdir_data) - - def exec_plugin_install(self, engine_path: Path, env: dict = None): - # set up the QThread and worker with necessary signals - env = env or os.environ - q_thread = QtCore.QThread() - ue_plugin_worker = UEPluginInstallWorker() - - q_thread.started.connect(ue_plugin_worker.run) - ue_plugin_worker.setup(engine_path, env) - ue_plugin_worker.moveToThread(q_thread) - - splash_screen = SplashScreen( - "Installing plugin", - resources.get_resource("app_icons", "ue4.png") - ) - - # set up the splash screen with necessary triggers - ue_plugin_worker.installing.connect( - splash_screen.update_top_label_text - ) - ue_plugin_worker.progress.connect(splash_screen.update_progress) - ue_plugin_worker.log.connect(splash_screen.append_log) - ue_plugin_worker.finished.connect(splash_screen.quit_and_close) - ue_plugin_worker.failed.connect(splash_screen.fail) - - splash_screen.start_thread(q_thread) - splash_screen.show_ui() - - if not splash_screen.was_proc_successful(): - raise ApplicationLaunchFailed("Couldn't run the application! " - "Plugin failed to install!") - - def exec_ue_project_gen(self, - engine_version: str, - unreal_project_name: str, - engine_path: Path, - project_dir: Path): - self.log.info(( - f"{self.signature} Creating unreal " - f"project [ {unreal_project_name} ]" - )) - - q_thread = QtCore.QThread() - ue_project_worker = UEProjectGenerationWorker() - ue_project_worker.setup( - engine_version, - self.data["project_name"], - unreal_project_name, - engine_path, - project_dir - ) - ue_project_worker.moveToThread(q_thread) - q_thread.started.connect(ue_project_worker.run) - - splash_screen = SplashScreen( - "Initializing UE project", - resources.get_resource("app_icons", "ue4.png") - ) - - ue_project_worker.stage_begin.connect( - splash_screen.update_top_label_text - ) - ue_project_worker.progress.connect(splash_screen.update_progress) - ue_project_worker.log.connect(splash_screen.append_log) - ue_project_worker.finished.connect(splash_screen.quit_and_close) - ue_project_worker.failed.connect(splash_screen.fail) - - splash_screen.start_thread(q_thread) - splash_screen.show_ui() - - if not splash_screen.was_proc_successful(): - raise ApplicationLaunchFailed("Couldn't run the application! " - "Failed to generate the project!") - - def execute(self): - """Hook entry method.""" - workdir = self.launch_context.env["AYON_WORKDIR"] - executable = str(self.launch_context.executable) - engine_version = self.app_name.split("/")[-1].replace("-", ".") - try: - if int(engine_version.split(".")[0]) < 4 and \ - int(engine_version.split(".")[1]) < 26: - raise ApplicationLaunchFailed(( - f"{self.signature} Old unsupported version of UE " - f"detected - {engine_version}")) - except ValueError: - # there can be string in minor version and in that case - # int cast is failing. This probably happens only with - # early access versions and is of no concert for this check - # so let's keep it quiet. - ... - - unreal_project_filename = self._get_work_filename() - unreal_project_name = os.path.splitext(unreal_project_filename)[0] - # Unreal is sensitive about project names longer then 20 chars - if len(unreal_project_name) > 20: - raise ApplicationLaunchFailed( - f"Project name exceeds 20 characters ({unreal_project_name})!" - ) - - # Unreal doesn't accept non alphabet characters at the start - # of the project name. This is because project name is then used - # in various places inside c++ code and there variable names cannot - # start with non-alpha. We append 'P' before project name to solve it. - # 😱 - if not unreal_project_name[:1].isalpha(): - self.log.warning(( - "Project name doesn't start with alphabet " - f"character ({unreal_project_name}). Appending 'P'" - )) - unreal_project_name = f"P{unreal_project_name}" - unreal_project_filename = f'{unreal_project_name}.uproject' - - project_path = Path(os.path.join(workdir, unreal_project_name)) - - self.log.info(( - f"{self.signature} requested UE version: " - f"[ {engine_version} ]" - )) - - project_path.mkdir(parents=True, exist_ok=True) - - # engine_path points to the specific Unreal Engine root - # so, we are going up from the executable itself 3 levels. - engine_path: Path = Path(executable).parents[3] - - # Check if new env variable exists, and if it does, if the path - # actually contains the plugin. If not, install it. - - built_plugin_path = self.launch_context.env.get( - "AYON_BUILT_UNREAL_PLUGIN", None) - - if unreal_lib.check_built_plugin_existance(built_plugin_path): - self.log.info(( - f"{self.signature} using existing built Ayon plugin from " - f"{built_plugin_path}" - )) - unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path)) - else: - # Set "AYON_UNREAL_PLUGIN" to current process environment for - # execution of `create_unreal_project` - env_key = "AYON_UNREAL_PLUGIN" - if self.launch_context.env.get(env_key): - self.log.info(( - f"{self.signature} using Ayon plugin from " - f"{self.launch_context.env.get(env_key)}" - )) - if self.launch_context.env.get(env_key): - os.environ[env_key] = self.launch_context.env[env_key] - - if not unreal_lib.check_plugin_existence(engine_path): - self.exec_plugin_install(engine_path) - - project_file = project_path / unreal_project_filename - - if not project_file.is_file(): - with tempfile.TemporaryDirectory() as temp_dir: - self.exec_ue_project_gen(engine_version, - unreal_project_name, - engine_path, - Path(temp_dir)) - try: - self.log.info(( - f"Moving from {temp_dir} to " - f"{project_path.as_posix()}" - )) - shutil.copytree( - temp_dir, project_path, dirs_exist_ok=True) - - except shutil.Error as e: - raise ApplicationLaunchFailed(( - f"{self.signature} Cannot copy directory {temp_dir} " - f"to {project_path.as_posix()} - {e}" - )) from e - - self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version - # Append project file to launch arguments - self.launch_context.launch_args.append( - f"\"{project_file.as_posix()}\"") diff --git a/client/ayon_core/hosts/unreal/integration b/client/ayon_core/hosts/unreal/integration deleted file mode 160000 index 04b35dbf5f..0000000000 --- a/client/ayon_core/hosts/unreal/integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04b35dbf5fc42d905281fc30d3a22b139c1855e5 diff --git a/client/ayon_core/hosts/unreal/lib.py b/client/ayon_core/hosts/unreal/lib.py deleted file mode 100644 index 185853a0aa..0000000000 --- a/client/ayon_core/hosts/unreal/lib.py +++ /dev/null @@ -1,551 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unreal launching and project tools.""" - -import json -import os -import platform -import re -import subprocess -from collections import OrderedDict -from distutils import dir_util -from pathlib import Path -from typing import List - -from ayon_core.settings import get_project_settings - - -def get_engine_versions(env=None): - """Detect Unreal Engine versions. - - This will try to detect location and versions of installed Unreal Engine. - Location can be overridden by `UNREAL_ENGINE_LOCATION` environment - variable. - - .. deprecated:: 3.15.4 - - Args: - env (dict, optional): Environment to use. - - Returns: - OrderedDict: dictionary with version as a key and dir as value. - so the highest version is first. - - Example: - >>> get_engine_versions() - { - "4.23": "C:/Epic Games/UE_4.23", - "4.24": "C:/Epic Games/UE_4.24" - } - - """ - env = env or os.environ - engine_locations = {} - try: - root, dirs, _ = next(os.walk(env["UNREAL_ENGINE_LOCATION"])) - - for directory in dirs: - if directory.startswith("UE"): - try: - ver = re.split(r"[-_]", directory)[1] - except IndexError: - continue - engine_locations[ver] = os.path.join(root, directory) - except KeyError: - # environment variable not set - pass - except OSError: - # specified directory doesn't exist - pass - except StopIteration: - # specified directory doesn't exist - pass - - # if we've got something, terminate auto-detection process - if engine_locations: - return OrderedDict(sorted(engine_locations.items())) - - # else kick in platform specific detection - if platform.system().lower() == "windows": - return OrderedDict(sorted(_win_get_engine_versions().items())) - if platform.system().lower() == "linux": - # on linux, there is no installation and getting Unreal Engine involves - # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. - pass - if platform.system().lower() == "darwin": - return OrderedDict(sorted(_darwin_get_engine_version().items())) - - return OrderedDict() - - -def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path: - """Get UE Editor executable path.""" - ue_path = engine_path / "Engine/Binaries" - - ue_name = "UnrealEditor" - - # handle older versions of Unreal Engine - if engine_version.split(".")[0] == "4": - ue_name = "UE4Editor" - - if platform.system().lower() == "windows": - ue_path /= f"Win64/{ue_name}.exe" - - elif platform.system().lower() == "linux": - ue_path /= f"Linux/{ue_name}" - - elif platform.system().lower() == "darwin": - ue_path /= f"Mac/{ue_name}" - - return ue_path - - -def _win_get_engine_versions(): - """Get Unreal Engine versions on Windows. - - If engines are installed via Epic Games Launcher then there is: - `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` - This file is JSON file listing installed stuff, Unreal engines - are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` - - .. deprecated:: 3.15.4 - - Returns: - dict: version as a key and path as a value. - - """ - install_json_path = os.path.join( - os.getenv("PROGRAMDATA"), - "Epic", - "UnrealEngineLauncher", - "LauncherInstalled.dat", - ) - - return _parse_launcher_locations(install_json_path) - - -def _darwin_get_engine_version() -> dict: - """Get Unreal Engine versions on MacOS. - - It works the same as on Windows, just JSON file location is different. - - .. deprecated:: 3.15.4 - - Returns: - dict: version as a key and path as a value. - - See Also: - :func:`_win_get_engine_versions`. - - """ - install_json_path = os.path.join( - os.getenv("HOME"), - "Library", - "Application Support", - "Epic", - "UnrealEngineLauncher", - "LauncherInstalled.dat", - ) - - return _parse_launcher_locations(install_json_path) - - -def _parse_launcher_locations(install_json_path: str) -> dict: - """This will parse locations from json file. - - .. deprecated:: 3.15.4 - - Args: - install_json_path (str): Path to `LauncherInstalled.dat`. - - Returns: - dict: with unreal engine versions as keys and - paths to those engine installations as value. - - """ - engine_locations = {} - if os.path.isfile(install_json_path): - with open(install_json_path, "r") as ilf: - try: - install_data = json.load(ilf) - except json.JSONDecodeError as e: - raise Exception( - "Invalid `LauncherInstalled.dat file. `" - "Cannot determine Unreal Engine location." - ) from e - - for installation in install_data.get("InstallationList", []): - if installation.get("AppName").startswith("UE_"): - ver = installation.get("AppName").split("_")[1] - engine_locations[ver] = installation.get("InstallLocation") - - return engine_locations - - -def create_unreal_project(project_name: str, - unreal_project_name: str, - ue_version: str, - pr_dir: Path, - engine_path: Path, - dev_mode: bool = False, - env: dict = None) -> None: - """This will create `.uproject` file at specified location. - - As there is no way I know to create a project via command line, this is - easiest option. Unreal project file is basically a JSON file. If we find - the `AYON_UNREAL_PLUGIN` environment variable we assume this is the - location of the Integration Plugin and we copy its content to the project - folder and enable this plugin. - - Args: - project_name (str): Name of the project in AYON. - unreal_project_name (str): Name of the project in Unreal. - ue_version (str): Unreal engine version (like 4.23). - pr_dir (Path): Path to directory where project will be created. - engine_path (Path): Path to Unreal Engine installation. - dev_mode (bool, optional): Flag to trigger C++ style Unreal project - needing Visual Studio and other tools to compile plugins from - sources. This will trigger automatically if `Binaries` - directory is not found in plugin folders as this indicates - this is only source distribution of the plugin. Dev mode - is also set in Settings. - env (dict, optional): Environment to use. If not set, `os.environ`. - - Throws: - NotImplementedError: For unsupported platforms. - - Returns: - None - - Deprecated: - since 3.16.0 - - """ - - preset = get_project_settings(project_name)["unreal"]["project_setup"] - # get unreal engine identifier - # ------------------------------------------------------------------------- - # FIXME (antirotor): As of 4.26 this is problem with UE4 built from - # sources. In that case Engine ID is calculated per machine/user and not - # from Engine files as this code then reads. This then prevents UE4 - # to directly open project as it will complain about project being - # created in different UE4 version. When user convert such project - # to his UE4 version, Engine ID is replaced in uproject file. If some - # other user tries to open it, it will present him with similar error. - - # engine_path should be the location of UE_X.X folder - - ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version) - cmdlet_project: Path = get_path_to_cmdlet_project(ue_version) - - project_file = pr_dir / f"{unreal_project_name}.uproject" - - print("--- Generating a new project ...") - commandlet_cmd = [ - ue_editor_exe.as_posix(), - cmdlet_project.as_posix(), - "-run=AyonGenerateProject", - project_file.resolve().as_posix() - ] - - if dev_mode or preset["dev_mode"]: - commandlet_cmd.append('-GenerateCode') - - gen_process = subprocess.Popen(commandlet_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in gen_process.stdout: - print(line.decode(), end='') - gen_process.stdout.close() - return_code = gen_process.wait() - - if return_code and return_code != 0: - raise RuntimeError( - (f"Failed to generate '{unreal_project_name}' project! " - f"Exited with return code {return_code}")) - - print("--- Project has been generated successfully.") - - with open(project_file.as_posix(), mode="r+") as pf: - pf_json = json.load(pf) - pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version) - pf.seek(0) - json.dump(pf_json, pf, indent=4) - pf.truncate() - print("--- Engine ID has been written into the project file") - - if dev_mode or preset["dev_mode"]: - u_build_tool = get_path_to_ubt(engine_path, ue_version) - - arch = "Win64" - if platform.system().lower() == "windows": - arch = "Win64" - elif platform.system().lower() == "linux": - arch = "Linux" - elif platform.system().lower() == "darwin": - # we need to test this out - arch = "Mac" - - command1 = [ - u_build_tool.as_posix(), - "-projectfiles", - f"-project={project_file}", - "-progress" - ] - - subprocess.run(command1) - - command2 = [ - u_build_tool.as_posix(), - f"-ModuleWithSuffix={unreal_project_name},3555", - arch, - "Development", - "-TargetType=Editor", - f"-Project={project_file}", - project_file, - "-IgnoreJunk" - ] - - subprocess.run(command2) - - # ensure we have PySide2 installed in engine - python_path = None - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/python.exe") - - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if not python_path: - raise NotImplementedError("Unsupported platform") - if not python_path.exists(): - raise RuntimeError(f"Unreal Python not found at {python_path}") - subprocess.check_call( - [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) - - -def get_path_to_uat(engine_path: Path) -> Path: - if platform.system().lower() == "windows": - return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" - - if platform.system().lower() in ["linux", "darwin"]: - return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" - - -def get_compatible_integration( - ue_version: str, integration_root: Path) -> List[Path]: - """Get path to compatible version of integration plugin. - - This will try to get the closest compatible versions to the one - specified in sorted list. - - Args: - ue_version (str): version of the current Unreal Engine. - integration_root (Path): path to built-in integration plugins. - - Returns: - list of Path: Sorted list of paths closest to the specified - version. - - """ - major, minor = ue_version.split(".") - integration_paths = [p for p in integration_root.iterdir() - if p.is_dir()] - - compatible_versions = [] - for i in integration_paths: - # parse version from path - try: - i_major, i_minor = re.search( - r"(?P\d+).(?P\d+)$", i.name).groups() - except AttributeError: - # in case there is no match, just skip to next - continue - - # consider versions with different major so different that they - # are incompatible - if int(major) != int(i_major): - continue - - compatible_versions.append(i) - - sorted(set(compatible_versions)) - return compatible_versions - - -def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path( - os.path.dirname(os.path.abspath(__file__))) - - # For now, only tested on Windows (For Linux and Mac - # it has to be implemented) - cmd_project /= f"integration/UE_{ue_version}" - - # if the integration doesn't exist for current engine version - # try to find the closest to it. - if cmd_project.exists(): - return cmd_project / "CommandletProject/CommandletProject.uproject" - - if compatible_versions := get_compatible_integration( - ue_version, cmd_project.parent - ): - return compatible_versions[-1] / "CommandletProject/CommandletProject.uproject" # noqa: E501 - else: - raise RuntimeError( - ("There are no compatible versions of Unreal " - "integration plugin compatible with running version " - f"of Unreal Engine {ue_version}")) - - -def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: - u_build_tool_path = engine_path / "Engine/Binaries/DotNET" - - if ue_version.split(".")[0] == "4": - u_build_tool_path /= "UnrealBuildTool.exe" - elif ue_version.split(".")[0] == "5": - u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe" - - return Path(u_build_tool_path) - - -def get_build_id(engine_path: Path, ue_version: str) -> str: - ue_modules = Path() - if platform.system().lower() == "windows": - ue_modules_path = engine_path / "Engine/Binaries/Win64" - if ue_version.split(".")[0] == "4": - ue_modules_path /= "UE4Editor.modules" - elif ue_version.split(".")[0] == "5": - ue_modules_path /= "UnrealEditor.modules" - ue_modules = Path(ue_modules_path) - - if platform.system().lower() == "linux": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Linux", "UE4Editor.modules")) - - if platform.system().lower() == "darwin": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Mac", "UE4Editor.modules")) - - if ue_modules.exists(): - print("--- Loading Engine ID from modules file ...") - with open(ue_modules, "r") as mp: - loaded_modules = json.load(mp) - - if loaded_modules.get("BuildId"): - return "{" + loaded_modules.get("BuildId") + "}" - - -def check_built_plugin_existance(plugin_path) -> bool: - if not plugin_path: - return False - - integration_plugin_path = Path(plugin_path) - - if not integration_plugin_path.is_dir(): - raise RuntimeError("Path to the integration plugin is null!") - - if not (integration_plugin_path / "Binaries").is_dir() \ - or not (integration_plugin_path / "Intermediate").is_dir(): - return False - - return True - - -def copy_built_plugin(engine_path: Path, plugin_path: Path) -> None: - ayon_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not ayon_plugin_path.is_dir(): - ayon_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path: Path = ayon_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - dir_util.copy_tree(plugin_path.as_posix(), ayon_plugin_path.as_posix()) - - -def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: - env = env or os.environ - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(integration_plugin_path): - raise RuntimeError("Path to the integration plugin is null!") - - # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not op_plugin_path.is_dir(): - return False - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - return False - - return True - - -def try_installing_plugin(engine_path: Path, env: dict = None) -> None: - env = env or os.environ - - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(integration_plugin_path): - raise RuntimeError("Path to the integration plugin is null!") - - # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not op_plugin_path.is_dir(): - op_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path: Path = op_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - _build_and_move_plugin(engine_path, op_plugin_path, env) - - -def _build_and_move_plugin(engine_path: Path, - plugin_build_path: Path, - env: dict = None) -> None: - uat_path: Path = get_path_to_uat(engine_path) - - env = env or os.environ - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if uat_path.is_file(): - temp_dir: Path = integration_plugin_path.parent / "Temp" - temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = integration_plugin_path / "Ayon.uplugin" - - # in order to successfully build the plugin, - # It must be built outside the Engine directory and then moved - build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', - 'BuildPlugin', - f'-Plugin={uplugin_path.as_posix()}', - f'-Package={temp_dir.as_posix()}'] - subprocess.run(build_plugin_cmd) - - # Copy the contents of the 'Temp' dir into the - # 'Ayon' directory in the engine - dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) - - # We need to also copy the config folder. - # The UAT doesn't include the Config folder in the build - plugin_install_config_path: Path = plugin_build_path / "Config" - integration_plugin_config_path = integration_plugin_path / "Config" - - dir_util.copy_tree(integration_plugin_config_path.as_posix(), - plugin_install_config_path.as_posix()) - - dir_util.remove_tree(temp_dir.as_posix()) diff --git a/client/ayon_core/hosts/unreal/plugins/__init__.py b/client/ayon_core/hosts/unreal/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_camera.py b/client/ayon_core/hosts/unreal/plugins/create/create_camera.py deleted file mode 100644 index 3ffb9dd70b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_camera.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateCamera(UnrealAssetCreator): - """Create Camera.""" - - identifier = "io.ayon.creators.unreal.camera" - label = "Camera" - product_type = "camera" - icon = "fa.camera" - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - # Add the current level path to the metadata - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - - instance_data["level"] = world.get_path_name() - - super(CreateCamera, self).create( - product_name, - instance_data, - pre_create_data) diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_layout.py b/client/ayon_core/hosts/unreal/plugins/create/create_layout.py deleted file mode 100644 index 9bcddfe507..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_layout.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from ayon_core.hosts.unreal.api.plugin import ( - UnrealActorCreator, -) - - -class CreateLayout(UnrealActorCreator): - """Layout output for character rigs.""" - - identifier = "io.ayon.creators.unreal.layout" - label = "Layout" - product_type = "layout" - icon = "cubes" diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_look.py b/client/ayon_core/hosts/unreal/plugins/create/create_look.py deleted file mode 100644 index edc6d45f2f..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_look.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.pipeline import ( - create_folder -) -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator -) -from ayon_core.lib import UILabelDef - - -class CreateLook(UnrealAssetCreator): - """Shader connections defining shape look.""" - - identifier = "io.ayon.creators.unreal.look" - label = "Look" - product_type = "look" - icon = "paint-brush" - - def create(self, product_name, instance_data, pre_create_data): - # We need to set this to True for the parent class to work - pre_create_data["use_selection"] = True - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one asset.") - - selected_asset = selection[0] - - look_directory = "/Game/Ayon/Looks" - - # Create the folder - folder_name = create_folder(look_directory, product_name) - path = f"{look_directory}/{folder_name}" - - instance_data["look"] = path - - # Create a new cube static mesh - ar = unreal.AssetRegistryHelpers.get_asset_registry() - cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") - - # Get the mesh of the selected object - original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() - materials = original_mesh.get_editor_property('static_materials') - - pre_create_data["members"] = [] - - # Add the materials to the cube - for material in materials: - mat_name = material.get_editor_property('material_slot_name') - object_path = f"{path}/{mat_name}.{mat_name}" - unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset( - cube.get_asset(), object_path - ) - - # Remove the default material of the cube object - unreal_object.get_editor_property('static_materials').pop() - - unreal_object.add_material( - material.get_editor_property('material_interface')) - - pre_create_data["members"].append(object_path) - - unreal.EditorAssetLibrary.save_asset(object_path) - - super(CreateLook, self).create( - product_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef("Select the asset from which to create the look.") - ] diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_render.py b/client/ayon_core/hosts/unreal/plugins/create/create_render.py deleted file mode 100644 index 5a96d9809c..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_render.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from ayon_core.hosts.unreal.api.pipeline import ( - UNREAL_VERSION, - create_folder, - get_subsequences, -) -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator -) -from ayon_core.lib import ( - UILabelDef, - UISeparatorDef, - BoolDef, - NumberDef -) - - -class CreateRender(UnrealAssetCreator): - """Create instance for sequence for rendering""" - - identifier = "io.ayon.creators.unreal.render" - label = "Render" - product_type = "render" - icon = "eye" - - def create_instance( - self, instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data - ): - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] - - super(CreateRender, self).create( - product_name, - instance_data, - pre_create_data) - - def create_with_new_sequence( - self, product_name, instance_data, pre_create_data - ): - # If the option to create a new level sequence is selected, - # create a new level sequence and a master level. - - root = "/Game/Ayon/Sequences" - - # Create a new folder for the sequence in root - sequence_dir_name = create_folder(root, product_name) - sequence_dir = f"{root}/{sequence_dir_name}" - - unreal.log_warning(f"sequence_dir: {sequence_dir}") - - # Create the level sequence - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - seq = asset_tools.create_asset( - asset_name=product_name, - package_path=sequence_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew()) - - seq.set_playback_start(pre_create_data.get("start_frame")) - seq.set_playback_end(pre_create_data.get("end_frame")) - - pre_create_data["members"] = [seq.get_path_name()] - - unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) - - # Create the master level - if UNREAL_VERSION.major >= 5: - curr_level = unreal.LevelEditorSubsystem().get_current_level() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - levels = unreal.EditorLevelUtils.get_levels(world) - curr_level = levels[0] if len(levels) else None - if not curr_level: - raise RuntimeError("No level loaded.") - curr_level_path = curr_level.get_outer().get_path_name() - - # If the level path does not start with "/Game/", the current - # level is a temporary, unsaved level. - if curr_level_path.startswith("/Game/"): - if UNREAL_VERSION.major >= 5: - unreal.LevelEditorSubsystem().save_current_level() - else: - unreal.EditorLevelLibrary.save_current_level() - - ml_path = f"{sequence_dir}/{product_name}_MasterLevel" - - if UNREAL_VERSION.major >= 5: - unreal.LevelEditorSubsystem().new_level(ml_path) - else: - unreal.EditorLevelLibrary.new_level(ml_path) - - seq_data = { - "sequence": seq, - "output": f"{seq.get_name()}", - "frame_range": ( - seq.get_playback_start(), - seq.get_playback_end())} - - self.create_instance( - instance_data, product_name, pre_create_data, - seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) - - def create_from_existing_sequence( - self, product_name, instance_data, pre_create_data - ): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [ - a.get_path_name() for a in sel_objects - if a.get_class().get_name() == "LevelSequence"] - - if len(selection) == 0: - raise RuntimeError("Please select at least one Level Sequence.") - - seq_data = None - - for sel in selection: - selected_asset = ar.get_asset_by_object_path(sel).get_asset() - selected_asset_path = selected_asset.get_path_name() - - # Check if the selected asset is a level sequence asset. - if selected_asset.get_class().get_name() != "LevelSequence": - unreal.log_warning( - f"Skipping {selected_asset.get_name()}. It isn't a Level " - "Sequence.") - - if pre_create_data.get("use_hierarchy"): - # The asset name is the the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace( - "/Game/Ayon/", "").split("/")[0] - - search_path = f"/Game/Ayon/{asset_name}" - else: - search_path = Path(selected_asset_path).parent.as_posix() - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - try: - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[search_path], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[search_path], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() - except IndexError: - raise RuntimeError( - "Could not find the hierarchy for the selected sequence.") - - # If the selected asset is the master sequence, we get its data - # and then we create the instance for the master sequence. - # Otherwise, we cycle from the master sequence to find the selected - # sequence and we get its data. This data will be used to create - # the instance for the selected sequence. In particular, - # we get the frame range of the selected sequence and its final - # output path. - master_seq_data = { - "sequence": master_seq_obj, - "output": f"{master_seq_obj.get_name()}", - "frame_range": ( - master_seq_obj.get_playback_start(), - master_seq_obj.get_playback_end())} - - if (selected_asset_path == master_seq or - pre_create_data.get("use_hierarchy")): - seq_data = master_seq_data - else: - seq_data_list = [master_seq_data] - - for seq in seq_data_list: - subscenes = get_subsequences(seq.get('sequence')) - - for sub_seq in subscenes: - sub_seq_obj = sub_seq.get_sequence() - curr_data = { - "sequence": sub_seq_obj, - "output": (f"{seq.get('output')}/" - f"{sub_seq_obj.get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), - sub_seq.get_end_frame() - 1)} - - # If the selected asset is the current sub-sequence, - # we get its data and we break the loop. - # Otherwise, we add the current sub-sequence data to - # the list of sequences to check. - if sub_seq_obj.get_path_name() == selected_asset_path: - seq_data = curr_data - break - - seq_data_list.append(curr_data) - - # If we found the selected asset, we break the loop. - if seq_data is not None: - break - - # If we didn't find the selected asset, we don't create the - # instance. - if not seq_data: - unreal.log_warning( - f"Skipping {selected_asset.get_name()}. It isn't a " - "sub-sequence of the master sequence.") - continue - - self.create_instance( - instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data) - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("create_seq"): - self.create_with_new_sequence( - product_name, instance_data, pre_create_data) - else: - self.create_from_existing_sequence( - product_name, instance_data, pre_create_data) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef( - "Select a Level Sequence to render or create a new one." - ), - BoolDef( - "create_seq", - label="Create a new Level Sequence", - default=False - ), - UILabelDef( - "WARNING: If you create a new Level Sequence, the current\n" - "level will be saved and a new Master Level will be created." - ), - NumberDef( - "start_frame", - label="Start Frame", - default=0, - minimum=-999999, - maximum=999999 - ), - NumberDef( - "end_frame", - label="Start Frame", - default=150, - minimum=-999999, - maximum=999999 - ), - UISeparatorDef(), - UILabelDef( - "The following settings are valid only if you are not\n" - "creating a new sequence." - ), - BoolDef( - "use_hierarchy", - label="Use Hierarchy", - default=False - ), - ] diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py b/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py deleted file mode 100644 index 603b852873..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateStaticMeshFBX(UnrealAssetCreator): - """Create Static Meshes as FBX geometry.""" - - identifier = "io.ayon.creators.unreal.staticmeshfbx" - label = "Static Mesh (FBX)" - product_type = "unrealStaticMesh" - icon = "cube" diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py b/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py deleted file mode 100644 index 1cd532c63d..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUAsset(UnrealAssetCreator): - """Create UAsset.""" - - identifier = "io.ayon.creators.unreal.uasset" - label = "UAsset" - product_type = "uasset" - icon = "cube" - - extension = ".uasset" - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != self.extension: - raise CreatorError( - f"{Path(sys_path).name} is not a {self.label}.") - - super(CreateUAsset, self).create( - product_name, - instance_data, - pre_create_data) - - -class CreateUMap(CreateUAsset): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - product_type = "uasset" - extension = ".umap" - - def create(self, product_name, instance_data, pre_create_data): - instance_data["families"] = ["umap"] - - super(CreateUMap, self).create( - product_name, - instance_data, - pre_create_data) diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py deleted file mode 100644 index 1f63a1697a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ /dev/null @@ -1,66 +0,0 @@ -import unreal - -from ayon_core.hosts.unreal.api.tools_ui import qt_app_context -from ayon_core.hosts.unreal.api.pipeline import delete_asset_if_unused -from ayon_core.pipeline import InventoryAction - - -class DeleteUnusedAssets(InventoryAction): - """Delete all the assets that are not used in any level. - """ - - label = "Delete Unused Assets" - icon = "trash" - color = "red" - order = 1 - - dialog = None - - def _delete_unused_assets(self, containers): - allowed_families = ["model", "rig"] - - for container in containers: - container_dir = container.get("namespace") - if container.get("family") not in allowed_families: - unreal.log_warning( - f"Container {container_dir} is not supported.") - continue - - asset_content = unreal.EditorAssetLibrary.list_assets( - container_dir, recursive=True, include_folder=False - ) - - delete_asset_if_unused(container, asset_content) - - def _show_confirmation_dialog(self, containers): - from qtpy import QtCore - from ayon_core.tools.utils import SimplePopup - from ayon_core.style import load_stylesheet - - dialog = SimplePopup() - dialog.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.WindowStaysOnTopHint - ) - dialog.setFocusPolicy(QtCore.Qt.StrongFocus) - dialog.setWindowTitle("Delete all unused assets") - dialog.set_message( - "You are about to delete all the assets in the project that \n" - "are not used in any level. Are you sure you want to continue?" - ) - dialog.set_button_text("Delete") - - dialog.on_clicked.connect( - lambda: self._delete_unused_assets(containers) - ) - - dialog.show() - dialog.raise_() - dialog.activateWindow() - dialog.setStyleSheet(load_stylesheet()) - - self.dialog = dialog - - def process(self, containers): - with qt_app_context(): - self._show_confirmation_dialog(containers) diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py b/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py deleted file mode 100644 index 96965d68e6..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py +++ /dev/null @@ -1,84 +0,0 @@ -import unreal - -from ayon_core.hosts.unreal.api.pipeline import ( - ls, - replace_static_mesh_actors, - replace_skeletal_mesh_actors, - replace_geometry_cache_actors, -) -from ayon_core.pipeline import InventoryAction - - -def update_assets(containers, selected): - allowed_families = ["model", "rig"] - - # Get all the containers in the Unreal Project - all_containers = ls() - - for container in containers: - container_dir = container.get("namespace") - if container.get("family") not in allowed_families: - unreal.log_warning( - f"Container {container_dir} is not supported.") - continue - - # Get all containers with same asset_name but different objectName. - # These are the containers that need to be updated in the level. - sa_containers = [ - i - for i in all_containers - if ( - i.get("asset_name") == container.get("asset_name") and - i.get("objectName") != container.get("objectName") - ) - ] - - asset_content = unreal.EditorAssetLibrary.list_assets( - container_dir, recursive=True, include_folder=False - ) - - # Update all actors in level - for sa_cont in sa_containers: - sa_dir = sa_cont.get("namespace") - old_content = unreal.EditorAssetLibrary.list_assets( - sa_dir, recursive=True, include_folder=False - ) - - if container.get("family") == "rig": - replace_skeletal_mesh_actors( - old_content, asset_content, selected) - replace_static_mesh_actors( - old_content, asset_content, selected) - elif container.get("family") == "model": - if container.get("loader") == "PointCacheAlembicLoader": - replace_geometry_cache_actors( - old_content, asset_content, selected) - else: - replace_static_mesh_actors( - old_content, asset_content, selected) - - unreal.EditorLevelLibrary.save_current_level() - - -class UpdateAllActors(InventoryAction): - """Update all the Actors in the current level to the version of the asset - selected in the scene manager. - """ - - label = "Replace all Actors in level to this version" - icon = "arrow-up" - - def process(self, containers): - update_assets(containers, False) - - -class UpdateSelectedActors(InventoryAction): - """Update only the selected Actors in the current level to the version - of the asset selected in the scene manager. - """ - - label = "Replace selected Actors in level to this version" - icon = "arrow-up" - - def process(self, containers): - update_assets(containers, True) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py deleted file mode 100644 index a12f4f41b4..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Alembic Animation.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class AnimationAlembicLoader(plugin.Loader): - """Load Unreal SkeletalMesh from Alembic""" - - product_types = {"animation"} - label = "Import Alembic Animation" - representations = {"abc"} - icon = "cube" - color = "orange" - - def get_task(self, filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - sm_settings = unreal.AbcStaticMeshSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, -1.0]) - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - options.static_mesh_settings = sm_settings - options.conversion_settings = conversion_settings - task.options = options - - return task - - def load(self, context, name, namespace, data): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_name = context["folder"]["name"] - folder_path = context["folder"]["path"] - product_type = context["product"]["productType"] - suffix = "_CON" - if folder_name: - asset_name = "{}_{}".format(folder_name, name) - else: - asset_name = "{}".format(name) - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) - - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - asset_tools.import_asset_tasks([task]) - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint( - f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_name = container["asset_name"] - repre_entity = context["representation"] - source_path = get_representation_path(repre_entity) - destination_path = container["namespace"] - - task = self.get_task( - source_path, destination_path, folder_name, True - ) - - # do import fbx and replace existing data - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - asset_tools.import_asset_tasks([task]) - - container_path = f"{container['namespace']}/{container['objectName']}" - - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_animation.py deleted file mode 100644 index f6a612ce53..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py +++ /dev/null @@ -1,337 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load FBX with animations.""" -import os -import json - -import unreal -from unreal import EditorAssetLibrary -from unreal import MovieSceneSkeletalAnimationTrack -from unreal import MovieSceneSkeletalAnimationSection - -from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline - - -class AnimationFBXLoader(plugin.Loader): - """Load Unreal SkeletalMesh from FBX.""" - - product_types = {"animation"} - label = "Import FBX Animation" - representations = {"fbx"} - icon = "cube" - color = "orange" - - def _process(self, path, asset_dir, asset_name, instance_name): - automated = False - actor = None - - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - if instance_name: - automated = True - # Old method to get the actor - # actor_name = 'PersistentLevel.' + instance_name - # actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) - actors = unreal.EditorLevelLibrary.get_all_level_actors() - for a in actors: - if a.get_class().get_name() != "SkeletalMeshActor": - continue - if a.get_actor_label() == instance_name: - actor = a - break - if not actor: - raise Exception(f"Could not find actor {instance_name}") - skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton - task.options.set_editor_property('skeleton', skeleton) - - if not actor: - return None - - folder_entity = get_current_folder_entity(fields=["attrib.fps"]) - - task.set_editor_property('filename', path) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', automated) - task.set_editor_property('save', False) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - animation = None - - for a in asset_content: - imported_asset_data = EditorAssetLibrary.find_asset_data(a) - imported_asset = unreal.AssetRegistryHelpers.get_asset( - imported_asset_data) - if imported_asset.__class__ == unreal.AnimSequence: - animation = imported_asset - break - - if animation: - animation.set_editor_property('enable_root_motion', True) - actor.skeletal_mesh_component.set_editor_property( - 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) - actor.skeletal_mesh_component.animation_data.set_editor_property( - 'anim_to_play', animation) - - return animation - - def load(self, context, name, namespace, options=None): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - root = "/Game/Ayon" - folder_path = context["folder"]["path"] - hierarchy = folder_path.lstrip("/").split("/") - folder_name = hierarchy.pop(-1) - product_type = context["product"]["productType"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/Animations/{folder_name}/{name}", suffix="") - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{hierarchy[0]}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - hierarchy_dir = root - for h in hierarchy: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir = f"{hierarchy_dir}/{folder_name}" - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{hierarchy_dir}/"], - recursive_paths=True) - levels = ar.get_assets(_filter) - level = levels[0].get_asset().get_path_name() - - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(level) - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - libpath = path.replace(".fbx", ".json") - - with open(libpath, "r") as fp: - data = json.load(fp) - - instance_name = data.get("instance_name") - - animation = self._process(path, asset_dir, asset_name, instance_name) - - asset_content = EditorAssetLibrary.list_assets( - hierarchy_dir, recursive=True, include_folder=False) - - # Get the sequence for the layout, excluding the camera one. - sequences = [a for a in asset_content - if (EditorAssetLibrary.find_asset_data(a).get_class() == - unreal.LevelSequence.static_class() and - "_camera" not in a.split("/")[-1])] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for s in sequences: - sequence = ar.get_asset_by_object_path(s).get_asset() - possessables = [ - p for p in sequence.get_possessables() - if p.get_display_name() == instance_name] - - for p in possessables: - tracks = [ - t for t in p.get_tracks() - if (t.get_class() == - MovieSceneSkeletalAnimationTrack.static_class())] - - for t in tracks: - sections = [ - s for s in t.get_sections() - if (s.get_class() == - MovieSceneSkeletalAnimationSection.static_class())] - - for s in sections: - s.params.set_editor_property('animation', animation) - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "folder_path": folder_path, - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - imported_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) - - for a in imported_content: - EditorAssetLibrary.save_asset(a) - - unreal.EditorLevelLibrary.save_current_level() - unreal.EditorLevelLibrary.load_level(master_level) - - def update(self, container, context): - repre_entity = context["representation"] - folder_name = container["asset_name"] - source_path = get_representation_path(repre_entity) - folder_entity = get_current_folder_entity(fields=["attrib.fps"]) - destination_path = container["namespace"] - - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - # strip suffix - task.set_editor_property('destination_name', folder_name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - skeletal_mesh = EditorAssetLibrary.load_asset( - container.get('namespace') + "/" + container.get('asset_name')) - skeleton = skeletal_mesh.get_editor_property('skeleton') - task.options.set_editor_property('skeleton', skeleton) - - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - EditorAssetLibrary.delete_directory(path) - - asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py b/client/ayon_core/hosts/unreal/plugins/load/load_camera.py deleted file mode 100644 index 681c83c6a1..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py +++ /dev/null @@ -1,591 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load camera from FBX.""" -from pathlib import Path - -import ayon_api - -import unreal -from unreal import ( - EditorAssetLibrary, - EditorLevelLibrary, - EditorLevelUtils, - LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, -) -from ayon_core.pipeline import ( - AYON_CONTAINER_ID, - get_current_project_name, - get_representation_path, -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - generate_sequence, - set_sequence_hierarchy, - create_container, - imprint, -) - - -class CameraLoader(plugin.Loader): - """Load Unreal StaticMesh from FBX""" - - product_types = {"camera"} - label = "Load Camera" - representations = {"fbx"} - icon = "cube" - color = "orange" - - def _import_camera( - self, world, sequence, bindings, import_fbx_settings, import_filename - ): - ue_version = unreal.SystemLibrary.get_engine_version().split('.') - ue_major = int(ue_version[0]) - ue_minor = int(ue_version[1]) - - if ue_major == 4 and ue_minor <= 26: - unreal.SequencerTools.import_fbx( - world, - sequence, - bindings, - import_fbx_settings, - import_filename - ) - elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5: - unreal.SequencerTools.import_level_sequence_fbx( - world, - sequence, - bindings, - import_fbx_settings, - import_filename - ) - else: - raise NotImplementedError( - f"Unreal version {ue_major} not supported") - - def load(self, context, name, namespace, data): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_attributes = folder_entity["attrib"] - folder_path = folder_entity["path"] - hierarchy_parts = folder_path.split("/") - # Remove empty string - hierarchy_parts.pop(0) - # Pop folder name - folder_name = hierarchy_parts.pop(-1) - - root = "/Game/Ayon" - hierarchy_dir = root - hierarchy_dir_list = [] - for h in hierarchy_parts: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir_list.append(hierarchy_dir) - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else name - - tools = unreal.AssetToolsHelpers().get_asset_tools() - - # Create a unique name for the camera directory - unique_number = 1 - if EditorAssetLibrary.does_directory_exist( - f"{hierarchy_dir}/{folder_name}" - ): - asset_content = EditorAssetLibrary.list_assets( - f"{root}/{folder_name}", recursive=False, include_folder=True - ) - - # Get highest number to make a unique name - folders = [a for a in asset_content - if a[-1] == "/" and f"{name}_" in a] - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] - f_numbers.sort() - unique_number = f_numbers[-1] + 1 if f_numbers else 1 - - asset_dir, container_name = tools.create_unique_asset_name( - f"{hierarchy_dir}/{folder_name}/{name}_{unique_number:02d}", suffix="") - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - # Create map for the shot, and create hierarchy of map. If the maps - # already exist, we will use them. - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy_dir[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - - level = ( - f"{asset_dir}/{folder_name}_map_camera.{folder_name}_map_camera" - ) - if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level( - f"{asset_dir}/{folder_name}_map_camera" - ) - - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic - ) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(level) - - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. - frame_ranges = [] - sequences = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy_parts): - root_content = EditorAssetLibrary.list_assets( - h_dir, recursive=False, include_folder=False) - - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] - - if existing_sequences: - for seq in existing_sequences: - sequences.append(seq.get_asset()) - frame_ranges.append(( - seq.get_asset().get_playback_start(), - seq.get_asset().get_playback_end())) - else: - sequence, frame_range = generate_sequence(h, h_dir) - - sequences.append(sequence) - frame_ranges.append(frame_range) - - EditorAssetLibrary.make_directory(asset_dir) - - cam_seq = tools.create_asset( - asset_name=f"{folder_name}_camera", - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - # Add sequences data to hierarchy - for i in range(len(sequences) - 1): - set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) - - clip_in = folder_attributes.get("clipIn") - clip_out = folder_attributes.get("clipOut") - - cam_seq.set_display_rate( - unreal.FrameRate(folder_attributes.get("fps"), 1.0)) - cam_seq.set_playback_start(clip_in) - cam_seq.set_playback_end(clip_out + 1) - set_sequence_hierarchy( - sequences[-1], cam_seq, - frame_ranges[-1][1], - clip_in, clip_out, - [level]) - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - if cam_seq: - path = self.filepath_from_context(context) - self._import_camera( - EditorLevelLibrary.get_editor_world(), - cam_seq, - cam_seq.get_bindings(), - settings, - path - ) - - # Set range of all sections - # Changing the range of the section is not enough. We need to change - # the frame of all the keys in the section. - for possessable in cam_seq.get_possessables(): - for tracks in possessable.get_tracks(): - for section in tracks.get_sections(): - section.set_range(clip_in, clip_out + 1) - for channel in section.get_all_channels(): - for key in channel.get_keys(): - old_time = key.get_time().get_editor_property( - 'frame_number') - old_time_value = old_time.get_editor_property( - 'value') - new_time = old_time_value + ( - clip_in - folder_attributes.get('frameStart') - ) - key.set_time(unreal.FrameNumber(value=new_time)) - - # Create Asset Container - create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_name, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(master_level) - - # Save all assets in the hierarchy - asset_content = EditorAssetLibrary.list_assets( - hierarchy_dir_list[0], recursive=True, include_folder=False - ) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - curr_level_sequence = LevelSequenceLib.get_current_level_sequence() - curr_time = LevelSequenceLib.get_current_time() - is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() - - editor_subsystem = unreal.UnrealEditorSubsystem() - vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() - - asset_dir = container.get('namespace') - - EditorLevelLibrary.save_current_level() - - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(_filter) - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=True) - maps = ar.get_assets(_filter) - - # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) - - level_sequence = sequences[0].get_asset() - - display_rate = level_sequence.get_display_rate() - playback_start = level_sequence.get_playback_start() - playback_end = level_sequence.get_playback_end() - - sequence_name = f"{container.get('asset')}_camera" - - # Get the actors in the level sequence. - objs = unreal.SequencerTools.get_bound_objects( - unreal.EditorLevelLibrary.get_editor_world(), - level_sequence, - level_sequence.get_bindings(), - unreal.SequencerScriptingRange( - has_start_value=True, - has_end_value=True, - inclusive_start=level_sequence.get_playback_start(), - exclusive_end=level_sequence.get_playback_end() - ) - ) - - # Delete actors from the map - for o in objs: - if o.bound_objects[0].get_class().get_name() == "CineCameraActor": - actor_path = o.bound_objects[0].get_path_name().split(":")[-1] - actor = EditorLevelLibrary.get_actor_reference(actor_path) - EditorLevelLibrary.destroy_actor(actor) - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - root = "/Game/Ayon" - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - sequences = [master_sequence] - - parent = None - sub_scene = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if ss.get_sequence().get_name() == sequence_name: - parent = s - sub_scene = ss - break - sequences.append(ss.get_sequence()) - for i, ss in enumerate(sections): - ss.set_row_index(i) - if parent: - break - - assert parent, "Could not find the parent sequence" - - EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - tools = unreal.AssetToolsHelpers().get_asset_tools() - new_sequence = tools.create_asset( - asset_name=sequence_name, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - new_sequence.set_display_rate(display_rate) - new_sequence.set_playback_start(playback_start) - new_sequence.set_playback_end(playback_end) - - sub_scene.set_sequence(new_sequence) - - repre_entity = context["representation"] - repre_path = get_representation_path(repre_entity) - self._import_camera( - EditorLevelLibrary.get_editor_world(), - new_sequence, - new_sequence.get_bindings(), - settings, - repre_path - ) - - # Set range of all sections - # Changing the range of the section is not enough. We need to change - # the frame of all the keys in the section. - project_name = get_current_project_name() - folder_path = container.get("folder_path") - if folder_path is None: - folder_path = container.get("asset") - folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - folder_attributes = folder_entity["attrib"] - - clip_in = folder_attributes["clipIn"] - clip_out = folder_attributes["clipOut"] - frame_start = folder_attributes["frameStart"] - for possessable in new_sequence.get_possessables(): - for tracks in possessable.get_tracks(): - for section in tracks.get_sections(): - section.set_range(clip_in, clip_out + 1) - for channel in section.get_all_channels(): - for key in channel.get_keys(): - old_time = key.get_time().get_editor_property( - 'frame_number') - old_time_value = old_time.get_editor_property( - 'value') - new_time = old_time_value + ( - clip_in - frame_start - ) - key.set_time(unreal.FrameNumber(value=new_time)) - - data = { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - } - imprint(f"{asset_dir}/{container.get('container_name')}", data) - - EditorLevelLibrary.save_current_level() - - asset_content = EditorAssetLibrary.list_assets( - f"{root}/{ms_asset}", recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - EditorLevelLibrary.load_level(master_level) - - if curr_level_sequence: - LevelSequenceLib.open_level_sequence(curr_level_sequence) - LevelSequenceLib.set_current_time(curr_time) - LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) - - editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) - - def remove(self, container): - asset_dir = container.get('namespace') - path = Path(asset_dir) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(_filter) - - if not sequences: - raise Exception("Could not find sequence.") - - world = ar.get_asset_by_object_path( - EditorLevelLibrary.get_editor_world().get_path_name()) - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=True) - maps = ar.get_assets(_filter) - - # There should be only one map in the list - if not maps: - raise Exception("Could not find map.") - - map = maps[0] - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(map.get_asset().get_path_name()) - - # Remove the camera from the level. - actors = EditorLevelLibrary.get_all_level_actors() - - for a in actors: - if a.__class__ == unreal.CineCameraActor: - EditorLevelLibrary.destroy_actor(a) - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(world.get_asset().get_path_name()) - - # There should be only one sequence in the path. - sequence_name = sequences[0].asset_name - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - root = "/Game/Ayon" - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() - - sequences = [master_sequence] - - parent = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if ss.get_sequence().get_name() == sequence_name: - parent = s - subscene_track.remove_section(ss) - break - sequences.append(ss.get_sequence()) - # Update subscenes indexes. - for i, ss in enumerate(sections): - ss.set_row_index(i) - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map_camera") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() - if parent: - break - - assert parent, "Could not find the parent sequence" - - # Create a temporary level to delete the layout level. - EditorLevelLibrary.save_all_dirty_levels() - EditorAssetLibrary.make_directory(f"{root}/tmp") - tmp_level = f"{root}/tmp/temp_map" - if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): - EditorLevelLibrary.new_level(tmp_level) - else: - EditorLevelLibrary.load_level(tmp_level) - - # Delete the layout directory. - EditorAssetLibrary.delete_directory(asset_dir) - - EditorLevelLibrary.load_level(master_level) - EditorAssetLibrary.delete_directory(f"{root}/tmp") - - # Check if there isn't any more assets in the parent folder, and - # delete it if not. - asset_content = EditorAssetLibrary.list_assets( - path.parent.as_posix(), recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py deleted file mode 100644 index ae7d41192a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for published alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) - -import unreal # noqa - - -class PointCacheAlembicLoader(plugin.Loader): - """Load Point Cache from Alembic""" - - product_types = {"model", "pointcache"} - label = "Import Alembic Point Cache" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task( - filename, asset_dir, asset_name, replace, - frame_start=None, frame_end=None - ): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - gc_settings = unreal.AbcGeometryCacheSettings() - conversion_settings = unreal.AbcConversionSettings() - sampling_settings = unreal.AbcSamplingSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.GEOMETRY_CACHE) - - gc_settings.set_editor_property('flatten_tracks', False) - - conversion_settings.set_editor_property('flip_u', False) - conversion_settings.set_editor_property('flip_v', True) - conversion_settings.set_editor_property( - 'scale', unreal.Vector(x=100.0, y=100.0, z=100.0)) - conversion_settings.set_editor_property( - 'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0)) - - if frame_start is not None: - sampling_settings.set_editor_property('frame_start', frame_start) - if frame_end is not None: - sampling_settings.set_editor_property('frame_end', frame_end) - - options.geometry_cache_settings = gc_settings - options.conversion_settings = conversion_settings - options.sampling_settings = sampling_settings - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - frame_start, frame_end - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, frame_start, frame_end) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - frame_start, - frame_end, - product_type, - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "frame_start": frame_start, - "frame_end": frame_end, - "product_type": product_type, - "folder_path": folder_path, - # TODO these should be probably removed - "family": product_type, - "asset": folder_path, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_path = folder_entity["path"] - folder_name = folder_entity["name"] - folder_attributes = folder_entity["attrib"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - frame_start = folder_attributes.get("frameStart") - frame_end = folder_attributes.get("frameEnd") - - # If frame start and end are the same, we increase the end frame by - # one, otherwise Unreal will not import it - if frame_start == frame_end: - frame_end += 1 - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name, - frame_start, frame_end) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - frame_start, - frame_end, - context["product"]["productType"] - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - # Create directory for folder and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - frame_start = int(container.get("frame_start")) - frame_end = int(container.get("frame_end")) - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name, - frame_start, frame_end) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - frame_start, - frame_end, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py deleted file mode 100644 index 49d95c6459..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py +++ /dev/null @@ -1,916 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for layouts.""" -import json -import collections -from pathlib import Path - -import unreal -from unreal import ( - EditorAssetLibrary, - EditorLevelLibrary, - EditorLevelUtils, - AssetToolsHelpers, - FBXImportType, - MovieSceneLevelVisibilityTrack, - MovieSceneSubTrack, - LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, -) -import ayon_api - -from ayon_core.pipeline import ( - discover_loader_plugins, - loaders_from_representation, - load_container, - get_representation_path, - AYON_CONTAINER_ID, - get_current_project_name, -) -from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.settings import get_current_project_settings -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - generate_sequence, - set_sequence_hierarchy, - create_container, - imprint, - ls, -) - - -class LayoutLoader(plugin.Loader): - """Load Layout from a JSON file""" - - product_types = {"layout"} - representations = {"json"} - - label = "Load Layout" - icon = "code-fork" - color = "orange" - ASSET_ROOT = "/Game/Ayon" - - def _get_asset_containers(self, path): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_content = EditorAssetLibrary.list_assets( - path, recursive=True) - - asset_containers = [] - - # Get all the asset containers - for a in asset_content: - obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == 'AyonAssetContainer': - asset_containers.append(obj) - - return asset_containers - - @staticmethod - def _get_fbx_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshFBXLoader" - elif family == 'model': - name = "StaticMeshFBXLoader" - elif family == 'camera': - name = "CameraLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - @staticmethod - def _get_abc_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': - name = "StaticMeshAlembicLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - def _transform_from_basis(self, transform, basis): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return new_transform.transform() - - def _process_family( - self, assets, class_name, transform, basis, sequence, inst_name=None - ): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - actors = [] - bindings = [] - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == class_name: - t = self._transform_from_basis(transform, basis) - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, t.translation - ) - actor.set_actor_rotation(t.rotation.rotator(), False) - actor.set_actor_scale3d(t.scale3d) - - if class_name == 'SkeletalMesh': - skm_comp = actor.get_editor_property( - 'skeletal_mesh_component') - skm_comp.set_bounds_scale(10.0) - - actors.append(actor) - - if sequence: - binding = None - for p in sequence.get_possessables(): - if p.get_name() == actor.get_name(): - binding = p - break - - if not binding: - binding = sequence.add_possessable(actor) - - bindings.append(binding) - - return actors, bindings - - def _import_animation( - self, asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence - ): - anim_file = Path(animation_file) - anim_file_name = anim_file.with_suffix('') - - anim_path = f"{asset_dir}/animations/{anim_file_name}" - - folder_entity = get_current_folder_entity() - # Import animation - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - task.set_editor_property( - 'filename', str(path.with_suffix(f".{animation_file}"))) - task.set_editor_property('destination_path', anim_path) - task.set_editor_property( - 'destination_name', f"{instance_name}_animation") - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', False) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - task.options.set_editor_property('skeleton', skeleton) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - asset_content = unreal.EditorAssetLibrary.list_assets( - anim_path, recursive=False, include_folder=False - ) - - animation = None - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) - imported_asset = unreal.AssetRegistryHelpers.get_asset( - imported_asset_data) - if imported_asset.__class__ == unreal.AnimSequence: - animation = imported_asset - break - - if animation: - actor = None - if actors_dict.get(instance_name): - for a in actors_dict.get(instance_name): - if a.get_class().get_name() == 'SkeletalMeshActor': - actor = a - break - - animation.set_editor_property('enable_root_motion', True) - actor.skeletal_mesh_component.set_editor_property( - 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) - actor.skeletal_mesh_component.animation_data.set_editor_property( - 'anim_to_play', animation) - - if sequence: - # Add animation to the sequencer - bindings = bindings_dict.get(instance_name) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for binding in bindings: - tracks = binding.get_tracks() - track = None - track = tracks[0] if tracks else binding.add_track( - unreal.MovieSceneSkeletalAnimationTrack) - - sections = track.get_sections() - section = None - if not sections: - section = track.add_section() - else: - section = sections[0] - - sec_params = section.get_editor_property('params') - curr_anim = sec_params.get_editor_property('animation') - - if curr_anim: - # Checks if the animation path has a container. - # If it does, it means that the animation is - # already in the sequencer. - anim_path = str(Path( - curr_anim.get_path_name()).parent - ).replace('\\', '/') - - _filter = unreal.ARFilter( - class_names=["AyonAssetContainer"], - package_paths=[anim_path], - recursive_paths=False) - containers = ar.get_assets(_filter) - - if len(containers) > 0: - return - - section.set_range( - sequence.get_playback_start(), - sequence.get_playback_end()) - sec_params = section.get_editor_property('params') - sec_params.set_editor_property('animation', animation) - - def _get_repre_entities_by_version_id(self, data): - version_ids = { - element.get("version") - for element in data - if element.get("representation") - } - version_ids.discard(None) - - output = collections.defaultdict(list) - if not version_ids: - return output - - project_name = get_current_project_name() - repre_entities = ayon_api.get_representations( - project_name, - representation_names={"fbx", "abc"}, - version_ids=version_ids, - fields={"id", "versionId", "name"} - ) - for repre_entity in repre_entities: - version_id = repre_entity["versionId"] - output[version_id].append(repre_entity) - return output - - def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - with open(lib_path, "r") as fp: - data = json.load(fp) - - all_loaders = discover_loader_plugins() - - if not repr_loaded: - repr_loaded = [] - - path = Path(lib_path) - - skeleton_dict = {} - actors_dict = {} - bindings_dict = {} - - loaded_assets = [] - - repre_entities_by_version_id = self._get_repre_entities_by_version_id( - data - ) - for element in data: - repre_id = None - repr_format = None - if element.get('representation'): - version_id = element.get("version") - repre_entities = repre_entities_by_version_id[version_id] - if not repre_entities: - self.log.error( - f"No valid representation found for version" - f" {version_id}") - continue - repre_entity = repre_entities[0] - repre_id = repre_entity["id"] - repr_format = repre_entity["name"] - - # This is to keep compatibility with old versions of the - # json format. - elif element.get('reference_fbx'): - repre_id = element.get('reference_fbx') - repr_format = 'fbx' - elif element.get('reference_abc'): - repre_id = element.get('reference_abc') - repr_format = 'abc' - - # If reference is None, this element is skipped, as it cannot be - # imported in Unreal - if not repre_id: - continue - - instance_name = element.get('instance_name') - - skeleton = None - - if repre_id not in repr_loaded: - repr_loaded.append(repre_id) - - product_type = element.get("product_type") - if product_type is None: - product_type = element.get("family") - loaders = loaders_from_representation( - all_loaders, repre_id) - - loader = None - - if repr_format == 'fbx': - loader = self._get_fbx_loader(loaders, product_type) - elif repr_format == 'abc': - loader = self._get_abc_loader(loaders, product_type) - - if not loader: - self.log.error( - f"No valid loader found for {repre_id}") - continue - - options = { - # "asset_dir": asset_dir - } - - assets = load_container( - loader, - repre_id, - namespace=instance_name, - options=options - ) - - container = None - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'AyonAssetContainer': - container = obj - if obj.get_class().get_name() == 'Skeleton': - skeleton = obj - - loaded_assets.append(container.get_path_name()) - - instances = [ - item for item in data - if ((item.get('version') and - item.get('version') == element.get('version')) or - item.get('reference_fbx') == repre_id or - item.get('reference_abc') == repre_id)] - - for instance in instances: - # transform = instance.get('transform') - transform = instance.get('transform_matrix') - basis = instance.get('basis') - inst = instance.get('instance_name') - - actors = [] - - if product_type == 'model': - actors, _ = self._process_family( - assets, 'StaticMesh', transform, basis, - sequence, inst - ) - elif product_type == 'rig': - actors, bindings = self._process_family( - assets, 'SkeletalMesh', transform, basis, - sequence, inst - ) - actors_dict[inst] = actors - bindings_dict[inst] = bindings - - if skeleton: - skeleton_dict[repre_id] = skeleton - else: - skeleton = skeleton_dict.get(repre_id) - - animation_file = element.get('animation') - - if animation_file and skeleton: - self._import_animation( - asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence) - - return loaded_assets - - @staticmethod - def _remove_family(assets, components, class_name, prop_name): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - objects = [] - for a in assets: - obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == class_name: - objects.append(obj) - for obj in objects: - for comp in components: - if comp.get_editor_property(prop_name) == obj.get_asset(): - comp.get_owner().destroy_actor() - - def _remove_actors(self, path): - asset_containers = self._get_asset_containers(path) - - # Get all the static and skeletal meshes components in the level - components = EditorLevelLibrary.get_all_level_actors_components() - static_meshes_comp = [ - c for c in components - if c.get_class().get_name() == 'StaticMeshComponent'] - skel_meshes_comp = [ - c for c in components - if c.get_class().get_name() == 'SkeletalMeshComponent'] - - # For all the asset containers, get the static and skeletal meshes. - # Then, check the components in the level and destroy the matching - # actors. - for asset_container in asset_containers: - package_path = asset_container.get_editor_property('package_path') - family = EditorAssetLibrary.get_metadata_tag( - asset_container.get_asset(), "family") - assets = EditorAssetLibrary.list_assets( - str(package_path), recursive=False) - if family == 'model': - self._remove_family( - assets, static_meshes_comp, 'StaticMesh', 'static_mesh') - elif family == 'rig': - self._remove_family( - assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh') - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_path = folder_entity["path"] - hierarchy = folder_path.lstrip("/").split("/") - # Remove folder name - folder_name = hierarchy.pop(-1) - root = self.ASSET_ROOT - hierarchy_dir = root - hierarchy_dir_list = [] - for h in hierarchy: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir_list.append(hierarchy_dir) - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else name - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(hierarchy_dir, folder_name, name), - suffix="" - ) - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - master_level = None - shot = None - sequences = [] - - level = f"{asset_dir}/{folder_name}_map.{folder_name}_map" - EditorLevelLibrary.new_level(f"{asset_dir}/{folder_name}_map") - - if create_sequences: - # Create map for the shot, and create hierarchy of map. If the - # maps already exist, we will use them. - if hierarchy: - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - - if master_level: - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic - ) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(level) - - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. - frame_ranges = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): - root_content = EditorAssetLibrary.list_assets( - h_dir, recursive=False, include_folder=False) - - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] - - if not existing_sequences: - sequence, frame_range = generate_sequence(h, h_dir) - - sequences.append(sequence) - frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) - - shot = tools.create_asset( - asset_name=folder_name, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - # sequences and frame_ranges have the same length - for i in range(0, len(sequences) - 1): - set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) - - project_name = get_current_project_name() - folder_attributes = ( - ayon_api.get_folder_by_path(project_name, folder_path)["attrib"] - ) - shot.set_display_rate( - unreal.FrameRate(folder_attributes.get("fps"), 1.0)) - shot.set_playback_start(0) - shot.set_playback_end( - folder_attributes.get('clipOut') - - folder_attributes.get('clipIn') - + 1 - ) - if sequences: - set_sequence_hierarchy( - sequences[-1], - shot, - frame_ranges[-1][1], - folder_attributes.get('clipIn'), - folder_attributes.get('clipOut'), - [level]) - - EditorLevelLibrary.load_level(level) - - path = self.filepath_from_context(context) - loaded_assets = self._process(path, asset_dir, shot) - - for s in sequences: - EditorAssetLibrary.save_asset(s.get_path_name()) - - EditorLevelLibrary.save_current_level() - - # Create Asset Container - create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": folder_name, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "family": context["product"]["productType"], - "loaded_assets": loaded_assets - } - imprint( - "{}/{}".format(asset_dir, container_name), data) - - save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir - - asset_content = EditorAssetLibrary.list_assets( - save_dir, recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - if master_level: - EditorLevelLibrary.load_level(master_level) - - return asset_content - - def update(self, container, context): - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - curr_level_sequence = LevelSequenceLib.get_current_level_sequence() - curr_time = LevelSequenceLib.get_current_time() - is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() - - editor_subsystem = unreal.UnrealEditorSubsystem() - vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() - - root = "/Game/Ayon" - - asset_dir = container.get('namespace') - - folder_entity = context["folder"] - repre_entity = context["representation"] - - hierarchy = folder_entity["path"].lstrip("/").split("/") - first_parent_name = hierarchy[0] - - sequence = None - master_level = None - - if create_sequences: - h_dir = f"{root}/{first_parent_name}" - h_asset = first_parent_name - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(filter) - sequence = sequences[0].get_asset() - - prev_level = None - - if not master_level: - curr_level = unreal.LevelEditorSubsystem().get_current_level() - curr_level_path = curr_level.get_outer().get_path_name() - # If the level path does not start with "/Game/", the current - # level is a temporary, unsaved level. - if curr_level_path.startswith("/Game/"): - prev_level = curr_level_path - - # Get layout level - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=False) - levels = ar.get_assets(filter) - - layout_level = levels[0].get_asset().get_path_name() - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(layout_level) - - # Delete all the actors in the level - actors = unreal.EditorLevelLibrary.get_all_level_actors() - for actor in actors: - unreal.EditorLevelLibrary.destroy_actor(actor) - - if create_sequences: - EditorLevelLibrary.save_current_level() - - EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") - - source_path = get_representation_path(repre_entity) - - loaded_assets = self._process(source_path, asset_dir, sequence) - - data = { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - "loaded_assets": loaded_assets, - } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) - - EditorLevelLibrary.save_current_level() - - save_dir = f"{root}/{first_parent_name}" if create_sequences else asset_dir - - asset_content = EditorAssetLibrary.list_assets( - save_dir, recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - if master_level: - EditorLevelLibrary.load_level(master_level) - elif prev_level: - EditorLevelLibrary.load_level(prev_level) - - if curr_level_sequence: - LevelSequenceLib.open_level_sequence(curr_level_sequence) - LevelSequenceLib.set_current_time(curr_time) - LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) - - editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) - - def remove(self, container): - """ - Delete the layout. First, check if the assets loaded with the layout - are used by other layouts. If not, delete the assets. - """ - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - root = "/Game/Ayon" - path = Path(container.get("namespace")) - - containers = ls() - layout_containers = [ - c for c in containers - if (c.get('asset_name') != container.get('asset_name') and - c.get('family') == "layout")] - - # Check if the assets have been loaded by other layouts, and deletes - # them if they haven't. - for asset in eval(container.get('loaded_assets')): - layouts = [ - lc for lc in layout_containers - if asset in lc.get('loaded_assets')] - - if not layouts: - EditorAssetLibrary.delete_directory(str(Path(asset).parent)) - - # Delete the parent folder if there aren't any more - # layouts in it. - asset_content = EditorAssetLibrary.list_assets( - str(Path(asset).parent.parent), recursive=False, - include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory( - str(Path(asset).parent.parent)) - - master_sequence = None - master_level = None - sequences = [] - - if create_sequences: - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to - # find the level sequence. - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - ar = unreal.AssetRegistryHelpers.get_asset_registry() - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - sequences = [master_sequence] - - parent = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): - parent = s - subscene_track.remove_section(ss) - break - sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: - ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() - if parent: - break - - assert parent, "Could not find the parent sequence" - - # Create a temporary level to delete the layout level. - EditorLevelLibrary.save_all_dirty_levels() - EditorAssetLibrary.make_directory(f"{root}/tmp") - tmp_level = f"{root}/tmp/temp_map" - if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): - EditorLevelLibrary.new_level(tmp_level) - else: - EditorLevelLibrary.load_level(tmp_level) - - # Delete the layout directory. - EditorAssetLibrary.delete_directory(str(path)) - - if create_sequences: - EditorLevelLibrary.load_level(master_level) - EditorAssetLibrary.delete_directory(f"{root}/tmp") - - # Delete the parent folder if there aren't any more layouts in it. - asset_content = EditorAssetLibrary.list_assets( - str(path.parent), recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(str(path.parent)) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py deleted file mode 100644 index f9d438367b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py +++ /dev/null @@ -1,451 +0,0 @@ -import json -from pathlib import Path - -import unreal -from unreal import EditorLevelLibrary -import ayon_api - -from ayon_core.pipeline import ( - discover_loader_plugins, - loaders_from_representation, - load_container, - get_representation_path, - AYON_CONTAINER_ID, -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as upipeline - - -class ExistingLayoutLoader(plugin.Loader): - """ - Load Layout for an existing scene, and match the existing assets. - """ - - product_types = {"layout"} - representations = {"json"} - - label = "Load Layout on Existing Scene" - icon = "code-fork" - color = "orange" - ASSET_ROOT = "/Game/Ayon" - - delete_unmatched_assets = True - - @classmethod - def apply_settings(cls, project_settings): - super(ExistingLayoutLoader, cls).apply_settings( - project_settings - ) - cls.delete_unmatched_assets = ( - project_settings["unreal"]["delete_unmatched_assets"] - ) - - @staticmethod - def _create_container( - asset_name, - asset_dir, - folder_path, - representation, - version_id, - product_type - ): - container_name = f"{asset_name}_CON" - - if not unreal.EditorAssetLibrary.does_asset_exist( - f"{asset_dir}/{container_name}" - ): - container = upipeline.create_container(container_name, asset_dir) - else: - ar = unreal.AssetRegistryHelpers.get_asset_registry() - obj = ar.get_asset_by_object_path( - f"{asset_dir}/{container_name}.{container_name}") - container = obj.get_asset() - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - # "loader": str(self.__class__.__name__), - "representation": representation, - "parent": version_id, - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - - upipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) - - return container.get_path_name() - - @staticmethod - def _get_current_level(): - ue_version = unreal.SystemLibrary.get_engine_version().split('.') - ue_major = ue_version[0] - - if ue_major == '4': - return EditorLevelLibrary.get_editor_world() - elif ue_major == '5': - return unreal.LevelEditorSubsystem().get_current_level() - - raise NotImplementedError( - f"Unreal version {ue_major} not supported") - - def _transform_from_basis(self, transform, basis): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return new_transform.transform() - - def _spawn_actor(self, obj, lasset): - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - - transform = lasset.get('transform_matrix') - basis = lasset.get('basis') - - computed_transform = self._transform_from_basis(transform, basis) - - actor.set_actor_transform(computed_transform, False, True) - - @staticmethod - def _get_fbx_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshFBXLoader" - elif family == 'model' or family == 'staticMesh': - name = "StaticMeshFBXLoader" - elif family == 'camera': - name = "CameraLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - @staticmethod - def _get_abc_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': - name = "StaticMeshAlembicLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - def _load_asset(self, repr_data, representation, instance_name, family): - repr_format = repr_data.get('name') - - all_loaders = discover_loader_plugins() - loaders = loaders_from_representation( - all_loaders, representation) - - loader = None - - if repr_format == 'fbx': - loader = self._get_fbx_loader(loaders, family) - elif repr_format == 'abc': - loader = self._get_abc_loader(loaders, family) - - if not loader: - self.log.error(f"No valid loader found for {representation}") - return [] - - # This option is necessary to avoid importing the assets with a - # different conversion compared to the other assets. For ABC files, - # it is in fact impossible to access the conversion settings. So, - # we must assume that the Maya conversion settings have been applied. - options = { - "default_conversion": True - } - - assets = load_container( - loader, - representation, - namespace=instance_name, - options=options - ) - - return assets - - def _get_valid_repre_entities(self, project_name, version_ids): - valid_formats = ['fbx', 'abc'] - - repre_entities = list(ayon_api.get_representations( - project_name, - representation_names=valid_formats, - version_ids=version_ids - )) - repre_entities_by_version_id = {} - for repre_entity in repre_entities: - version_id = repre_entity["versionId"] - repre_entities_by_version_id[version_id] = repre_entity - return repre_entities_by_version_id - - def _process(self, lib_path, project_name): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - actors = EditorLevelLibrary.get_all_level_actors() - - with open(lib_path, "r") as fp: - data = json.load(fp) - - elements = [] - repre_ids = set() - # Get all the representations in the JSON from the database. - for element in data: - repre_id = element.get('representation') - if repre_id: - repre_ids.add(repre_id) - elements.append(element) - - repre_entities = ayon_api.get_representations( - project_name, representation_ids=repre_ids - ) - repre_entities_by_id = { - repre_entity["id"]: repre_entity - for repre_entity in repre_entities - } - layout_data = [] - version_ids = set() - for element in elements: - repre_id = element.get("representation") - repre_entity = repre_entities_by_id.get(repre_id) - if not repre_entity: - raise AssertionError("Representation not found") - if not ( - repre_entity.get("attrib") - or repre_entity["attrib"].get("path") - ): - raise AssertionError("Representation does not have path") - if not repre_entity.get('context'): - raise AssertionError("Representation does not have context") - - layout_data.append((repre_entity, element)) - version_ids.add(repre_entity["versionId"]) - - repre_parents_by_id = ayon_api.get_representation_parents( - project_name, repre_entities_by_id.keys() - ) - - # Prequery valid repre documents for all elements at once - valid_repre_entities_by_version_id = self._get_valid_repre_entities( - project_name, version_ids) - containers = [] - actors_matched = [] - - for (repre_entity, lasset) in layout_data: - # For every actor in the scene, check if it has a representation in - # those we got from the JSON. If so, create a container for it. - # Otherwise, remove it from the scene. - found = False - repre_id = repre_entity["id"] - repre_parents = repre_parents_by_id[repre_id] - folder_path = repre_parents.folder["path"] - folder_name = repre_parents.folder["name"] - product_name = repre_parents.product["name"] - product_type = repre_parents.product["productType"] - - for actor in actors: - if not actor.get_class().get_name() == 'StaticMeshActor': - continue - if actor in actors_matched: - continue - - # Get the original path of the file from which the asset has - # been imported. - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - if (not path.name or - path.name not in repre_entity["attrib"]["path"]): - continue - - actor.set_actor_label(lasset.get('instance_name')) - - mesh_path = Path(mesh.get_path_name()).parent.as_posix() - - # Create the container for the asset. - container = self._create_container( - f"{folder_name}_{product_name}", - mesh_path, - folder_path, - repre_entity["id"], - repre_entity["versionId"], - product_type - ) - containers.append(container) - - # Set the transform for the actor. - transform = lasset.get('transform_matrix') - basis = lasset.get('basis') - - computed_transform = self._transform_from_basis( - transform, basis) - actor.set_actor_transform(computed_transform, False, True) - - actors_matched.append(actor) - found = True - break - - # If an actor has not been found for this representation, - # we check if it has been loaded already by checking all the - # loaded containers. If so, we add it to the scene. Otherwise, - # we load it. - if found: - continue - - all_containers = upipeline.ls() - - loaded = False - - for container in all_containers: - repre_id = container.get('representation') - - if not repre_id == repre_entity["id"]: - continue - - asset_dir = container.get('namespace') - - arfilter = unreal.ARFilter( - class_names=["StaticMesh"], - package_paths=[asset_dir], - recursive_paths=False) - assets = ar.get_assets(arfilter) - - for asset in assets: - obj = asset.get_asset() - self._spawn_actor(obj, lasset) - - loaded = True - break - - # If the asset has not been loaded yet, we load it. - if loaded: - continue - - version_id = lasset.get('version') - assets = self._load_asset( - valid_repre_entities_by_version_id.get(version_id), - lasset.get('representation'), - lasset.get('instance_name'), - lasset.get('family') - ) - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'StaticMesh': - continue - self._spawn_actor(obj, lasset) - - break - - # Check if an actor was not matched to a representation. - # If so, remove it from the scene. - for actor in actors: - if not actor.get_class().get_name() == 'StaticMeshActor': - continue - if actor not in actors_matched: - self.log.warning(f"Actor {actor.get_name()} not matched.") - if self.delete_unmatched_assets: - EditorLevelLibrary.destroy_actor(actor) - - return containers - - def load(self, context, name, namespace, options): - print("Loading Layout and Match Assets") - - folder_name = context["folder"]["name"] - folder_path = context["folder"]["path"] - product_type = context["product"]["productType"] - asset_name = f"{folder_name}_{name}" if folder_name else name - container_name = f"{folder_name}_{name}_CON" - - curr_level = self._get_current_level() - - if not curr_level: - raise AssertionError("Current level not saved") - - project_name = context["project"]["name"] - path = self.filepath_from_context(context) - containers = self._process(path, project_name) - - curr_level_path = Path( - curr_level.get_outer().get_path_name()).parent.as_posix() - - if not unreal.EditorAssetLibrary.does_asset_exist( - f"{curr_level_path}/{container_name}" - ): - upipeline.create_container( - container=container_name, path=curr_level_path) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": curr_level_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - "loaded_assets": containers, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - upipeline.imprint(f"{curr_level_path}/{container_name}", data) - - def update(self, container, context): - asset_dir = container.get('namespace') - - project_name = context["project"]["name"] - repre_entity = context["representation"] - - source_path = get_representation_path(repre_entity) - containers = self._process(source_path, project_name) - - data = { - "representation": repre_entity["id"], - "loaded_assets": containers, - "parent": repre_entity["versionId"], - } - upipeline.imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py deleted file mode 100644 index dfc5d58708..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ /dev/null @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Skeletal Mesh alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class SkeletalMeshAlembicLoader(plugin.Loader): - """Load Unreal SkeletalMesh from Alembic""" - - product_types = {"pointcache", "skeletalMesh"} - label = "Import Alembic Skeletal Mesh" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace, default_conversion): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - if not default_conversion: - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - options.conversion_settings = conversion_settings - - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - default_conversion=False - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, default_conversion) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - default_conversion = False - if options.get("default_conversion"): - default_conversion = options.get("default_conversion") - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name, default_conversion) - - product_type = context["product"]["productType"] - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for folder and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type, - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py deleted file mode 100644 index 513404ab98..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Skeletal Meshes form FBX.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class SkeletalMeshFBXLoader(plugin.Loader): - """Load Unreal SkeletalMesh from FBX.""" - - product_types = {"rig", "skeletalMesh"} - label = "Import FBX Skeletal Mesh" - representations = {"fbx"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.FbxImportUI() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'automated_import_should_detect_type', False) - options.set_editor_property('import_as_skeletal', True) - options.set_editor_property('import_animations', False) - options.set_editor_property('import_mesh', True) - options.set_editor_property('import_materials', False) - options.set_editor_property('import_textures', False) - options.set_editor_property('skeleton', None) - options.set_editor_property('create_physics_asset', False) - - options.set_editor_property( - 'mesh_type_to_import', - unreal.FBXImportType.FBXIT_SKELETAL_MESH) - - options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_ALL) - - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) - - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_name = context["folder"]["name"] - product_type = context["product"]["productType"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version_entity = context["version"] - # Check if version is hero version and use different name - version = version_entity["version"] - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="" - ) - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_name, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py deleted file mode 100644 index 0bf6ce9eaa..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for Static Mesh alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class StaticMeshAlembicLoader(plugin.Loader): - """Load Unreal StaticMesh from Alembic""" - - product_types = {"model", "staticMesh"} - label = "Import Alembic Static Mesh" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace, default_conversion): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - sm_settings = unreal.AbcStaticMeshSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options.set_editor_property( - 'import_type', unreal.AlembicImportType.STATIC_MESH) - - sm_settings.set_editor_property('merge_meshes', True) - - if not default_conversion: - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - options.conversion_settings = conversion_settings - - options.static_mesh_settings = sm_settings - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - default_conversion=False - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, default_conversion) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type, - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["path"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - default_conversion = False - if options.get("default_conversion"): - default_conversion = options.get("default_conversion") - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name, default_conversion) - - product_type = context["product"]["productType"] - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py deleted file mode 100644 index b7bb57ac23..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Static meshes form FBX.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class StaticMeshFBXLoader(plugin.Loader): - """Load Unreal StaticMesh from FBX.""" - - product_types = {"model", "staticMesh"} - label = "Import FBX Static Mesh" - representations = {"fbx"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.FbxImportUI() - import_data = unreal.FbxStaticMeshImportData() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - options.set_editor_property( - 'automated_import_should_detect_type', False) - options.set_editor_property('import_animations', False) - - import_data.set_editor_property('combine_meshes', True) - import_data.set_editor_property('remove_degenerates', False) - - options.static_mesh_import_data = import_data - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "folder_path": folder_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="" - ) - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - context["product"]["productType"] - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type, - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py b/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py deleted file mode 100644 index 63f23ecc11..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load UAsset.""" -from pathlib import Path -import shutil - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UAssetLoader(plugin.Loader): - """Load UAsset.""" - - product_types = {"uasset"} - label = "Load UAsset" - representations = {"uasset"} - icon = "cube" - color = "orange" - - extension = "uasset" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name}", suffix="" - ) - - unique_number = 1 - while unreal.EditorAssetLibrary.does_directory_exist( - f"{asset_dir}_{unique_number:02}" - ): - unique_number += 1 - - asset_dir = f"{asset_dir}_{unique_number:02}" - container_name = f"{container_name}_{unique_number:02}{suffix}" - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - - path = self.filepath_from_context(context) - shutil.copy( - path, - f"{destination_path}/{name}_{unique_number:02}.{self.extension}") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "folder_path": folder_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - - product_name = context["product"]["name"] - repre_entity = context["representation"] - - unique_number = container["container_name"].split("_")[-2] - - destination_path = asset_dir.replace( - "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != "AyonAssetContainer": - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(repre_entity) - - shutil.copy( - update_filepath, - f"{destination_path}/{product_name}_{unique_number}.{self.extension}" - ) - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - } - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) - - -class UMapLoader(UAssetLoader): - """Load Level.""" - - product_types = {"uasset"} - label = "Load Level" - representations = {"umap"} - - extension = "umap" diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py b/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py deleted file mode 100644 index 708fc83745..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for Yeti Cache.""" -import os -import json - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class YetiLoader(plugin.Loader): - """Load Yeti Cache""" - - product_types = {"yeticacheUE"} - label = "Import Yeti" - representations = {"abc"} - icon = "pagelines" - color = "orange" - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - task.options = options - - return task - - @staticmethod - def is_groom_module_active(): - """ - Check if Groom plugin is active. - - This is a workaround, because the Unreal python API don't have - any method to check if plugin is active. - """ - prj_file = unreal.Paths.get_project_file_path() - - with open(prj_file, "r") as fp: - data = json.load(fp) - - plugins = data.get("Plugins") - - if not plugins: - return False - - plugin_names = [p.get("Name") for p in plugins] - - return "HairStrands" in plugin_names - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - - """ - # Check if Groom plugin is active - if not self.is_groom_module_active(): - raise RuntimeError("Groom plugin is not activated.") - - # Create directory for asset and Ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name}", suffix="") - - unique_number = 1 - while unreal.EditorAssetLibrary.does_directory_exist( - f"{asset_dir}_{unique_number:02}" - ): - unique_number += 1 - - asset_dir = f"{asset_dir}_{unique_number:02}" - container_name = f"{container_name}_{unique_number:02}{suffix}" - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "folder_path": folder_path, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - repre_entity = context["representation"] - name = container["asset_name"] - source_path = get_representation_path(repre_entity) - destination_path = container["namespace"] - - task = self.get_task(source_path, destination_path, name, True) - - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py deleted file mode 100644 index acd4c5c8d2..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect current project path.""" -import unreal # noqa -import pyblish.api - - -class CollectUnrealCurrentFile(pyblish.api.ContextPlugin): - """Inject the current working file into context.""" - - order = pyblish.api.CollectorOrder - 0.5 - label = "Unreal Current File" - hosts = ['unreal'] - - def process(self, context): - """Inject the current working file.""" - current_file = unreal.Paths.get_project_file_path() - context.data['currentFile'] = current_file - - assert current_file != '', "Current file is empty. " \ - "Save the file before continuing." diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py deleted file mode 100644 index de10e7b119..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py +++ /dev/null @@ -1,46 +0,0 @@ -import unreal - -import pyblish.api - - -class CollectInstanceMembers(pyblish.api.InstancePlugin): - """ - Collect members of instance. - - This collector will collect the assets for the families that support to - have them included as External Data, and will add them to the instance - as members. - """ - - order = pyblish.api.CollectorOrder + 0.1 - hosts = ["unreal"] - families = ["camera", "look", "unrealStaticMesh", "uasset"] - label = "Collect Instance Members" - - def process(self, instance): - """Collect members of instance.""" - self.log.info("Collecting instance members") - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - inst_path = instance.data.get('instance_path') - inst_name = inst_path.split('/')[-1] - - pub_instance = ar.get_asset_by_object_path( - f"{inst_path}.{inst_name}").get_asset() - - if not pub_instance: - self.log.error(f"{inst_path}.{inst_name}") - raise RuntimeError(f"Instance {instance} not found.") - - if not pub_instance.get_editor_property("add_external_assets"): - # No external assets in the instance - return - - assets = pub_instance.get_editor_property('asset_data_external') - - members = [asset.get_path_name() for asset in assets] - - self.log.debug(f"Members: {members}") - - instance.data["members"] = members diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py deleted file mode 100644 index ce2a03155b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py +++ /dev/null @@ -1,116 +0,0 @@ -from pathlib import Path - -import unreal -import pyblish.api - -from ayon_core.pipeline import get_current_project_name -from ayon_core.pipeline import Anatomy -from ayon_core.hosts.unreal.api import pipeline - - -class CollectRenderInstances(pyblish.api.InstancePlugin): - """ This collector will try to find all the rendered frames. - - """ - order = pyblish.api.CollectorOrder - hosts = ["unreal"] - families = ["render"] - label = "Collect Render Instances" - - def process(self, instance): - self.log.debug("Preparing Rendering Instances") - - context = instance.context - - data = instance.data - data['remove'] = True - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sequence = ar.get_asset_by_object_path( - data.get('sequence')).get_asset() - - sequences = [{ - "sequence": sequence, - "output": data.get('output'), - "frame_range": ( - data.get('frameStart'), data.get('frameEnd')) - }] - - for s in sequences: - self.log.debug(f"Processing: {s.get('sequence').get_name()}") - subscenes = pipeline.get_subsequences(s.get('sequence')) - - if subscenes: - for ss in subscenes: - sequences.append({ - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), - "frame_range": ( - ss.get_start_frame(), ss.get_end_frame() - 1) - }) - else: - # Avoid creating instances for camera sequences - if "_camera" not in s.get('sequence').get_name(): - seq = s.get('sequence') - seq_name = seq.get_name() - - product_type = "render" - new_product_name = f"{data.get('productName')}_{seq_name}" - new_instance = context.create_instance( - new_product_name - ) - new_instance[:] = seq_name - - new_data = new_instance.data - - new_data["folderPath"] = f"/{s.get('output')}" - new_data["setMembers"] = seq_name - new_data["productName"] = new_product_name - new_data["productType"] = product_type - new_data["family"] = product_type - new_data["families"] = [product_type, "review"] - new_data["parent"] = data.get("parent") - new_data["level"] = data.get("level") - new_data["output"] = s.get('output') - new_data["fps"] = seq.get_display_rate().numerator - new_data["frameStart"] = int(s.get('frame_range')[0]) - new_data["frameEnd"] = int(s.get('frame_range')[1]) - new_data["sequence"] = seq.get_path_name() - new_data["master_sequence"] = data["master_sequence"] - new_data["master_level"] = data["master_level"] - - self.log.debug(f"new instance data: {new_data}") - - try: - project = get_current_project_name() - anatomy = Anatomy(project) - root = anatomy.roots['renders'] - except Exception as e: - raise Exception(( - "Could not find render root " - "in anatomy settings.")) from e - - render_dir = f"{root}/{project}/{s.get('output')}" - render_path = Path(render_dir) - - frames = [] - - for x in render_path.iterdir(): - if x.is_file() and x.suffix == '.png': - frames.append(str(x.name)) - - if "representations" not in new_instance.data: - new_instance.data["representations"] = [] - - repr = { - 'frameStart': instance.data["frameStart"], - 'frameEnd': instance.data["frameEnd"], - 'name': 'png', - 'ext': 'png', - 'files': frames, - 'stagingDir': render_dir, - 'tags': ['review'] - } - new_instance.data["representations"].append(repr) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py deleted file mode 100644 index ebc5452011..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -"""Extract camera from Unreal.""" -import os - -import unreal - -from ayon_core.pipeline import publish -from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION - - -class ExtractCamera(publish.Extractor): - """Extract a camera.""" - - label = "Extract Camera" - hosts = ["unreal"] - families = ["camera"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - # Define extract output file path - staging_dir = self.staging_dir(instance) - fbx_filename = "{}.fbx".format(instance.name) - - # Perform extraction - self.log.info("Performing extraction..") - - # Check if the loaded level is the same of the instance - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - current_level = world.get_path_name() - assert current_level == instance.data.get("level"), \ - "Wrong level loaded" - - for member in instance.data.get('members'): - data = ar.get_asset_by_object_path(member) - if UNREAL_VERSION.major == 5: - is_level_sequence = ( - data.asset_class_path.asset_name == "LevelSequence") - else: - is_level_sequence = (data.asset_class == "LevelSequence") - - if is_level_sequence: - sequence = data.get_asset() - if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1: - params = unreal.SequencerExportFBXParams( - world=world, - root_sequence=sequence, - sequence=sequence, - bindings=sequence.get_bindings(), - master_tracks=sequence.get_master_tracks(), - fbx_file_name=os.path.join(staging_dir, fbx_filename) - ) - unreal.SequencerTools.export_level_sequence_fbx(params) - elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26: - unreal.SequencerTools.export_fbx( - world, - sequence, - sequence.get_bindings(), - unreal.FbxExportOption(), - os.path.join(staging_dir, fbx_filename) - ) - else: - # Unreal 5.0 or 4.27 - unreal.SequencerTools.export_level_sequence_fbx( - world, - sequence, - sequence.get_bindings(), - unreal.FbxExportOption(), - os.path.join(staging_dir, fbx_filename) - ) - - if not os.path.isfile(os.path.join(staging_dir, fbx_filename)): - raise RuntimeError("Failed to extract camera") - - if "representations" not in instance.data: - instance.data["representations"] = [] - - fbx_representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': fbx_filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(fbx_representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py deleted file mode 100644 index 5489057021..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import json -import math - -import unreal -from unreal import EditorLevelLibrary as ell -from unreal import EditorAssetLibrary as eal -import ayon_api - -from ayon_core.pipeline import publish - - -class ExtractLayout(publish.Extractor): - """Extract a layout.""" - - label = "Extract Layout" - hosts = ["unreal"] - families = ["layout"] - optional = True - - def process(self, instance): - # Define extract output file path - staging_dir = self.staging_dir(instance) - - # Perform extraction - self.log.info("Performing extraction..") - - # Check if the loaded level is the same of the instance - current_level = ell.get_editor_world().get_path_name() - assert current_level == instance.data.get("level"), \ - "Wrong level loaded" - - json_data = [] - project_name = instance.context.data["projectName"] - - for member in instance[:]: - actor = ell.get_actor_reference(member) - mesh = None - - # Check type the type of mesh - if actor.get_class().get_name() == 'SkeletalMeshActor': - mesh = actor.skeletal_mesh_component.skeletal_mesh - elif actor.get_class().get_name() == 'StaticMeshActor': - mesh = actor.static_mesh_component.static_mesh - - if mesh: - # Search the reference to the Asset Container for the object - path = unreal.Paths.get_path(mesh.get_path_name()) - filter = unreal.ARFilter( - class_names=["AyonAssetContainer"], package_paths=[path]) - ar = unreal.AssetRegistryHelpers.get_asset_registry() - try: - asset_container = ar.get_assets(filter)[0].get_asset() - except IndexError: - self.log.error("AssetContainer not found.") - return - - parent_id = eal.get_metadata_tag(asset_container, "parent") - family = eal.get_metadata_tag(asset_container, "family") - - self.log.info("Parent: {}".format(parent_id)) - blend = ayon_api.get_representation_by_name( - project_name, "blend", parent_id, fields={"id"} - ) - blend_id = blend["id"] - - json_element = {} - json_element["reference"] = str(blend_id) - json_element["family"] = family - json_element["product_type"] = family - json_element["instance_name"] = actor.get_name() - json_element["asset_name"] = mesh.get_name() - import_data = mesh.get_editor_property("asset_import_data") - json_element["file_path"] = import_data.get_first_filename() - transform = actor.get_actor_transform() - - json_element["transform"] = { - "translation": { - "x": -transform.translation.x, - "y": transform.translation.y, - "z": transform.translation.z - }, - "rotation": { - "x": math.radians(transform.rotation.euler().x), - "y": math.radians(transform.rotation.euler().y), - "z": math.radians(180.0 - transform.rotation.euler().z) - }, - "scale": { - "x": transform.scale3d.x, - "y": transform.scale3d.y, - "z": transform.scale3d.z - } - } - json_data.append(json_element) - - json_filename = "{}.json".format(instance.name) - json_path = os.path.join(staging_dir, json_filename) - - with open(json_path, "w+") as file: - json.dump(json_data, fp=file, indent=2) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - json_representation = { - 'name': 'json', - 'ext': 'json', - 'files': json_filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(json_representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py deleted file mode 100644 index fd1277e302..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import os - -import unreal -from unreal import MaterialEditingLibrary as mat_lib - -from ayon_core.pipeline import publish - - -class ExtractLook(publish.Extractor): - """Extract look.""" - - label = "Extract Look" - hosts = ["unreal"] - families = ["look"] - optional = True - - def process(self, instance): - # Define extract output file path - staging_dir = self.staging_dir(instance) - resources_dir = instance.data["resourcesDir"] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - transfers = [] - - json_data = [] - - for member in instance: - asset = ar.get_asset_by_object_path(member) - obj = asset.get_asset() - - name = asset.get_editor_property('asset_name') - - json_element = {'material': str(name)} - - material_obj = obj.get_editor_property('static_materials')[0] - material = material_obj.material_interface - - base_color = mat_lib.get_material_property_input_node( - material, unreal.MaterialProperty.MP_BASE_COLOR) - - base_color_name = base_color.get_editor_property('parameter_name') - - texture = mat_lib.get_material_default_texture_parameter_value( - material, base_color_name) - - if texture: - # Export Texture - tga_filename = f"{instance.name}_{name}_texture.tga" - - tga_exporter = unreal.TextureExporterTGA() - - tga_export_task = unreal.AssetExportTask() - - tga_export_task.set_editor_property('exporter', tga_exporter) - tga_export_task.set_editor_property('automated', True) - tga_export_task.set_editor_property('object', texture) - tga_export_task.set_editor_property( - 'filename', f"{staging_dir}/{tga_filename}") - tga_export_task.set_editor_property('prompt', False) - tga_export_task.set_editor_property('selected', False) - - unreal.Exporter.run_asset_export_task(tga_export_task) - - json_element['tga_filename'] = tga_filename - - transfers.append(( - f"{staging_dir}/{tga_filename}", - f"{resources_dir}/{tga_filename}")) - - fbx_filename = f"{instance.name}_{name}.fbx" - - fbx_exporter = unreal.StaticMeshExporterFBX() - fbx_exporter.set_editor_property('text', False) - - options = unreal.FbxExportOption() - options.set_editor_property('ascii', False) - options.set_editor_property('collision', False) - - task = unreal.AssetExportTask() - task.set_editor_property('exporter', fbx_exporter) - task.set_editor_property('options', options) - task.set_editor_property('automated', True) - task.set_editor_property('object', object) - task.set_editor_property( - 'filename', f"{staging_dir}/{fbx_filename}") - task.set_editor_property('prompt', False) - task.set_editor_property('selected', False) - - unreal.Exporter.run_asset_export_task(task) - - json_element['fbx_filename'] = fbx_filename - - transfers.append(( - f"{staging_dir}/{fbx_filename}", - f"{resources_dir}/{fbx_filename}")) - - json_data.append(json_element) - - json_filename = f"{instance.name}.json" - json_path = os.path.join(staging_dir, json_filename) - - with open(json_path, "w+") as file: - json.dump(json_data, fp=file, indent=2) - - if "transfers" not in instance.data: - instance.data["transfers"] = [] - if "representations" not in instance.data: - instance.data["representations"] = [] - - json_representation = { - 'name': 'json', - 'ext': 'json', - 'files': json_filename, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(json_representation) - instance.data["transfers"].extend(transfers) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py deleted file mode 100644 index fa4fb4c04a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py +++ /dev/null @@ -1,50 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from ayon_core.pipeline import publish - - -class ExtractUAsset(publish.Extractor): - """Extract a UAsset.""" - - label = "Extract UAsset" - hosts = ["unreal"] - families = ["uasset", "umap"] - optional = True - - def process(self, instance): - extension = ( - "umap" if "umap" in instance.data.get("families") else "uasset") - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.debug("Performing extraction..") - staging_dir = self.staging_dir(instance) - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - self.log.info(f"instance.data: {instance.data}") - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": extension, - "ext": extension, - "files": filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py b/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py deleted file mode 100644 index c760129550..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py +++ /dev/null @@ -1,41 +0,0 @@ -import unreal - -import pyblish.api - - -class ValidateNoDependencies(pyblish.api.InstancePlugin): - """Ensure that the uasset has no dependencies - - The uasset is checked for dependencies. If there are any, the instance - cannot be published. - """ - - order = pyblish.api.ValidatorOrder - label = "Check no dependencies" - families = ["uasset"] - hosts = ["unreal"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - all_dependencies = [] - - for obj in instance[:]: - asset = ar.get_asset_by_object_path(obj) - dependencies = ar.get_dependencies( - asset.package_name, - unreal.AssetRegistryDependencyOptions( - include_soft_package_references=False, - include_hard_package_references=True, - include_searchable_names=False, - include_soft_management_references=False, - include_hard_management_references=False - )) - if dependencies: - for dep in dependencies: - if str(dep).startswith("/Game/"): - all_dependencies.append(str(dep)) - - if all_dependencies: - raise RuntimeError( - f"Dependencies found: {all_dependencies}") diff --git a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py b/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py deleted file mode 100644 index 85214a2b0d..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ /dev/null @@ -1,83 +0,0 @@ -import clique -import os -import re - -import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError - - -class ValidateSequenceFrames(pyblish.api.InstancePlugin): - """Ensure the sequence of frames is complete - - The files found in the folder are checked against the frameStart and - frameEnd of the instance. If the first or last file is not - corresponding with the first or last frame it is flagged as invalid. - """ - - order = pyblish.api.ValidatorOrder - label = "Validate Sequence Frames" - families = ["render"] - hosts = ["unreal"] - optional = True - - def process(self, instance): - representations = instance.data.get("representations") - folder_attributes = ( - instance.data - .get("folderEntity", {}) - .get("attrib", {}) - ) - for repr in representations: - repr_files = repr["files"] - if isinstance(repr_files, str): - continue - - ext = repr.get("ext") - if not ext: - _, ext = os.path.splitext(repr_files[0]) - elif not ext.startswith("."): - ext = ".{}".format(ext) - pattern = r"\D?(?P(?P0*)\d+){}$".format( - re.escape(ext)) - patterns = [pattern] - - collections, remainder = clique.assemble( - repr["files"], minimum_items=1, patterns=patterns) - - if remainder: - raise PublishValidationError( - "Some files have been found outside a sequence. " - f"Invalid files: {remainder}") - if not collections: - raise PublishValidationError( - "We have been unable to find a sequence in the " - "files. Please ensure the files are named " - "appropriately. " - f"Files: {repr_files}") - if len(collections) > 1: - raise PublishValidationError( - "Multiple collections detected. There should be a single " - "collection per representation. " - f"Collections identified: {collections}") - - collection = collections[0] - frames = list(collection.indexes) - - if instance.data.get("slate"): - # Slate is not part of the frame range - frames = frames[1:] - - current_range = (frames[0], frames[-1]) - required_range = (folder_attributes["clipIn"], - folder_attributes["clipOut"]) - - if current_range != required_range: - raise PublishValidationError( - f"Invalid frame range: {current_range} - " - f"expected: {required_range}") - - missing = collection.holes().indexes - if missing: - raise PublishValidationError( - "Missing frames have been detected. " - f"Missing frames: {missing}") diff --git a/client/ayon_core/hosts/unreal/ue_workers.py b/client/ayon_core/hosts/unreal/ue_workers.py deleted file mode 100644 index 256c0557be..0000000000 --- a/client/ayon_core/hosts/unreal/ue_workers.py +++ /dev/null @@ -1,434 +0,0 @@ -import json -import os -import platform -import re -import subprocess -import tempfile -from distutils import dir_util -from distutils.dir_util import copy_tree -from pathlib import Path -from typing import List, Union - -from qtpy import QtCore - -import ayon_core.hosts.unreal.lib as ue_lib -from ayon_core.settings import get_project_settings - - -def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)): - match = re.search(r"\[[1-9]+/[0-9]+]", line) - if match is not None: - split: list[str] = match.group().split("/") - curr: float = float(split[0][1:]) - total: float = float(split[1][:-1]) - progress_signal.emit(int((curr / total) * 100.0)) - - -def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)): - match = re.search("@progress", line) - if match is not None: - percent_match = re.search(r"\d{1,3}", line) - progress_signal.emit(int(percent_match.group())) - - -def retrieve_exit_code(line: str): - match = re.search(r"ExitCode=\d+", line) - if match is not None: - split: list[str] = match.group().split("=") - return int(split[1]) - - return None - - -class UEWorker(QtCore.QObject): - finished = QtCore.Signal(str) - failed = QtCore.Signal(str, int) - progress = QtCore.Signal(int) - log = QtCore.Signal(str) - - engine_path: Path = None - env = None - - def execute(self): - raise NotImplementedError("Please implement this method!") - - def run(self): - try: - self.execute() - except Exception as e: - import traceback - self.log.emit(str(e)) - self.log.emit(traceback.format_exc()) - self.failed.emit(str(e), 1) - raise e - - -class UEProjectGenerationWorker(UEWorker): - stage_begin = QtCore.Signal(str) - - ue_version: str = None - project_name: str = None - project_dir: Path = None - dev_mode = False - - def setup(self, ue_version: str, - project_name: str, - unreal_project_name, - engine_path: Path, - project_dir: Path, - dev_mode: bool = False, - env: dict = None): - """Set the worker with necessary parameters. - - Args: - ue_version (str): Unreal Engine version. - project_name (str): Name of the project in AYON. - unreal_project_name (str): Name of the project in Unreal. - engine_path (Path): Path to the Unreal Engine. - project_dir (Path): Path to the project directory. - dev_mode (bool, optional): Whether to run the project in dev mode. - Defaults to False. - env (dict, optional): Environment variables. Defaults to None. - - """ - - self.ue_version = ue_version - self.project_dir = project_dir - self.env = env or os.environ - - preset = get_project_settings(project_name)["unreal"]["project_setup"] - - if dev_mode or preset["dev_mode"]: - self.dev_mode = True - - self.project_name = unreal_project_name - self.engine_path = engine_path - - def execute(self): - # engine_path should be the location of UE_X.X folder - - ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, - self.ue_version) - cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version) - project_file = self.project_dir / f"{self.project_name}.uproject" - - print("--- Generating a new project ...") - # 1st stage - stage_count = 2 - if self.dev_mode: - stage_count = 4 - - self.stage_begin.emit( - ("Generating a new UE project ... 1 out of " - f"{stage_count}")) - - # Need to copy the commandlet project to a temporary folder where - # users don't need admin rights to write to. - cmdlet_tmp = tempfile.TemporaryDirectory() - cmdlet_filename = cmdlet_project.name - cmdlet_dir = cmdlet_project.parent.as_posix() - cmdlet_tmp_name = Path(cmdlet_tmp.name) - cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) - copy_tree( - cmdlet_dir, - cmdlet_tmp_name.as_posix()) - - commandlet_cmd = [ - f"{ue_editor_exe.as_posix()}", - f"{cmdlet_tmp_file.as_posix()}", - "-run=AyonGenerateProject", - f"{project_file.resolve().as_posix()}", - ] - - if self.dev_mode: - commandlet_cmd.append("-GenerateCode") - - gen_process = subprocess.Popen(commandlet_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in gen_process.stdout: - decoded_line = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - gen_process.stdout.close() - return_code = gen_process.wait() - - cmdlet_tmp.cleanup() - - if return_code and return_code != 0: - msg = ( - f"Failed to generate {self.project_name} " - f"project! Exited with return code {return_code}" - ) - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - print("--- Project has been generated successfully.") - self.stage_begin.emit( - (f"Writing the Engine ID of the build UE ... 1" - f" out of {stage_count}")) - - if not project_file.is_file(): - msg = ("Failed to write the Engine ID into .uproject file! Can " - "not read!") - self.failed.emit(msg) - raise RuntimeError(msg) - - with open(project_file.as_posix(), mode="r+") as pf: - pf_json = json.load(pf) - pf_json["EngineAssociation"] = ue_lib.get_build_id( - self.engine_path, - self.ue_version - ) - print(pf_json["EngineAssociation"]) - pf.seek(0) - json.dump(pf_json, pf, indent=4) - pf.truncate() - print("--- Engine ID has been written into the project file") - - self.progress.emit(90) - if self.dev_mode: - # 2nd stage - self.stage_begin.emit( - (f"Generating project files ... 2 out of " - f"{stage_count}")) - - self.progress.emit(0) - ubt_path = ue_lib.get_path_to_ubt(self.engine_path, - self.ue_version) - - arch = "Win64" - if platform.system().lower() == "windows": - arch = "Win64" - elif platform.system().lower() == "linux": - arch = "Linux" - elif platform.system().lower() == "darwin": - # we need to test this out - arch = "Mac" - - gen_prj_files_cmd = [ubt_path.as_posix(), - "-projectfiles", - f"-project={project_file}", - "-progress"] - gen_proc = subprocess.Popen(gen_prj_files_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - for line in gen_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - parse_prj_progress(decoded_line, self.progress) - - gen_proc.stdout.close() - return_code = gen_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to generate project files! " - f"Exited with return code {return_code}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - self.stage_begin.emit( - f"Building the project ... 3 out of {stage_count}") - self.progress.emit(0) - # 3rd stage - build_prj_cmd = [ubt_path.as_posix(), - f"-ModuleWithSuffix={self.project_name},3555", - arch, - "Development", - "-TargetType=Editor", - f"-Project={project_file}", - f"{project_file}", - "-IgnoreJunk"] - - build_prj_proc = subprocess.Popen(build_prj_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - for line in build_prj_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - parse_comp_progress(decoded_line, self.progress) - - build_prj_proc.stdout.close() - return_code = build_prj_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to build project! " - f"Exited with return code {return_code}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - # ensure we have PySide2/6 installed in engine - - self.progress.emit(0) - self.stage_begin.emit( - (f"Checking Qt bindings installation... {stage_count} " - f" out of {stage_count}")) - python_path = None - if platform.system().lower() == "windows": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/python.exe") - - if platform.system().lower() == "linux": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if not python_path: - msg = "Unsupported platform" - self.failed.emit(msg, 1) - raise NotImplementedError(msg) - if not python_path.exists(): - msg = f"Unreal Python not found at {python_path}" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - pyside_version = "PySide2" - ue_version = self.ue_version.split(".") - if int(ue_version[0]) == 5 and int(ue_version[1]) >= 4: - # Use PySide6 6.6.3 because 6.7.0 had a bug - # - 'QPushButton' can't be added to 'QBoxLayout' - pyside_version = "PySide6==6.6.3" - - site_packages_prefix = python_path.parent.as_posix() - - pyside_cmd = [ - python_path.as_posix(), - "-m", "pip", - "install", - "--ignore-installed", - pyside_version, - - ] - - if platform.system().lower() == "windows": - pyside_cmd += ["--target", site_packages_prefix] - - print(f"--- Installing {pyside_version} ...") - print(" ".join(pyside_cmd)) - - pyside_install = subprocess.Popen(pyside_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in pyside_install.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - - pyside_install.stdout.close() - return_code = pyside_install.wait() - - if return_code and return_code != 0: - msg = (f"Failed to create the project! {return_code} " - f"The installation of {pyside_version} has failed!: {pyside_install}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - self.progress.emit(100) - self.finished.emit("Project successfully built!") - - -class UEPluginInstallWorker(UEWorker): - installing = QtCore.Signal(str) - - def setup(self, engine_path: Path, env: dict = None, ): - self.engine_path = engine_path - self.env = env or os.environ - - def _build_and_move_plugin(self, plugin_build_path: Path): - uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) - src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(src_plugin_dir): - msg = "Path to the integration plugin is null!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - if not uat_path.is_file(): - msg = "Building failed! Path to UAT is invalid!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - temp_dir: Path = src_plugin_dir.parent / "Temp" - temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = src_plugin_dir / "Ayon.uplugin" - - # in order to successfully build the plugin, - # It must be built outside the Engine directory and then moved - build_plugin_cmd: List[str] = [f"{uat_path.as_posix()}", - "BuildPlugin", - f"-Plugin={uplugin_path.as_posix()}", - f"-Package={temp_dir.as_posix()}"] - - build_proc = subprocess.Popen(build_plugin_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - return_code: Union[None, int] = None - for line in build_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - if return_code is None: - return_code = retrieve_exit_code(decoded_line) - parse_comp_progress(decoded_line, self.progress) - - build_proc.stdout.close() - build_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to build plugin" - f" project! Exited with return code {return_code}") - dir_util.remove_tree(temp_dir.as_posix()) - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - # Copy the contents of the 'Temp' dir into the - # 'Ayon' directory in the engine - dir_util.copy_tree(temp_dir.as_posix(), - plugin_build_path.as_posix()) - - # We need to also copy the config folder. - # The UAT doesn't include the Config folder in the build - plugin_install_config_path: Path = plugin_build_path / "Config" - src_plugin_config_path = src_plugin_dir / "Config" - - dir_util.copy_tree(src_plugin_config_path.as_posix(), - plugin_install_config_path.as_posix()) - - dir_util.remove_tree(temp_dir.as_posix()) - - def execute(self): - src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(src_plugin_dir): - msg = "Path to the integration plugin is null!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - # Create a path to the plugin in the engine - op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ - "/Ayon" - - if not op_plugin_path.is_dir(): - self.installing.emit("Installing and building the plugin ...") - op_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path = op_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - self.installing.emit("Building the plugin ...") - print("--- Building the plugin...") - - self._build_and_move_plugin(op_plugin_path) - - self.finished.emit("Plugin successfully installed") diff --git a/client/ayon_core/hosts/unreal/ui/__init__.py b/client/ayon_core/hosts/unreal/ui/__init__.py deleted file mode 100644 index 606b21ef19..0000000000 --- a/client/ayon_core/hosts/unreal/ui/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .splash_screen import SplashScreen - -__all__ = ( - "SplashScreen", -) diff --git a/client/ayon_core/hosts/unreal/ui/splash_screen.py b/client/ayon_core/hosts/unreal/ui/splash_screen.py deleted file mode 100644 index cf34943515..0000000000 --- a/client/ayon_core/hosts/unreal/ui/splash_screen.py +++ /dev/null @@ -1,262 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui -from ayon_core import style, resources - - -class SplashScreen(QtWidgets.QDialog): - """Splash screen for executing a process on another thread. It is able - to inform about the progress of the process and log given information. - """ - - splash_icon = None - top_label = None - show_log_btn: QtWidgets.QLabel = None - progress_bar = None - log_text: QtWidgets.QLabel = None - scroll_area: QtWidgets.QScrollArea = None - close_btn: QtWidgets.QPushButton = None - scroll_bar: QtWidgets.QScrollBar = None - - is_log_visible = False - is_scroll_auto = True - - thread_return_code = None - q_thread: QtCore.QThread = None - - def __init__(self, - window_title: str, - splash_icon=None, - window_icon=None): - """ - Args: - window_title (str): String which sets the window title - splash_icon (str | bytes | None): A resource (pic) which is used - for the splash icon - window_icon (str | bytes | None: A resource (pic) which is used for - the window's icon - """ - super(SplashScreen, self).__init__() - - if splash_icon is None: - splash_icon = resources.get_ayon_icon_filepath() - - if window_icon is None: - window_icon = resources.get_ayon_icon_filepath() - - self.splash_icon = splash_icon - self.setWindowIcon(QtGui.QIcon(window_icon)) - self.setWindowTitle(window_title) - self.init_ui() - - def was_proc_successful(self) -> bool: - return self.thread_return_code == 0 - - def start_thread(self, q_thread: QtCore.QThread): - """Saves the reference to this thread and starts it. - - Args: - q_thread (QtCore.QThread): A QThread containing a given worker - (QtCore.QObject) - - Returns: - None - """ - if not q_thread: - raise RuntimeError("Failed to run a worker thread! " - "The thread is null!") - - self.q_thread = q_thread - self.q_thread.start() - - @QtCore.Slot() - def quit_and_close(self): - """Quits the thread and closes the splash screen. Note that this means - the thread has exited with the return code 0! - - Returns: - None - """ - self.thread_return_code = 0 - self.q_thread.quit() - - if not self.q_thread.wait(5000): - raise RuntimeError("Failed to quit the QThread! " - "The deadline has been reached! The thread " - "has not finished it's execution!.") - self.close() - - - @QtCore.Slot() - def toggle_log(self): - if self.is_log_visible: - self.scroll_area.hide() - width = self.width() - self.adjustSize() - self.resize(width, self.height()) - else: - self.scroll_area.show() - self.scroll_bar.setValue(self.scroll_bar.maximum()) - self.resize(self.width(), 300) - - self.is_log_visible = not self.is_log_visible - - def show_ui(self): - """Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING - (The execution of code can not proceed further beyond this function - until the splash screen is closed!) - - Returns: - None - """ - self.show() - self.exec_() - - def init_ui(self): - self.resize(450, 100) - self.setMinimumWidth(250) - self.setStyleSheet(style.load_stylesheet()) - - # Top Section - self.top_label = QtWidgets.QLabel(self) - self.top_label.setText("Starting process ...") - self.top_label.setWordWrap(True) - - icon = QtWidgets.QLabel(self) - icon.setPixmap(QtGui.QPixmap(self.splash_icon)) - icon.setFixedHeight(45) - icon.setFixedWidth(45) - icon.setScaledContents(True) - - self.close_btn = QtWidgets.QPushButton(self) - self.close_btn.setText("Quit") - self.close_btn.clicked.connect(self.close) - self.close_btn.setFixedWidth(80) - self.close_btn.hide() - - self.show_log_btn = QtWidgets.QPushButton(self) - self.show_log_btn.setText("Show log") - self.show_log_btn.setFixedWidth(80) - self.show_log_btn.clicked.connect(self.toggle_log) - - button_layout = QtWidgets.QVBoxLayout() - button_layout.addWidget(self.show_log_btn) - button_layout.addWidget(self.close_btn) - - # Progress Bar - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setValue(0) - self.progress_bar.setAlignment(QtCore.Qt.AlignTop) - - # Log Content - self.scroll_area = QtWidgets.QScrollArea(self) - self.scroll_area.hide() - log_widget = QtWidgets.QWidget(self.scroll_area) - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOn - ) - self.scroll_area.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOn - ) - self.scroll_area.setWidget(log_widget) - - self.scroll_bar = self.scroll_area.verticalScrollBar() - self.scroll_bar.sliderMoved.connect(self.on_scroll) - - self.log_text = QtWidgets.QLabel(self) - self.log_text.setText('') - self.log_text.setAlignment(QtCore.Qt.AlignTop) - - log_layout = QtWidgets.QVBoxLayout(log_widget) - log_layout.addWidget(self.log_text) - - top_layout = QtWidgets.QHBoxLayout() - top_layout.setAlignment(QtCore.Qt.AlignTop) - top_layout.addWidget(icon) - top_layout.addSpacing(10) - top_layout.addWidget(self.top_label) - top_layout.addSpacing(10) - top_layout.addLayout(button_layout) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addLayout(top_layout) - main_layout.addSpacing(10) - main_layout.addWidget(self.progress_bar) - main_layout.addSpacing(10) - main_layout.addWidget(self.scroll_area) - - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowMinimizeButtonHint - ) - - desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self) - center = desktop_rect.center() - self.move( - center.x() - (self.width() * 0.5), - center.y() - (self.height() * 0.5) - ) - - @QtCore.Slot(int) - def update_progress(self, value: int): - self.progress_bar.setValue(value) - - @QtCore.Slot(str) - def update_top_label_text(self, text: str): - self.top_label.setText(text) - - @QtCore.Slot(str, str) - def append_log(self, text: str, end: str = ''): - """A slot used for receiving log info and appending it to scroll area's - content. - Args: - text (str): A log text that will append to the current one in the - scroll area. - end (str): end string which can be appended to the end of the given - line (for ex. a line break). - - Returns: - None - """ - self.log_text.setText(self.log_text.text() + text + end) - if self.is_scroll_auto: - self.scroll_bar.setValue(self.scroll_bar.maximum()) - - @QtCore.Slot(int) - def on_scroll(self, position: int): - """ - A slot for the vertical scroll bar's movement. This ensures the - auto-scrolling feature of the scroll area when the scroll bar is at its - maximum value. - - Args: - position (int): Position value of the scroll bar. - - Returns: - None - """ - if self.scroll_bar.maximum() == position: - self.is_scroll_auto = True - return - - self.is_scroll_auto = False - - @QtCore.Slot(str, int) - def fail(self, text: str, return_code: int = 1): - """ - A slot used for signals which can emit when a worker (process) has - failed. at this moment the splash screen doesn't close by itself. - it has to be closed by the user. - - Args: - text (str): A text which can be set to the top label. - - Returns: - return_code (int): Return code of the thread's code - """ - self.top_label.setText(text) - self.close_btn.show() - self.thread_return_code = return_code - self.q_thread.exit(return_code) - self.q_thread.wait() diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index e25d3479ee..1f864284cd 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -1,19 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa E402 """AYON lib functions.""" -# add vendor to sys path based on Python version -import sys -import os -import site -from ayon_core import AYON_CORE_ROOT - -# Add Python version specific vendor folder -python_version_dir = os.path.join( - AYON_CORE_ROOT, "vendor", "python", "python_{}".format(sys.version[0]) -) -# Prepend path in sys paths -sys.path.insert(0, python_version_dir) -site.addsitedir(python_version_dir) from .local_settings import ( IniSettingRegistry, diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 3dd284b8e4..0a9d38ab65 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -281,7 +281,7 @@ class HiddenDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default kwargs["hidden"] = True - super(UnknownDef, self).__init__(key, **kwargs) + super(HiddenDef, self).__init__(key, **kwargs) def convert_value(self, value): return value diff --git a/client/ayon_core/modules/deadline/version.py b/client/ayon_core/modules/deadline/version.py deleted file mode 100644 index 569b1212f7..0000000000 --- a/client/ayon_core/modules/deadline/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.10" diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py index 38e88d36ca..344b0bc389 100644 --- a/client/ayon_core/modules/launcher_action.py +++ b/client/ayon_core/modules/launcher_action.py @@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction class LauncherAction(AYONAddon, ITrayAction): label = "Launcher" name = "launcher_tool" + version = "1.0.0" def initialize(self, settings): diff --git a/client/ayon_core/modules/loader_action.py b/client/ayon_core/modules/loader_action.py index 1e45db05dc..a58d7fd456 100644 --- a/client/ayon_core/modules/loader_action.py +++ b/client/ayon_core/modules/loader_action.py @@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon class LoaderAddon(AYONAddon, ITrayAddon): name = "loader_tool" + version = "1.0.0" def initialize(self, settings): # Tray attributes diff --git a/client/ayon_core/modules/python_console_interpreter/addon.py b/client/ayon_core/modules/python_console_interpreter/addon.py index ffad3ce707..b0dce2585e 100644 --- a/client/ayon_core/modules/python_console_interpreter/addon.py +++ b/client/ayon_core/modules/python_console_interpreter/addon.py @@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction class PythonInterpreterAction(AYONAddon, ITrayAction): label = "Console" name = "python_interpreter" + version = "1.0.0" admin_action = True def initialize(self, settings): diff --git a/client/ayon_core/modules/webserver/__init__.py b/client/ayon_core/modules/webserver/__init__.py index 0d3f767638..32f2c55f65 100644 --- a/client/ayon_core/modules/webserver/__init__.py +++ b/client/ayon_core/modules/webserver/__init__.py @@ -1,8 +1,13 @@ +from .version import __version__ +from .structures import HostMsgAction from .webserver_module import ( WebServerAddon ) __all__ = ( + "__version__", + + "HostMsgAction", "WebServerAddon", ) diff --git a/client/ayon_core/modules/webserver/host_console_listener.py b/client/ayon_core/modules/webserver/host_console_listener.py index ed8a32b9f2..2efd768e24 100644 --- a/client/ayon_core/modules/webserver/host_console_listener.py +++ b/client/ayon_core/modules/webserver/host_console_listener.py @@ -9,22 +9,18 @@ from qtpy import QtWidgets from ayon_core.addon import ITrayService from ayon_core.tools.stdout_broker.window import ConsoleDialog +from .structures import HostMsgAction + log = logging.getLogger(__name__) +# Host listener icon type class IconType: IDLE = "idle" RUNNING = "running" FAILED = "failed" -class MsgAction: - CONNECTING = "connecting" - INITIALIZED = "initialized" - ADD = "add" - CLOSE = "close" - - class HostListener: def __init__(self, webserver, module): self._window_per_id = {} @@ -96,22 +92,22 @@ class HostListener: if msg.type == aiohttp.WSMsgType.TEXT: host_name, action, text = self._parse_message(msg) - if action == MsgAction.CONNECTING: + if action == HostMsgAction.CONNECTING: self._action_per_id[host_name] = None # must be sent to main thread, or action wont trigger self.module.execute_in_main_thread( lambda: self._host_is_connecting(host_name, text)) - elif action == MsgAction.CLOSE: + elif action == HostMsgAction.CLOSE: # clean close self._close(host_name) await ws.close() - elif action == MsgAction.INITIALIZED: + elif action == HostMsgAction.INITIALIZED: self.module.execute_in_main_thread( # must be queued as _host_is_connecting might not # be triggered/finished yet lambda: self._set_host_icon(host_name, IconType.RUNNING)) - elif action == MsgAction.ADD: + elif action == HostMsgAction.ADD: self.module.execute_in_main_thread( lambda: self._add_text(host_name, text)) elif msg.type == aiohttp.WSMsgType.ERROR: diff --git a/client/ayon_core/modules/webserver/structures.py b/client/ayon_core/modules/webserver/structures.py new file mode 100644 index 0000000000..a598e3342a --- /dev/null +++ b/client/ayon_core/modules/webserver/structures.py @@ -0,0 +1,6 @@ +# Host listener message actions +class HostMsgAction: + CONNECTING = "connecting" + INITIALIZED = "initialized" + ADD = "add" + CLOSE = "close" diff --git a/client/ayon_core/modules/webserver/version.py b/client/ayon_core/modules/webserver/version.py new file mode 100644 index 0000000000..5becc17c04 --- /dev/null +++ b/client/ayon_core/modules/webserver/version.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/client/ayon_core/modules/webserver/webserver_module.py b/client/ayon_core/modules/webserver/webserver_module.py index c324e0dd18..997b6f754c 100644 --- a/client/ayon_core/modules/webserver/webserver_module.py +++ b/client/ayon_core/modules/webserver/webserver_module.py @@ -26,9 +26,12 @@ import socket from ayon_core import resources from ayon_core.addon import AYONAddon, ITrayService +from .version import __version__ + class WebServerAddon(AYONAddon, ITrayService): name = "webserver" + version = __version__ label = "WebServer" webserver_url_env = "AYON_WEBSERVER_URL" diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index c32d04c44c..8b72405048 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler from ayon_core import AYON_CORE_ROOT from ayon_core.host import HostBase -from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event +from ayon_core.lib import ( + is_in_tests, + initialize_ayon_connection, + emit_event, + version_up +) from ayon_core.addon import load_addons, AddonsManager from ayon_core.settings import get_project_settings @@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names from .workfile import ( get_workdir, get_custom_workfile_template_by_string_context, + get_workfile_template_key_from_context, + get_last_workfile ) from . import ( register_loader_plugin_path, @@ -579,3 +586,48 @@ def get_process_id(): if _process_id is None: _process_id = str(uuid.uuid4()) return _process_id + + +def version_up_current_workfile(): + """Function to increment and save workfile + """ + host = registered_host() + if not host.has_unsaved_changes(): + print("No unsaved changes, skipping file save..") + return + + project_name = get_current_project_name() + folder_path = get_current_folder_path() + task_name = get_current_task_name() + host_name = get_current_host_name() + + template_key = get_workfile_template_key_from_context( + project_name, + folder_path, + task_name, + host_name, + ) + anatomy = Anatomy(project_name) + + data = get_template_data_with_names( + project_name, folder_path, task_name, host_name + ) + data["root"] = anatomy.roots + + work_template = anatomy.get_template_item("work", template_key) + + # Define saving file extension + extensions = host.get_workfile_extensions() + current_file = host.get_current_workfile() + if current_file: + extensions = [os.path.splitext(current_file)[-1]] + + work_root = work_template["directory"].format_strict(data) + file_template = work_template["file"].template + last_workfile_path = get_last_workfile( + work_root, file_template, data, extensions, True + ) + new_workfile_path = version_up(last_workfile_path) + if os.path.exists(new_workfile_path): + new_workfile_path = version_up(new_workfile_path) + host.save_workfile(new_workfile_path) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 7615ce6aee..0d8722dab1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -37,6 +37,7 @@ from .creator_plugins import ( # Changes of instances and context are send as tuple of 2 information UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) +_NOT_SET = object() class UnavailableSharedData(Exception): @@ -681,7 +682,7 @@ class PublishAttributeValues(AttributeValues): @property def parent(self): - self.publish_attributes.parent + return self.publish_attributes.parent class PublishAttributes: @@ -1401,6 +1402,11 @@ class CreateContext: self._current_folder_path = None self._current_task_name = None self._current_workfile_path = None + self._current_project_settings = None + + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._current_task_type = _NOT_SET self._current_project_anatomy = None @@ -1571,6 +1577,64 @@ class CreateContext: return self._current_task_name + def get_current_task_type(self): + """Task type which was used as current context on context reset. + + Returns: + Union[str, None]: Task type. + + """ + if self._current_task_type is _NOT_SET: + task_type = None + task_entity = self.get_current_task_entity() + if task_entity: + task_type = task_entity["taskType"] + self._current_task_type = task_type + return self._current_task_type + + def get_current_folder_entity(self): + """Folder entity for current context folder. + + Returns: + Union[dict[str, Any], None]: Folder entity. + + """ + if self._current_folder_entity is not _NOT_SET: + return copy.deepcopy(self._current_folder_entity) + folder_entity = None + folder_path = self.get_current_folder_path() + if folder_path: + project_name = self.get_current_project_name() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + self._current_folder_entity = folder_entity + return copy.deepcopy(self._current_folder_entity) + + def get_current_task_entity(self): + """Task entity for current context task. + + Returns: + Union[dict[str, Any], None]: Task entity. + + """ + if self._current_task_entity is not _NOT_SET: + return copy.deepcopy(self._current_task_entity) + task_entity = None + task_name = self.get_current_task_name() + if task_name: + folder_entity = self.get_current_folder_entity() + if folder_entity: + project_name = self.get_current_project_name() + task_entity = ayon_api.get_task_by_name( + project_name, + folder_id=folder_entity["id"], + task_name=task_name + ) + self._current_task_entity = task_entity + return copy.deepcopy(self._current_task_entity) + + def get_current_workfile_path(self): """Workfile path which was opened on context reset. @@ -1592,6 +1656,12 @@ class CreateContext: self._current_project_name) return self._current_project_anatomy + def get_current_project_settings(self): + if self._current_project_settings is None: + self._current_project_settings = get_project_settings( + self.get_current_project_name()) + return self._current_project_settings + @property def context_has_changed(self): """Host context has changed. @@ -1718,7 +1788,12 @@ class CreateContext: self._current_task_name = task_name self._current_workfile_path = workfile_path + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._current_task_type = _NOT_SET + self._current_project_anatomy = None + self._current_project_settings = None def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. @@ -1772,7 +1847,7 @@ class CreateContext: def _reset_creator_plugins(self): # Prepare settings - project_settings = get_project_settings(self.project_name) + project_settings = self.get_current_project_settings() # Discover and prepare creators creators = {} diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8d3644637b..7f63089d33 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None): settings_category = getattr(plugin, "settings_category", None) if settings_category: try: - return ( - project_settings - [settings_category] - ["publish"] - [plugin.__name__] - ) + category_settings = project_settings[settings_category] except KeyError: log.warning(( - "Couldn't find plugin '{}' settings" - " under settings category '{}'" - ).format(plugin.__name__, settings_category)) + "Couldn't find settings category '{}' in project settings" + ).format(settings_category)) + return {} + + try: + return category_settings["publish"][plugin.__name__] + except KeyError: return {} # Use project settings based on a category name diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index ad5a5d43fc..b6636696c1 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -313,7 +313,14 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # Define version version_number = None - if self.follow_workfile_version: + + # Allow an instance to force enable or disable the version + # following of the current context + use_context_version = self.follow_workfile_version + if "followWorkfileVersion" in instance.data: + use_context_version = instance.data["followWorkfileVersion"] + + if use_context_version: version_number = context.data("version") # Even if 'follow_workfile_version' is enabled, it may not be set @@ -391,7 +398,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): anatomy_data.update(folder_data) return - if instance.data.get("newAssetPublishing"): + if ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ): hierarchy = instance.data["hierarchy"] anatomy_data["hierarchy"] = hierarchy @@ -409,7 +420,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): "path": instance.data["folderPath"], # TODO get folder type from hierarchy # Using 'Shot' is current default behavior of editorial - # (or 'newAssetPublishing') publishing. + # (or 'newHierarchyIntegration') publishing. "type": "Shot", }, }) @@ -432,15 +443,22 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if task_data: # Fill task data # - if we're in editorial, make sure the task type is filled - if ( - not instance.data.get("newAssetPublishing") - or task_data["type"] - ): + new_hierarchy = ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ) + if not new_hierarchy or task_data["type"]: anatomy_data["task"] = task_data return # New hierarchy is not created, so we can only skip rest of the logic - if not instance.data.get("newAssetPublishing"): + new_hierarchy = ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ) + if not new_hierarchy: return # Try to find task data based on hierarchy context and folder path diff --git a/client/ayon_core/plugins/publish/collect_farm_target.py b/client/ayon_core/plugins/publish/collect_farm_target.py index 3bf89450ec..e0edd795d8 100644 --- a/client/ayon_core/plugins/publish/collect_farm_target.py +++ b/client/ayon_core/plugins/publish/collect_farm_target.py @@ -14,22 +14,20 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): if not instance.data.get("farm"): return - context = instance.context + addons_manager = instance.context.data.get("ayonAddonsManager") - farm_name = "" - addons_manager = context.data.get("ayonAddonsManager") - - for farm_renderer in ["deadline", "royalrender"]: - addon = addons_manager.get(farm_renderer, False) - - if not addon: - self.log.error("Cannot find AYON addon '{0}'.".format( - farm_renderer)) - elif addon.enabled: + farm_renderer_addons = ["deadline", "royalrender"] + for farm_renderer in farm_renderer_addons: + addon = addons_manager.get(farm_renderer) + if addon and addon.enabled: farm_name = farm_renderer - - if farm_name: - self.log.debug("Collected render target: {0}".format(farm_name)) - instance.data["toBeRenderedOn"] = farm_name + break else: - AssertionError("No AYON renderer addon found") + # No enabled farm render addon found, then report all farm + # addons that were searched for yet not found + for farm_renderer in farm_renderer_addons: + self.log.error(f"Cannot find AYON addon '{farm_renderer}'.") + raise RuntimeError("No AYON renderer addon found.") + + self.log.debug("Collected render target: {0}".format(farm_name)) + instance.data["toBeRenderedOn"] = farm_name diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index b04900c74e..ea4823d62a 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -27,7 +27,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "nuke", "photoshop", "resolve", - "tvpaint" + "tvpaint", + "motionbuilder", + "substancepainter" ] # in some cases of headless publishing (for example webpublisher using PS) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1130c575a3..a28a761e7e 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -202,43 +202,16 @@ class ExtractOIIOTranscode(publish.Extractor): added_representations = True if added_representations: - self._mark_original_repre_for_deletion(repre, profile, - added_review) + self._mark_original_repre_for_deletion( + repre, profile, added_review + ) - for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) instance.data["representations"].extend(new_representations) - def _rename_in_representation(self, new_repre, files_to_convert, - output_name, output_extension): - """Replace old extension with new one everywhere in representation. - - Args: - new_repre (dict) - files_to_convert (list): of filenames from repre["files"], - standardized to always list - output_name (str): key of output definition from Settings, - if "" token used, keep original repre name - output_extension (str): extension from output definition - """ - if output_name != "passthrough": - new_repre["name"] = output_name - if not output_extension: - return - - new_repre["ext"] = output_extension - - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(file_name) - new_repre["files"] = renamed_files - def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. @@ -364,7 +337,7 @@ class ExtractOIIOTranscode(publish.Extractor): if not repre.get("colorspaceData"): self.log.debug("Representation '{}' has no colorspace data. " - "Skipped.") + "Skipped.".format(repre["name"])) return False return True diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 865b566e6e..1a4cda4dbb 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -380,29 +380,28 @@ class IntegrateAsset(pyblish.api.InstancePlugin): data = { "families": get_instance_families(instance) } - attribibutes = {} + attributes = {} product_group = instance.data.get("productGroup") if product_group: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group elif existing_product_entity: # Preserve previous product group if new version does not set it product_group = existing_product_entity.get("attrib", {}).get( "productGroup" ) if product_group is not None: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - product_entity = new_product_entity( product_name, product_type, folder_entity["id"], data=data, - attribs=attribibutes, + attribs=attributes, entity_id=product_id ) @@ -464,6 +463,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): version_number, product_entity["id"], task_id=task_id, + status=instance.data.get("status"), data=version_data, attribs=version_attributes, entity_id=version_id, diff --git a/client/ayon_core/plugins/publish/validate_asset_docs.py b/client/ayon_core/plugins/publish/validate_asset_docs.py index 95fe4252be..b80b81b366 100644 --- a/client/ayon_core/plugins/publish/validate_asset_docs.py +++ b/client/ayon_core/plugins/publish/validate_asset_docs.py @@ -24,7 +24,11 @@ class ValidateFolderEntities(pyblish.api.InstancePlugin): if instance.data.get("folderEntity"): self.log.debug("Instance has set fodler entity in its data.") - elif instance.data.get("newAssetPublishing"): + elif ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ): # skip if it is editorial self.log.debug("Editorial instance has no need to check...") diff --git a/client/ayon_core/plugins/publish/validate_version.py b/client/ayon_core/plugins/publish/validate_version.py index 9031194e8c..0359f8fb53 100644 --- a/client/ayon_core/plugins/publish/validate_version.py +++ b/client/ayon_core/plugins/publish/validate_version.py @@ -1,8 +1,14 @@ import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError + +from ayon_core.lib import filter_profiles +from ayon_core.pipeline.publish import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from ayon_core.pipeline import get_current_host_name -class ValidateVersion(pyblish.api.InstancePlugin): +class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate instance version. AYON does not allow overwriting previously published versions. @@ -11,13 +17,39 @@ class ValidateVersion(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Version" - hosts = ["nuke", "maya", "houdini", "blender", - "photoshop", "aftereffects"] optional = False active = True + @classmethod + def apply_settings(cls, settings): + # Disable if no profile is found for the current host + profiles = ( + settings + ["core"] + ["publish"] + ["ValidateVersion"] + ["plugin_state_profiles"] + ) + profile = filter_profiles( + profiles, {"host_names": get_current_host_name()} + ) + if not profile: + cls.enabled = False + return + + # Apply settings from profile + for attr_name in { + "enabled", + "optional", + "active", + }: + setattr(cls, attr_name, profile[attr_name]) + def process(self, instance): + if not self.is_active(instance.data): + return + version = instance.data.get("version") latest_version = instance.data.get("latestVersion") diff --git a/client/ayon_core/resources/app_icons/motionbuilder.png b/client/ayon_core/resources/app_icons/motionbuilder.png new file mode 100644 index 0000000000..68a17f7afb Binary files /dev/null and b/client/ayon_core/resources/app_icons/motionbuilder.png differ diff --git a/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg b/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg deleted file mode 100644 index c02b8f83d8..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg b/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg deleted file mode 100644 index e838ee9f28..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg b/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg deleted file mode 100644 index 5cf5d423dd..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg b/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg deleted file mode 100644 index f549e6142b..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg b/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg deleted file mode 100644 index 18efc273aa..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg b/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg deleted file mode 100644 index 0e5821b0be..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg b/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg deleted file mode 100644 index 855bdae7c5..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/Delivery.svg b/client/ayon_core/resources/ftrack/action_icons/Delivery.svg deleted file mode 100644 index a6333333ae..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/Delivery.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg b/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg deleted file mode 100644 index 40113fc709..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg b/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg deleted file mode 100644 index c2abc6146f..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg b/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg deleted file mode 100644 index 644d83f84d..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/RV.png b/client/ayon_core/resources/ftrack/action_icons/RV.png deleted file mode 100644 index 741e7a9772..0000000000 Binary files a/client/ayon_core/resources/ftrack/action_icons/RV.png and /dev/null differ diff --git a/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg b/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg deleted file mode 100644 index ff818b5ecb..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/SortReview.svg b/client/ayon_core/resources/ftrack/action_icons/SortReview.svg deleted file mode 100644 index 13a7def648..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/SortReview.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/TestAction.svg b/client/ayon_core/resources/ftrack/action_icons/TestAction.svg deleted file mode 100644 index 917ef2d0c7..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/TestAction.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg b/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg deleted file mode 100644 index 9af330e79a..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/sign_in_message.html b/client/ayon_core/resources/ftrack/sign_in_message.html deleted file mode 100644 index 8ee2828c26..0000000000 --- a/client/ayon_core/resources/ftrack/sign_in_message.html +++ /dev/null @@ -1,32 +0,0 @@ - - - -

Sign in to Ftrack was successful

-

- You signed in with username {}. -

-

- You can close this window now. -

- - diff --git a/client/ayon_core/scripts/otio_burnin.py b/client/ayon_core/scripts/otio_burnin.py index f12d298ac6..6b132b9a6a 100644 --- a/client/ayon_core/scripts/otio_burnin.py +++ b/client/ayon_core/scripts/otio_burnin.py @@ -14,9 +14,10 @@ from ayon_core.lib import ( convert_ffprobe_fps_value, ) +FFMPEG_EXE_COMMAND = subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) FFMPEG = ( '{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' -).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))) +).format(FFMPEG_EXE_COMMAND) DRAWTEXT = ( "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" @@ -482,10 +483,19 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): ) print("Launching command: {}".format(command)) + use_shell = True + try: + test_proc = subprocess.Popen( + f"{FFMPEG_EXE_COMMAND} --help", shell=True + ) + test_proc.wait() + except BaseException: + use_shell = False + kwargs = { "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, - "shell": True, + "shell": use_shell, } proc = subprocess.Popen(command, **kwargs) diff --git a/client/ayon_core/tools/adobe_webserver/readme.txt b/client/ayon_core/tools/adobe_webserver/readme.txt deleted file mode 100644 index d02d390277..0000000000 --- a/client/ayon_core/tools/adobe_webserver/readme.txt +++ /dev/null @@ -1,12 +0,0 @@ -Adobe webserver ---------------- -Aiohttp (Asyncio) based websocket server used for communication with host -applications, currently only for Adobe (but could be used for any non python -DCC which has websocket client). - -This webserver is started in spawned Python process that opens DCC during -its launch, waits for connection from DCC and handles communication going -forward. Server is closed before Python process is killed. - -(Different from `ayon_core/modules/webserver` as that one is running in Tray, -this one is running in spawn Python process.) \ No newline at end of file diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index 8895515b1a..f09edfeab2 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -14,6 +14,7 @@ from .hierarchy import ( ) from .thumbnails import ThumbnailsModel from .selection import HierarchyExpectedSelection +from .users import UsersModel __all__ = ( @@ -32,4 +33,6 @@ __all__ = ( "ThumbnailsModel", "HierarchyExpectedSelection", + + "UsersModel", ) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 19a38bee21..89dd881a10 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -5,7 +5,7 @@ import ayon_api import six from ayon_core.style import get_default_entity_icon_color -from ayon_core.lib import CacheItem +from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" @@ -17,6 +17,49 @@ class AbstractHierarchyController: pass +class StatusItem: + """Item representing status of project. + + Args: + name (str): Status name ("Not ready"). + color (str): Status color in hex ("#434a56"). + short (str): Short status name ("NRD"). + icon (str): Icon name in MaterialIcons ("fiber_new"). + state (Literal["not_started", "in_progress", "done", "blocked"]): + Status state. + + """ + def __init__(self, name, color, short, icon, state): + self.name = name + self.color = color + self.short = short + self.icon = icon + self.state = state + + def to_data(self): + return { + "name": self.name, + "color": self.color, + "short": self.short, + "icon": self.icon, + "state": self.state, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + @classmethod + def from_project_item(cls, status_data): + return cls( + name=status_data["name"], + color=status_data["color"], + short=status_data["shortName"], + icon=status_data["icon"], + state=status_data["state"], + ) + + class ProjectItem: """Item representing folder entity on a server. @@ -40,6 +83,23 @@ class ProjectItem: } self.icon = icon + @classmethod + def from_entity(cls, project_entity): + """Creates folder item from entity. + + Args: + project_entity (dict[str, Any]): Project entity. + + Returns: + ProjectItem: Project item. + + """ + return cls( + project_entity["name"], + project_entity["active"], + project_entity["library"], + ) + def to_data(self): """Converts folder item to data. @@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects): """ return [ - ProjectItem(project["name"], project["active"], project["library"]) + ProjectItem.from_entity(project) for project in projects ] @@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): self._projects_cache = CacheItem(default_factory=list) - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache = NestedCacheItem( + levels=1, default_factory=list + ) + self._projects_by_name = NestedCacheItem( + levels=1, default_factory=list + ) self._is_refreshing = False self._controller = controller def reset(self): self._projects_cache.reset() - self._project_items_by_name = {} - self._projects_by_name = {} + self._project_statuses_cache.reset() + self._projects_by_name.reset() def refresh(self): + """Refresh project items. + + This method will requery list of ProjectItem returned by + 'get_project_items'. + + To reset all cached items use 'reset' method. + """ self._refresh_projects_cache() def get_project_items(self, sender): @@ -117,12 +188,51 @@ class ProjectsModel(object): return self._projects_cache.get_data() def get_project_entity(self, project_name): - if project_name not in self._projects_by_name: + """Get project entity. + + Args: + project_name (str): Project name. + + Returns: + Union[dict[str, Any], None]: Project entity or None if project + was not found by name. + + """ + project_cache = self._projects_by_name[project_name] + if not project_cache.is_valid: entity = None if project_name: entity = ayon_api.get_project(project_name) - self._projects_by_name[project_name] = entity - return self._projects_by_name[project_name] + project_cache.update_data(entity) + return project_cache.get_data() + + def get_project_status_items(self, project_name, sender): + """Get project status items. + + Args: + project_name (str): Project name. + sender (Union[str, None]): Name of sender who asked for items. + + Returns: + list[StatusItem]: Status items for project. + + """ + statuses_cache = self._project_statuses_cache[project_name] + if not statuses_cache.is_valid: + with self._project_statuses_refresh_event_manager( + sender, project_name + ): + project_entity = None + if project_name: + project_entity = self.get_project_entity(project_name) + statuses = [] + if project_entity: + statuses = [ + StatusItem.from_project_item(status) + for status in project_entity["statuses"] + ] + statuses_cache.update_data(statuses) + return statuses_cache.get_data() @contextlib.contextmanager def _project_refresh_event_manager(self, sender): @@ -143,6 +253,23 @@ class ProjectsModel(object): ) self._is_refreshing = False + @contextlib.contextmanager + def _project_statuses_refresh_event_manager(self, sender, project_name): + self._controller.emit_event( + "projects.statuses.refresh.started", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "projects.statuses.refresh.finished", + {"sender": sender, "project_name": project_name}, + PROJECTS_MODEL_SENDER + ) + def _refresh_projects_cache(self, sender=None): if self._is_refreshing: return None diff --git a/client/ayon_core/tools/common_models/users.py b/client/ayon_core/tools/common_models/users.py new file mode 100644 index 0000000000..f7939e5cd3 --- /dev/null +++ b/client/ayon_core/tools/common_models/users.py @@ -0,0 +1,164 @@ +import json +import collections + +import ayon_api +from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict + +from ayon_core.lib import NestedCacheItem + + +# --- Implementation that should be in ayon-python-api --- +# The implementation is not available in all versions of ayon-python-api. +def users_graphql_query(fields): + query = GraphQlQuery("Users") + names_var = query.add_variable("userNames", "[String!]") + project_name_var = query.add_variable("projectName", "String!") + + users_field = query.add_field_with_edges("users") + users_field.set_filter("names", names_var) + users_field.set_filter("projectName", project_name_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, users_field)) + + while query_queue: + item = query_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + query_queue.append((k, v, field)) + return query + + +def get_users(project_name=None, usernames=None, fields=None): + """Get Users. + + Only administrators and managers can fetch all users. For other users + it is required to pass in 'project_name' filter. + + Args: + project_name (Optional[str]): Project name. + usernames (Optional[Iterable[str]]): Filter by usernames. + fields (Optional[Iterable[str]]): Fields to be queried + for users. + + Returns: + Generator[dict[str, Any]]: Queried users. + + """ + filters = {} + if usernames is not None: + usernames = set(usernames) + if not usernames: + return + filters["userNames"] = list(usernames) + + if project_name is not None: + filters["projectName"] = project_name + + con = ayon_api.get_server_api_connection() + if not fields: + fields = con.get_default_fields_for_type("user") + + query = users_graphql_query(set(fields)) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(con): + for user in parsed_data["users"]: + user["accessGroups"] = json.loads(user["accessGroups"]) + yield user +# --- END of ayon-python-api implementation --- + + +class UserItem: + def __init__( + self, + username, + full_name, + email, + avatar_url, + active, + ): + self.username = username + self.full_name = full_name + self.email = email + self.avatar_url = avatar_url + self.active = active + + @classmethod + def from_entity_data(cls, user_data): + return cls( + user_data["name"], + user_data["attrib"]["fullName"], + user_data["attrib"]["email"], + user_data["attrib"]["avatarUrl"], + user_data["active"], + ) + + +class UsersModel: + def __init__(self, controller): + self._controller = controller + self._users_cache = NestedCacheItem(default_factory=list) + + def get_user_items(self, project_name): + """Get user items. + + Returns: + List[UserItem]: List of user items. + + """ + self._invalidate_cache(project_name) + return self._users_cache[project_name].get_data() + + def get_user_items_by_name(self, project_name): + """Get user items by name. + + Implemented as most of cases using this model will need to find + user information by username. + + Returns: + Dict[str, UserItem]: Dictionary of user items by name. + + """ + return { + user_item.username: user_item + for user_item in self.get_user_items(project_name) + } + + def get_user_item_by_username(self, project_name, username): + """Get user item by username. + + Args: + username (str): Username. + + Returns: + Union[UserItem, None]: User item or None if not found. + + """ + self._invalidate_cache(project_name) + for user_item in self.get_user_items(project_name): + if user_item.username == username: + return user_item + return None + + def _invalidate_cache(self, project_name): + cache = self._users_cache[project_name] + if cache.is_valid: + return + + if project_name is None: + cache.update_data([]) + return + + self._users_cache[project_name].update_data([ + UserItem.from_entity_data(user) + for user in get_users(project_name) + ]) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 226a57930b..ad48e8ac77 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -70,7 +70,7 @@ class HierarchyPage(QtWidgets.QWidget): main_layout.addWidget(content_body, 1) btn_back.clicked.connect(self._on_back_clicked) - refresh_btn.clicked.connect(self._on_refreh_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) folders_filter_text.textChanged.connect(self._on_filter_text_changed) self._is_visible = False @@ -99,7 +99,7 @@ class HierarchyPage(QtWidgets.QWidget): def _on_back_clicked(self): self._controller.set_selected_project(None) - def _on_refreh_clicked(self): + def _on_refresh_clicked(self): self._controller.refresh() def _on_filter_text_changed(self, text): diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 7a7d335092..ba1dcb73b6 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -114,6 +114,7 @@ class VersionItem: thumbnail_id (Union[str, None]): Thumbnail id. published_time (Union[str, None]): Published time in format '%Y%m%dT%H%M%SZ'. + status (Union[str, None]): Status name. author (Union[str, None]): Author. frame_range (Union[str, None]): Frame range. duration (Union[int, None]): Duration. @@ -132,6 +133,7 @@ class VersionItem: thumbnail_id, published_time, author, + status, frame_range, duration, handles, @@ -146,6 +148,7 @@ class VersionItem: self.is_hero = is_hero self.published_time = published_time self.author = author + self.status = status self.frame_range = frame_range self.duration = duration self.handles = handles @@ -169,12 +172,30 @@ class VersionItem: def __gt__(self, other): if not isinstance(other, VersionItem): return False - if ( - other.version == self.version - and self.is_hero - ): + # Make sure hero versions are positive + version = abs(self.version) + other_version = abs(other.version) + # Hero version is greater than non-hero + if version == other_version: + return not self.is_hero + return version > other_version + + def __lt__(self, other): + if not isinstance(other, VersionItem): return True - return other.version < self.version + # Make sure hero versions are positive + version = abs(self.version) + other_version = abs(other.version) + # Non-hero version is lesser than hero + if version == other_version: + return self.is_hero + return version < other_version + + def __ge__(self, other): + return self.__eq__(other) or self.__gt__(other) + + def __le__(self, other): + return self.__eq__(other) or self.__lt__(other) def to_data(self): return { @@ -185,6 +206,7 @@ class VersionItem: "is_hero": self.is_hero, "published_time": self.published_time, "author": self.author, + "status": self.status, "frame_range": self.frame_range, "duration": self.duration, "handles": self.handles, @@ -488,6 +510,27 @@ class FrontendLoaderController(_BaseLoaderController): pass + @abstractmethod + def get_project_status_items(self, project_name, sender=None): + """Items for all projects available on server. + + Triggers event topics "projects.statuses.refresh.started" and + "projects.statuses.refresh.finished" with data: + { + "sender": sender, + "project_name": project_name + } + + Args: + project_name (Union[str, None]): Project name. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[StatusItem]: List of status items. + """ + + pass + @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 0c9bb369c7..9fead226f0 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) + def get_project_status_items(self, project_name, sender=None): + return self._projects_model.get_project_status_items( + project_name, sender + ) + def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) @@ -343,10 +348,18 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): return set() if not self._loaded_products_cache.is_valid: - if isinstance(self._host, ILoadHost): - containers = self._host.get_containers() - else: - containers = self._host.ls() + try: + if isinstance(self._host, ILoadHost): + containers = self._host.get_containers() + else: + containers = self._host.ls() + + except BaseException: + self.log.error( + "Failed to collect loaded products.", exc_info=True + ) + containers = [] + repre_ids = set() for container in containers: repre_id = container.get("representation") diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index a3bbc30a09..c9325c4480 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -58,6 +58,7 @@ def version_item_from_entity(version): thumbnail_id=version["thumbnailId"], published_time=published_time, author=author, + status=version["status"], frame_range=frame_range, duration=duration, handles=handles, @@ -526,8 +527,11 @@ class ProductsModel: products = list(ayon_api.get_products(project_name, **kwargs)) product_ids = {product["id"] for product in products} + # Add 'status' to fields -> fixed in ayon-python-api 1.0.4 + fields = ayon_api.get_default_fields_for_type("version") + fields.add("status") versions = ayon_api.get_versions( - project_name, product_ids=product_ids + project_name, product_ids=product_ids, fields=fields ) return self._create_product_items( diff --git a/client/ayon_core/tools/loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py index 7b146456da..efd041937d 100644 --- a/client/ayon_core/tools/loader/ui/folders_widget.py +++ b/client/ayon_core/tools/loader/ui/folders_widget.py @@ -321,6 +321,8 @@ class LoaderFoldersWidget(QtWidgets.QWidget): """ self._folders_proxy_model.setFilterFixedString(name) + if name: + self._folders_view.expandAll() def set_merged_products_selection(self, items): """ diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 12ed1165ae..cedac6199b 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -104,7 +104,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): style = QtWidgets.QApplication.style() style.drawControl( - style.CE_ItemViewItem, option, painter, option.widget + QtWidgets.QCommonStyle.CE_ItemViewItem, + option, + painter, + option.widget ) painter.save() @@ -116,9 +119,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): pen.setColor(fg_color) painter.setPen(pen) - text_rect = style.subElementRect(style.SE_ItemViewItemText, option) + text_rect = style.subElementRect( + QtWidgets.QCommonStyle.SE_ItemViewItemText, + option + ) text_margin = style.proxy().pixelMetric( - style.PM_FocusFrameHMargin, option, option.widget + QtWidgets.QCommonStyle.PM_FocusFrameHMargin, + option, + option.widget ) + 1 painter.drawText( diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index b465679c3b..7b9124608b 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -22,18 +22,22 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11 VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12 VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13 VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14 -VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15 -VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16 -VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17 -VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18 -VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19 -VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20 -VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21 -ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22 -REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 -REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24 -SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25 -SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17 +VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 18 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 19 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 20 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 21 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 22 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 23 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 24 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 25 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 28 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 class ProductsModel(QtGui.QStandardItemModel): @@ -44,6 +48,7 @@ class ProductsModel(QtGui.QStandardItemModel): "Product type", "Folder", "Version", + "Status", "Time", "Author", "Frames", @@ -69,11 +74,35 @@ class ProductsModel(QtGui.QStandardItemModel): ] ] - version_col = column_labels.index("Version") - published_time_col = column_labels.index("Time") + product_name_col = column_labels.index("Product name") + product_type_col = column_labels.index("Product type") folders_label_col = column_labels.index("Folder") + version_col = column_labels.index("Version") + status_col = column_labels.index("Status") + published_time_col = column_labels.index("Time") + author_col = column_labels.index("Author") + frame_range_col = column_labels.index("Frames") + duration_col = column_labels.index("Duration") + handles_col = column_labels.index("Handles") + step_col = column_labels.index("Step") in_scene_col = column_labels.index("In scene") sitesync_avail_col = column_labels.index("Availability") + _display_role_mapping = { + product_name_col: QtCore.Qt.DisplayRole, + product_type_col: PRODUCT_TYPE_ROLE, + folders_label_col: FOLDER_LABEL_ROLE, + version_col: VERSION_NAME_ROLE, + status_col: VERSION_STATUS_NAME_ROLE, + published_time_col: VERSION_PUBLISH_TIME_ROLE, + author_col: VERSION_AUTHOR_ROLE, + frame_range_col: VERSION_FRAME_RANGE_ROLE, + duration_col: VERSION_DURATION_ROLE, + handles_col: VERSION_HANDLES_ROLE, + step_col: VERSION_STEP_ROLE, + in_scene_col: PRODUCT_IN_SCENE_ROLE, + sitesync_avail_col: VERSION_AVAILABLE_ROLE, + + } def __init__(self, controller): super(ProductsModel, self).__init__() @@ -96,6 +125,7 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = None self._last_folder_ids = [] + self._last_project_statuses = {} def get_product_item_indexes(self): return [ @@ -141,6 +171,15 @@ class ProductsModel(QtGui.QStandardItemModel): if not index.isValid(): return None + if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE): + status_name = self.data(index, VERSION_STATUS_NAME_ROLE) + status_item = self._last_project_statuses.get(status_name) + if status_item is None: + return "" + if role == VERSION_STATUS_SHORT_ROLE: + return status_item.short + return status_item.color + col = index.column() if col == 0: return super(ProductsModel, self).data(index, role) @@ -160,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel): product_item = self._product_items_by_id.get(product_id) if product_item is None: return None - return list(product_item.version_items.values()) + product_items = list(product_item.version_items.values()) + product_items.sort(reverse=True) + return product_items if role == QtCore.Qt.EditRole: return None @@ -168,29 +209,8 @@ class ProductsModel(QtGui.QStandardItemModel): if role == QtCore.Qt.DisplayRole: if not index.data(PRODUCT_ID_ROLE): return None - if col == self.version_col: - role = VERSION_NAME_ROLE - elif col == 1: - role = PRODUCT_TYPE_ROLE - elif col == 2: - role = FOLDER_LABEL_ROLE - elif col == 4: - role = VERSION_PUBLISH_TIME_ROLE - elif col == 5: - role = VERSION_AUTHOR_ROLE - elif col == 6: - role = VERSION_FRAME_RANGE_ROLE - elif col == 7: - role = VERSION_DURATION_ROLE - elif col == 8: - role = VERSION_HANDLES_ROLE - elif col == 9: - role = VERSION_STEP_ROLE - elif col == 10: - role = PRODUCT_IN_SCENE_ROLE - elif col == 11: - role = VERSION_AVAILABLE_ROLE - else: + role = self._display_role_mapping.get(col) + if role is None: return None index = self.index(index.row(), 0, index.parent()) @@ -312,6 +332,7 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.published_time, VERSION_PUBLISH_TIME_ROLE ) model_item.setData(version_item.author, VERSION_AUTHOR_ROLE) + model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE) model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE) model_item.setData(version_item.duration, VERSION_DURATION_ROLE) model_item.setData(version_item.handles, VERSION_HANDLES_ROLE) @@ -393,6 +414,11 @@ class ProductsModel(QtGui.QStandardItemModel): self._last_project_name = project_name self._last_folder_ids = folder_ids + status_items = self._controller.get_project_status_items(project_name) + self._last_project_statuses = { + status_item.name: status_item + for status_item in status_items + } active_site_icon_def = self._controller.get_active_site_icon_def( project_name diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index d9f027153e..61ddd690e9 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -6,7 +6,7 @@ from ayon_core.tools.utils import ( RecursiveSortFilterProxyModel, DeselectableTreeView, ) -from ayon_core.tools.utils.delegates import PrettyTimeDelegate +from ayon_core.tools.utils.delegates import PrettyTimeDelegate, StatusDelegate from .products_model import ( ProductsModel, @@ -17,12 +17,16 @@ from .products_model import ( FOLDER_ID_ROLE, PRODUCT_ID_ROLE, VERSION_ID_ROLE, + VERSION_STATUS_NAME_ROLE, + VERSION_STATUS_SHORT_ROLE, + VERSION_STATUS_COLOR_ROLE, + VERSION_STATUS_ICON_ROLE, VERSION_THUMBNAIL_ID_ROLE, ) from .products_delegates import ( VersionDelegate, LoadedInSceneDelegate, - SiteSyncDelegate + SiteSyncDelegate, ) from .actions_utils import show_actions_menu @@ -89,6 +93,7 @@ class ProductsWidget(QtWidgets.QWidget): 90, # Product type 130, # Folder label 60, # Version + 100, # Status 125, # Time 75, # Author 75, # Frames @@ -128,20 +133,24 @@ class ProductsWidget(QtWidgets.QWidget): products_view.setColumnWidth(idx, width) version_delegate = VersionDelegate() - products_view.setItemDelegateForColumn( - products_model.version_col, version_delegate) - time_delegate = PrettyTimeDelegate() - products_view.setItemDelegateForColumn( - products_model.published_time_col, time_delegate) - + status_delegate = StatusDelegate( + VERSION_STATUS_NAME_ROLE, + VERSION_STATUS_SHORT_ROLE, + VERSION_STATUS_COLOR_ROLE, + VERSION_STATUS_ICON_ROLE, + ) in_scene_delegate = LoadedInSceneDelegate() - products_view.setItemDelegateForColumn( - products_model.in_scene_col, in_scene_delegate) - sitesync_delegate = SiteSyncDelegate() - products_view.setItemDelegateForColumn( - products_model.sitesync_avail_col, sitesync_delegate) + + for col, delegate in ( + (products_model.version_col, version_delegate), + (products_model.published_time_col, time_delegate), + (products_model.status_col, status_delegate), + (products_model.in_scene_col, in_scene_delegate), + (products_model.sitesync_avail_col, sitesync_delegate), + ): + products_view.setItemDelegateForColumn(col, delegate) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -175,6 +184,7 @@ class ProductsWidget(QtWidgets.QWidget): self._version_delegate = version_delegate self._time_delegate = time_delegate + self._status_delegate = status_delegate self._in_scene_delegate = in_scene_delegate self._sitesync_delegate = sitesync_delegate diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index ede772b917..4e2cfd8783 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -8,6 +8,7 @@ import tempfile import shutil import inspect from abc import ABCMeta, abstractmethod +import re import six import arrow @@ -39,6 +40,7 @@ from ayon_core.pipeline.create.context import ( ) from ayon_core.pipeline.publish import get_publish_instance_label from ayon_core.tools.common_models import HierarchyModel +from ayon_core.lib.profiles_filtering import filter_profiles # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -1686,6 +1688,15 @@ class PublisherController(BasePublisherController): """Publish plugins.""" return self._create_context.publish_plugins + def _get_current_project_settings(self): + """Current project settings. + + Returns: + dict + """ + + return self._create_context.get_current_project_settings() + # Hierarchy model def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) @@ -1827,8 +1838,13 @@ class PublisherController(BasePublisherController): def _collect_creator_items(self): # TODO add crashed initialization of create plugins to report output = {} + allowed_creator_pattern = self._get_allowed_creators_pattern() for identifier, creator in self._create_context.creators.items(): try: + if (not self._is_label_allowed( + creator.label, allowed_creator_pattern)): + self.log.debug(f"{creator.label} not allowed for context") + continue output[identifier] = CreatorItem.from_creator(creator) except Exception: self.log.error( @@ -1839,6 +1855,60 @@ class PublisherController(BasePublisherController): return output + def _get_allowed_creators_pattern(self): + """Provide regex pattern for configured creator labels in this context + + If no profile matches current context, it shows all creators. + Support usage of regular expressions for configured values. + Returns: + (re.Pattern)[optional]: None or regex compiled patterns + into single one ('Render|Image.*') + """ + + task_type = self._create_context.get_current_task_type() + project_settings = self._get_current_project_settings() + + filter_creator_profiles = ( + project_settings + ["core"] + ["tools"] + ["creator"] + ["filter_creator_profiles"] + ) + filtering_criteria = { + "task_names": self.current_task_name, + "task_types": task_type, + "host_names": self._create_context.host_name + } + profile = filter_profiles( + filter_creator_profiles, + filtering_criteria, + logger=self.log + ) + + allowed_creator_pattern = None + if profile: + allowed_creator_labels = { + label + for label in profile["creator_labels"] + if label + } + self.log.debug(f"Only allowed `{allowed_creator_labels}` creators") + allowed_creator_pattern = ( + re.compile("|".join(allowed_creator_labels))) + return allowed_creator_pattern + + def _is_label_allowed(self, label, allowed_labels_regex): + """Implement regex support for allowed labels. + + Args: + label (str): Label of creator - shown in Publisher + allowed_labels_regex (re.Pattern): compiled regular expression + """ + if not allowed_labels_regex: + return True + return bool(allowed_labels_regex.match(label)) + def _reset_instances(self): """Reset create instances.""" if self._resetting_instances: diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 592113455c..b890462506 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -1,14 +1,14 @@ import ayon_api from ayon_core.lib.events import QueuedEventSystem -from ayon_core.host import ILoadHost +from ayon_core.host import HostBase from ayon_core.pipeline import ( registered_host, get_current_context, ) -from ayon_core.tools.common_models import HierarchyModel +from ayon_core.tools.common_models import HierarchyModel, ProjectsModel -from .models import SiteSyncModel +from .models import SiteSyncModel, ContainersModel class SceneInventoryController: @@ -28,11 +28,16 @@ class SceneInventoryController: self._current_folder_id = None self._current_folder_set = False + self._containers_model = ContainersModel(self) self._sitesync_model = SiteSyncModel(self) # Switch dialog requirements self._hierarchy_model = HierarchyModel(self) + self._projects_model = ProjectsModel(self) self._event_system = self._create_event_system() + def get_host(self) -> HostBase: + return self._host + def emit_event(self, topic, data=None, source=None): if data is None: data = {} @@ -47,6 +52,7 @@ class SceneInventoryController: self._current_folder_id = None self._current_folder_set = False + self._containers_model.reset() self._sitesync_model.reset() self._hierarchy_model.reset() @@ -80,13 +86,32 @@ class SceneInventoryController: self._current_folder_set = True return self._current_folder_id + def get_project_status_items(self): + project_name = self.get_current_project_name() + return self._projects_model.get_project_status_items( + project_name, None + ) + + # Containers methods def get_containers(self): - host = self._host - if isinstance(host, ILoadHost): - return list(host.get_containers()) - elif hasattr(host, "ls"): - return list(host.ls()) - return [] + return self._containers_model.get_containers() + + def get_containers_by_item_ids(self, item_ids): + return self._containers_model.get_containers_by_item_ids(item_ids) + + def get_container_items(self): + return self._containers_model.get_container_items() + + def get_container_items_by_id(self, item_ids): + return self._containers_model.get_container_items_by_id(item_ids) + + def get_representation_info_items(self, representation_ids): + return self._containers_model.get_representation_info_items( + representation_ids + ) + + def get_version_items(self, product_ids): + return self._containers_model.get_version_items(product_ids) # Site Sync methods def is_sitesync_enabled(self): diff --git a/client/ayon_core/tools/sceneinventory/delegates.py b/client/ayon_core/tools/sceneinventory/delegates.py index 2126fa1cbe..6f91587613 100644 --- a/client/ayon_core/tools/sceneinventory/delegates.py +++ b/client/ayon_core/tools/sceneinventory/delegates.py @@ -1,38 +1,10 @@ -import numbers - -import ayon_api - -from ayon_core.pipeline import HeroVersionType -from ayon_core.tools.utils.models import TreeModel -from ayon_core.tools.utils.lib import format_version - from qtpy import QtWidgets, QtCore, QtGui +from .model import VERSION_LABEL_ROLE + class VersionDelegate(QtWidgets.QStyledItemDelegate): """A delegate that display version integer formatted as version string.""" - - version_changed = QtCore.Signal() - first_run = False - lock = False - - def __init__(self, controller, *args, **kwargs): - self._controller = controller - super(VersionDelegate, self).__init__(*args, **kwargs) - - def get_project_name(self): - return self._controller.get_current_project_name() - - def displayText(self, value, locale): - if isinstance(value, HeroVersionType): - return format_version(value) - if not isinstance(value, numbers.Integral): - # For cases where no version is resolved like NOT FOUND cases - # where a representation might not exist in current database - return - - return format_version(value) - def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) if fg_color: @@ -44,7 +16,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): fg_color = None if not fg_color: - return super(VersionDelegate, self).paint(painter, option, index) + return super().paint(painter, option, index) if option.widget: style = option.widget.style() @@ -60,9 +32,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): painter.save() - text = self.displayText( - index.data(QtCore.Qt.DisplayRole), option.locale - ) + text = index.data(VERSION_LABEL_ROLE) pen = painter.pen() pen.setColor(fg_color) painter.setPen(pen) @@ -82,77 +52,3 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): ) painter.restore() - - def createEditor(self, parent, option, index): - item = index.data(TreeModel.ItemRole) - if item.get("isGroup") or item.get("isMerged"): - return - - editor = QtWidgets.QComboBox(parent) - - def commit_data(): - if not self.first_run: - self.commitData.emit(editor) # Update model data - self.version_changed.emit() # Display model data - editor.currentIndexChanged.connect(commit_data) - - self.first_run = True - self.lock = False - - return editor - - def setEditorData(self, editor, index): - if self.lock: - # Only set editor data once per delegation - return - - editor.clear() - - # Current value of the index - item = index.data(TreeModel.ItemRole) - value = index.data(QtCore.Qt.DisplayRole) - - project_name = self.get_project_name() - # Add all available versions to the editor - product_id = item["version_entity"]["productId"] - version_entities = list(sorted( - ayon_api.get_versions( - project_name, product_ids={product_id}, active=True - ), - key=lambda item: abs(item["version"]) - )) - - selected = None - items = [] - is_hero_version = value < 0 - for version_entity in version_entities: - version = version_entity["version"] - label = format_version(version) - item = QtGui.QStandardItem(label) - item.setData(version_entity, QtCore.Qt.UserRole) - items.append(item) - - if ( - version == value - or is_hero_version and version < 0 - ): - selected = item - - # Reverse items so latest versions be upper - items.reverse() - for item in items: - editor.model().appendRow(item) - - index = 0 - if selected: - index = selected.row() - - # Will trigger index-change signal - editor.setCurrentIndex(index) - self.first_run = False - self.lock = True - - def setModelData(self, editor, model, index): - """Apply the integer version back in the model""" - version = editor.itemData(editor.currentIndex()) - model.setData(index, version["name"]) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 330b174218..1c51b7a98b 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -1,57 +1,113 @@ import re import logging -import uuid -from collections import defaultdict +import collections -import ayon_api from qtpy import QtCore, QtGui import qtawesome -from ayon_core.pipeline import ( - get_current_project_name, - HeroVersionType, -) from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.utils import get_qt_icon -from ayon_core.tools.utils.models import TreeModel, Item +from ayon_core.tools.utils.lib import format_version + +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +NAME_COLOR_ROLE = QtCore.Qt.UserRole + 2 +COUNT_ROLE = QtCore.Qt.UserRole + 3 +IS_CONTAINER_ITEM_ROLE = QtCore.Qt.UserRole + 4 +VERSION_IS_LATEST_ROLE = QtCore.Qt.UserRole + 5 +VERSION_IS_HERO_ROLE = QtCore.Qt.UserRole + 6 +VERSION_LABEL_ROLE = QtCore.Qt.UserRole + 7 +VERSION_COLOR_ROLE = QtCore.Qt.UserRole + 8 +STATUS_NAME_ROLE = QtCore.Qt.UserRole + 9 +STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 10 +STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 11 +STATUS_ICON_ROLE = QtCore.Qt.UserRole + 12 +PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 13 +PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 14 +PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 15 +PRODUCT_GROUP_NAME_ROLE = QtCore.Qt.UserRole + 16 +PRODUCT_GROUP_ICON_ROLE = QtCore.Qt.UserRole + 17 +LOADER_NAME_ROLE = QtCore.Qt.UserRole + 18 +OBJECT_NAME_ROLE = QtCore.Qt.UserRole + 19 +ACTIVE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 20 +REMOTE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 21 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 +# This value hold unique value of container that should be used to identify +# containers inbetween refresh. +ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 24 -def walk_hierarchy(node): - """Recursively yield group node.""" - for child in node.children(): - if child.get("isGroupNode"): - yield child - - for _child in walk_hierarchy(child): - yield _child - - -class InventoryModel(TreeModel): +class InventoryModel(QtGui.QStandardItemModel): """The model for the inventory""" - Columns = [ + column_labels = [ "Name", - "version", - "count", - "productType", - "group", - "loader", - "objectName", - "active_site", - "remote_site", + "Version", + "Status", + "Count", + "Product type", + "Group", + "Loader", + "Object name", + "Active site", + "Remote site", ] - active_site_col = Columns.index("active_site") - remote_site_col = Columns.index("remote_site") + name_col = column_labels.index("Name") + version_col = column_labels.index("Version") + status_col = column_labels.index("Status") + count_col = column_labels.index("Count") + product_type_col = column_labels.index("Product type") + product_group_col = column_labels.index("Group") + loader_col = column_labels.index("Loader") + object_name_col = column_labels.index("Object name") + active_site_col = column_labels.index("Active site") + remote_site_col = column_labels.index("Remote site") + display_role_by_column = { + name_col: QtCore.Qt.DisplayRole, + version_col: VERSION_LABEL_ROLE, + status_col: STATUS_NAME_ROLE, + count_col: COUNT_ROLE, + product_type_col: PRODUCT_TYPE_ROLE, + product_group_col: PRODUCT_GROUP_NAME_ROLE, + loader_col: LOADER_NAME_ROLE, + object_name_col: OBJECT_NAME_ROLE, + active_site_col: ACTIVE_SITE_PROGRESS_ROLE, + remote_site_col: REMOTE_SITE_PROGRESS_ROLE, + } + decoration_role_by_column = { + name_col: QtCore.Qt.DecorationRole, + product_type_col: PRODUCT_TYPE_ICON_ROLE, + product_group_col: PRODUCT_GROUP_ICON_ROLE, + active_site_col: ACTIVE_SITE_ICON_ROLE, + remote_site_col: REMOTE_SITE_ICON_ROLE, + } + foreground_role_by_column = { + name_col: NAME_COLOR_ROLE, + version_col: VERSION_COLOR_ROLE, + status_col: STATUS_COLOR_ROLE + } + width_by_column = { + name_col: 250, + version_col: 55, + status_col: 100, + count_col: 55, + product_type_col: 150, + product_group_col: 120, + loader_col: 150, + } OUTDATED_COLOR = QtGui.QColor(235, 30, 30) CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30) GRAYOUT_COLOR = QtGui.QColor(160, 160, 160) - UniqueRole = QtCore.Qt.UserRole + 2 # unique label role - def __init__(self, controller, parent=None): - super(InventoryModel, self).__init__(parent) + super().__init__(parent) + + self.setColumnCount(len(self.column_labels)) + for idx, label in enumerate(self.column_labels): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) + self.log = logging.getLogger(self.__class__.__name__) self._controller = controller @@ -60,103 +116,217 @@ class InventoryModel(TreeModel): self._default_icon_color = get_default_entity_icon_color() - site_icons = self._controller.get_site_provider_icons() - - self._site_icons = { - provider: get_qt_icon(icon_def) - for provider, icon_def in site_icons.items() - } - def outdated(self, item): return item.get("isOutdated", True) + def refresh(self, selected=None): + """Refresh the model""" + # for debugging or testing, injecting items from outside + container_items = self._controller.get_container_items() + + self._clear_items() + + items_by_repre_id = {} + for container_item in container_items: + # if ( + # selected is not None + # and container_item.item_id not in selected + # ): + # continue + repre_id = container_item.representation_id + items = items_by_repre_id.setdefault(repre_id, []) + items.append(container_item) + + repre_id = set(items_by_repre_id.keys()) + repre_info_by_id = self._controller.get_representation_info_items( + repre_id + ) + product_ids = { + repre_info.product_id + for repre_info in repre_info_by_id.values() + } + version_items_by_product_id = self._controller.get_version_items( + product_ids + ) + # SiteSync addon information + progress_by_id = self._controller.get_representations_site_progress( + repre_id + ) + sites_info = self._controller.get_sites_information() + site_icons = { + provider: get_qt_icon(icon_def) + for provider, icon_def in ( + self._controller.get_site_provider_icons().items() + ) + } + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items() + } + + group_item_icon = qtawesome.icon( + "fa.folder", color=self._default_icon_color + ) + valid_item_icon = qtawesome.icon( + "fa.file-o", color=self._default_icon_color + ) + invalid_item_icon = qtawesome.icon( + "fa.exclamation-circle", color=self._default_icon_color + ) + group_icon = qtawesome.icon( + "fa.object-group", color=self._default_icon_color + ) + product_type_icon = qtawesome.icon( + "fa.folder", color="#0091B2" + ) + group_item_font = QtGui.QFont() + group_item_font.setBold(True) + + active_site_icon = site_icons.get(sites_info["active_site_provider"]) + remote_site_icon = site_icons.get(sites_info["remote_site_provider"]) + + root_item = self.invisibleRootItem() + + group_items = [] + for repre_id, container_items in items_by_repre_id.items(): + repre_info = repre_info_by_id[repre_id] + version_label = "N/A" + version_color = None + is_latest = False + is_hero = False + status_name = None + status_color = None + status_short = None + if not repre_info.is_valid: + group_name = "< Entity N/A >" + item_icon = invalid_item_icon + + else: + group_name = "{}_{}: ({})".format( + repre_info.folder_path.rsplit("/")[-1], + repre_info.product_name, + repre_info.representation_name + ) + item_icon = valid_item_icon + + version_items = ( + version_items_by_product_id[repre_info.product_id] + ) + version_item = version_items[repre_info.version_id] + version_label = format_version(version_item.version) + is_hero = version_item.version < 0 + is_latest = version_item.is_latest + if not is_latest: + version_color = self.OUTDATED_COLOR + status_name = version_item.status + status_item = status_items_by_name.get(status_name) + if status_item: + status_short = status_item.short + status_color = status_item.color + + container_model_items = [] + for container_item in container_items: + unique_name = ( + repre_info.representation_name + + container_item.object_name or "" + ) + + item = QtGui.QStandardItem() + item.setColumnCount(root_item.columnCount()) + item.setData(container_item.namespace, QtCore.Qt.DisplayRole) + item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE) + item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE) + item.setData(item_icon, QtCore.Qt.DecorationRole) + item.setData(repre_info.product_id, PRODUCT_ID_ROLE) + item.setData(container_item.item_id, ITEM_ID_ROLE) + item.setData(version_label, VERSION_LABEL_ROLE) + item.setData(container_item.loader_name, LOADER_NAME_ROLE) + item.setData(container_item.object_name, OBJECT_NAME_ROLE) + item.setData(True, IS_CONTAINER_ITEM_ROLE) + item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) + container_model_items.append(item) + + if not container_model_items: + continue + + progress = progress_by_id[repre_id] + active_site_progress = "{}%".format( + max(progress["active_site"], 0) * 100 + ) + remote_site_progress = "{}%".format( + max(progress["remote_site"], 0) * 100 + ) + + group_item = QtGui.QStandardItem() + group_item.setColumnCount(root_item.columnCount()) + group_item.setData(group_name, QtCore.Qt.DisplayRole) + group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE) + group_item.setData(group_item_icon, QtCore.Qt.DecorationRole) + group_item.setData(group_item_font, QtCore.Qt.FontRole) + group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE) + group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE) + group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) + group_item.setData(is_latest, VERSION_IS_LATEST_ROLE) + group_item.setData(is_hero, VERSION_IS_HERO_ROLE) + group_item.setData(version_label, VERSION_LABEL_ROLE) + group_item.setData(len(container_items), COUNT_ROLE) + group_item.setData(status_name, STATUS_NAME_ROLE) + group_item.setData(status_short, STATUS_SHORT_ROLE) + group_item.setData(status_color, STATUS_COLOR_ROLE) + + group_item.setData( + active_site_progress, ACTIVE_SITE_PROGRESS_ROLE + ) + group_item.setData( + remote_site_progress, REMOTE_SITE_PROGRESS_ROLE + ) + group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE) + group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE) + group_item.setData(False, IS_CONTAINER_ITEM_ROLE) + + if version_color is not None: + group_item.setData(version_color, VERSION_COLOR_ROLE) + + if repre_info.product_group: + group_item.setData( + repre_info.product_group, PRODUCT_GROUP_NAME_ROLE + ) + group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE) + + group_item.appendRows(container_model_items) + group_items.append(group_item) + + if group_items: + root_item.appendRows(group_items) + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + def data(self, index, role): if not index.isValid(): return - item = index.internalPointer() + col = index.column() + if role == QtCore.Qt.DisplayRole: + role = self.display_role_by_column.get(col) + if role is None: + print(col, role) + return None - if role == QtCore.Qt.FontRole: - # Make top-level entries bold - if item.get("isGroupNode") or item.get("isNotSet"): # group-item - font = QtGui.QFont() - font.setBold(True) - return font + elif role == QtCore.Qt.DecorationRole: + role = self.decoration_role_by_column.get(col) + if role is None: + return None - if role == QtCore.Qt.ForegroundRole: - # Set the text color to the OUTDATED_COLOR when the - # collected version is not the same as the highest version - key = self.Columns[index.column()] - if key == "version": # version - if item.get("isGroupNode"): # group-item - if self.outdated(item): - return self.OUTDATED_COLOR + elif role == QtCore.Qt.ForegroundRole: + role = self.foreground_role_by_column.get(col) + if role is None: + return None - if self._hierarchy_view: - # If current group is not outdated, check if any - # outdated children. - for _node in walk_hierarchy(item): - if self.outdated(_node): - return self.CHILD_OUTDATED_COLOR - else: + if col != 0: + index = self.index(index.row(), 0, index.parent()) - if self._hierarchy_view: - # Although this is not a group item, we still need - # to distinguish which one contain outdated child. - for _node in walk_hierarchy(item): - if self.outdated(_node): - return self.CHILD_OUTDATED_COLOR.darker(150) - - return self.GRAYOUT_COLOR - - if key == "Name" and not item.get("isGroupNode"): - return self.GRAYOUT_COLOR - - # Add icons - if role == QtCore.Qt.DecorationRole: - if index.column() == 0: - # Override color - color = item.get("color", self._default_icon_color) - if item.get("isGroupNode"): # group-item - return qtawesome.icon("fa.folder", color=color) - if item.get("isNotSet"): - return qtawesome.icon("fa.exclamation-circle", color=color) - - return qtawesome.icon("fa.file-o", color=color) - - if index.column() == 3: - # Product type icon - return item.get("productTypeIcon", None) - - column_name = self.Columns[index.column()] - - if column_name == "group" and item.get("group"): - return qtawesome.icon("fa.object-group", - color=get_default_entity_icon_color()) - - if item.get("isGroupNode"): - if column_name == "active_site": - provider = item.get("active_site_provider") - return self._site_icons.get(provider) - - if column_name == "remote_site": - provider = item.get("remote_site_provider") - return self._site_icons.get(provider) - - if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): - column_name = self.Columns[index.column()] - progress = None - if column_name == "active_site": - progress = item.get("active_site_progress", 0) - elif column_name == "remote_site": - progress = item.get("remote_site_progress", 0) - if progress is not None: - return "{}%".format(max(progress, 0) * 100) - - if role == self.UniqueRole: - return item["representation"] + item.get("objectName", "") - - return super(InventoryModel, self).data(index, role) + return super().data(index, role) def set_hierarchy_view(self, state): """Set whether to display products in hierarchy view.""" @@ -165,299 +335,34 @@ class InventoryModel(TreeModel): if state != self._hierarchy_view: self._hierarchy_view = state - def refresh(self, selected=None, containers=None): - """Refresh the model""" - - # for debugging or testing, injecting items from outside - if containers is None: - containers = self._controller.get_containers() - - self.clear() - if not selected or not self._hierarchy_view: - self._add_containers(containers) - return - - # Filter by cherry-picked items - self._add_containers(( - container - for container in containers - if container["objectName"] in selected - )) - - def _add_containers(self, containers, parent=None): - """Add the items to the model. - - The items should be formatted similar to `api.ls()` returns, an item - is then represented as: - {"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma, - full/filename/of/loaded/filename_v001.ma], - "nodetype" : "reference", - "node": "referenceNode1"} - - Note: When performing an additional call to `add_items` it will *not* - group the new items with previously existing item groups of the - same type. - - Args: - containers (generator): Container items. - parent (Item, optional): Set this item as parent for the added - items when provided. Defaults to the root of the model. - - Returns: - node.Item: root node which has children added based on the data - """ - - project_name = get_current_project_name() - - self.beginResetModel() - - # Group by representation - grouped = defaultdict(lambda: {"containers": list()}) - for container in containers: - repre_id = container["representation"] - grouped[repre_id]["containers"].append(container) - - ( - repres_by_id, - versions_by_id, - products_by_id, - folders_by_id, - ) = self._query_entities(project_name, set(grouped.keys())) - # Add to model - not_found = defaultdict(list) - not_found_ids = [] - for repre_id, group_dict in sorted(grouped.items()): - group_containers = group_dict["containers"] - representation = repres_by_id.get(repre_id) - if not representation: - not_found["representation"].extend(group_containers) - not_found_ids.append(repre_id) + def get_outdated_item_ids(self, ignore_hero=True): + outdated_item_ids = [] + root_item = self.invisibleRootItem() + for row in range(root_item.rowCount()): + group_item = root_item.child(row) + if group_item.data(VERSION_IS_LATEST_ROLE): continue - version_entity = versions_by_id.get(representation["versionId"]) - if not version_entity: - not_found["version"].extend(group_containers) - not_found_ids.append(repre_id) + if ignore_hero and group_item.data(VERSION_IS_HERO_ROLE): continue - product_entity = products_by_id.get(version_entity["productId"]) - if not product_entity: - not_found["product"].extend(group_containers) - not_found_ids.append(repre_id) - continue + for idx in range(group_item.rowCount()): + item = group_item.child(idx) + outdated_item_ids.append(item.data(ITEM_ID_ROLE)) + return outdated_item_ids - folder_entity = folders_by_id.get(product_entity["folderId"]) - if not folder_entity: - not_found["folder"].extend(group_containers) - not_found_ids.append(repre_id) - continue - - group_dict.update({ - "representation": representation, - "version": version_entity, - "product": product_entity, - "folder": folder_entity - }) - - for _repre_id in not_found_ids: - grouped.pop(_repre_id) - - for where, group_containers in not_found.items(): - # create the group header - group_node = Item() - name = "< NOT FOUND - {} >".format(where) - group_node["Name"] = name - group_node["representation"] = name - group_node["count"] = len(group_containers) - group_node["isGroupNode"] = False - group_node["isNotSet"] = True - - self.add_child(group_node, parent=parent) - - for container in group_containers: - item_node = Item() - item_node.update(container) - item_node["Name"] = container.get("objectName", "NO NAME") - item_node["isNotFound"] = True - self.add_child(item_node, parent=group_node) - - # TODO Use product icons - product_type_icon = qtawesome.icon( - "fa.folder", color="#0091B2" - ) - # Prepare site sync specific data - progress_by_id = self._controller.get_representations_site_progress( - set(grouped.keys()) - ) - sites_info = self._controller.get_sites_information() - - # Query the highest available version so the model can know - # whether current version is currently up-to-date. - highest_version_by_product_id = ayon_api.get_last_versions( - project_name, - product_ids={ - group["version"]["productId"] for group in grouped.values() - }, - fields={"productId", "version"} - ) - # Map value to `version` key - highest_version_by_product_id = { - product_id: version["version"] - for product_id, version in highest_version_by_product_id.items() - } - - for repre_id, group_dict in sorted(grouped.items()): - group_containers = group_dict["containers"] - repre_entity = group_dict["representation"] - version_entity = group_dict["version"] - folder_entity = group_dict["folder"] - product_entity = group_dict["product"] - - product_type = product_entity["productType"] - - # create the group header - group_node = Item() - group_node["Name"] = "{}_{}: ({})".format( - folder_entity["name"], - product_entity["name"], - repre_entity["name"] - ) - group_node["representation"] = repre_id - - # Detect hero version type - version = version_entity["version"] - if version < 0: - version = HeroVersionType(version) - group_node["version"] = version - - # Check if the version is outdated. - # Hero versions are never considered to be outdated. - is_outdated = False - if not isinstance(version, HeroVersionType): - last_version = highest_version_by_product_id.get( - version_entity["productId"]) - if last_version is not None: - is_outdated = version_entity["version"] != last_version - group_node["isOutdated"] = is_outdated - - group_node["productType"] = product_type or "" - group_node["productTypeIcon"] = product_type_icon - group_node["count"] = len(group_containers) - group_node["isGroupNode"] = True - group_node["group"] = product_entity["attrib"].get("productGroup") - - # Site sync specific data - progress = progress_by_id[repre_id] - group_node.update(sites_info) - group_node["active_site_progress"] = progress["active_site"] - group_node["remote_site_progress"] = progress["remote_site"] - - self.add_child(group_node, parent=parent) - - for container in group_containers: - item_node = Item() - item_node.update(container) - - # store the current version on the item - item_node["version"] = version_entity["version"] - item_node["version_entity"] = version_entity - - # Remapping namespace to item name. - # Noted that the name key is capital "N", by doing this, we - # can view namespace in GUI without changing container data. - item_node["Name"] = container["namespace"] - - self.add_child(item_node, parent=group_node) - - self.endResetModel() - - return self._root_item - - def _query_entities(self, project_name, repre_ids): - """Query entities for representations from containers. - - Returns: - tuple[dict, dict, dict, dict]: Representation, version, product - and folder documents by id. - """ - - repres_by_id = {} - versions_by_id = {} - products_by_id = {} - folders_by_id = {} - output = ( - repres_by_id, - versions_by_id, - products_by_id, - folders_by_id, - ) - - filtered_repre_ids = set() - for repre_id in repre_ids: - # Filter out invalid representation ids - # NOTE: This is added because scenes from OpenPype did contain - # ObjectId from mongo. - try: - uuid.UUID(repre_id) - filtered_repre_ids.add(repre_id) - except ValueError: - continue - if not filtered_repre_ids: - return output - - repre_entities = ayon_api.get_representations(project_name, repre_ids) - repres_by_id.update({ - repre_entity["id"]: repre_entity - for repre_entity in repre_entities - }) - version_ids = { - repre_entity["versionId"] - for repre_entity in repres_by_id.values() - } - if not version_ids: - return output - - versions_by_id.update({ - version_entity["id"]: version_entity - for version_entity in ayon_api.get_versions( - project_name, version_ids=version_ids - ) - }) - - product_ids = { - version_entity["productId"] - for version_entity in versions_by_id.values() - } - if not product_ids: - return output - - products_by_id.update({ - product_entity["id"]: product_entity - for product_entity in ayon_api.get_products( - project_name, product_ids=product_ids - ) - }) - folder_ids = { - product_entity["folderId"] - for product_entity in products_by_id.values() - } - if not folder_ids: - return output - - folders_by_id.update({ - folder_entity["id"]: folder_entity - for folder_entity in ayon_api.get_folders( - project_name, folder_ids=folder_ids - ) - }) - return output + def _clear_items(self): + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) class FilterProxyModel(QtCore.QSortFilterProxyModel): """Filter model to where key column's value is in the filtered tags""" def __init__(self, *args, **kwargs): - super(FilterProxyModel, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + self.setDynamicSortFilter(True) + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) self._filter_outdated = False self._hierarchy_view = False @@ -467,28 +372,23 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): # Always allow bottom entries (individual containers), since their # parent group hidden if it wouldn't have been validated. - rows = model.rowCount(source_index) - if not rows: + if source_index.data(IS_CONTAINER_ITEM_ROLE): return True - # Filter by regex - if hasattr(self, "filterRegExp"): - regex = self.filterRegExp() - else: - regex = self.filterRegularExpression() - pattern = regex.pattern() - if pattern: - pattern = re.escape(pattern) - - if not self._matches(row, parent, pattern): - return False - if self._filter_outdated: # When filtering to outdated we filter the up to date entries # thus we "allow" them when they are outdated - if not self._is_outdated(row, parent): + if source_index.data(VERSION_IS_LATEST_ROLE): return False + # Filter by regex + if hasattr(self, "filterRegularExpression"): + regex = self.filterRegularExpression() + else: + regex = self.filterRegExp() + + if not self._matches(row, parent, regex.pattern()): + return False return True def set_filter_outdated(self, state): @@ -505,37 +405,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if state != self._hierarchy_view: self._hierarchy_view = state - def _is_outdated(self, row, parent): - """Return whether row is outdated. - - A row is considered outdated if `isOutdated` data is true or not set. - - """ - def outdated(node): - return node.get("isOutdated", True) - - index = self.sourceModel().index(row, self.filterKeyColumn(), parent) - - # The scene contents are grouped by "representation", e.g. the same - # "representation" loaded twice is grouped under the same header. - # Since the version check filters these parent groups we skip that - # check for the individual children. - has_parent = index.parent().isValid() - if has_parent and not self._hierarchy_view: - return True - - # Filter to those that have the different version numbers - node = index.internalPointer() - if outdated(node): - return True - - if self._hierarchy_view: - for _node in walk_hierarchy(node): - if outdated(_node): - return True - - return False - def _matches(self, row, parent, pattern): """Return whether row matches regex pattern. @@ -548,38 +417,31 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): bool """ + if not pattern: + return True + + flags = 0 + if self.sortCaseSensitivity() == QtCore.Qt.CaseInsensitive: + flags = re.IGNORECASE + + regex = re.compile(re.escape(pattern), flags=flags) + model = self.sourceModel() column = self.filterKeyColumn() role = self.filterRole() - def matches(row, parent, pattern): + matches_queue = collections.deque() + matches_queue.append((row, parent)) + while matches_queue: + queue_item = matches_queue.popleft() + row, parent = queue_item + index = model.index(row, column, parent) - key = model.data(index, role) - if re.search(pattern, key, re.IGNORECASE): + value = model.data(index, role) + if regex.search(value): return True - if matches(row, parent, pattern): - return True + for idx in range(model.rowCount(index)): + matches_queue.append((idx, index)) - # Also allow if any of the children matches - source_index = model.index(row, column, parent) - rows = model.rowCount(source_index) - - if any( - matches(idx, source_index, pattern) - for idx in range(rows) - ): - return True - - if not self._hierarchy_view: - return False - - for idx in range(rows): - child_index = model.index(idx, column, source_index) - child_rows = model.rowCount(child_index) - return any( - self._matches(child_idx, child_index, pattern) - for child_idx in range(child_rows) - ) - - return True + return False diff --git a/client/ayon_core/tools/sceneinventory/models/__init__.py b/client/ayon_core/tools/sceneinventory/models/__init__.py index f840a45aa8..28bc7be4d4 100644 --- a/client/ayon_core/tools/sceneinventory/models/__init__.py +++ b/client/ayon_core/tools/sceneinventory/models/__init__.py @@ -1,6 +1,8 @@ +from .containers import ContainersModel from .sitesync import SiteSyncModel __all__ = ( + "ContainersModel", "SiteSyncModel", ) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py new file mode 100644 index 0000000000..95c5322343 --- /dev/null +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -0,0 +1,350 @@ +import uuid +import collections + +import ayon_api +from ayon_api.graphql import GraphQlQuery +from ayon_core.host import ILoadHost + + +# --- Implementation that should be in ayon-python-api --- +# The implementation is not available in all versions of ayon-python-api. +RepresentationHierarchy = collections.namedtuple( + "RepresentationHierarchy", + ("folder", "product", "version", "representation") +) + + +def representations_parent_ids_qraphql_query(): + query = GraphQlQuery("RepresentationsHierarchyQuery") + + project_name_var = query.add_variable("projectName", "String!") + repre_ids_var = query.add_variable("representationIds", "[String!]") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + repres_field = project_field.add_field_with_edges("representations") + repres_field.add_field("id") + repres_field.add_field("name") + repres_field.set_filter("ids", repre_ids_var) + version_field = repres_field.add_field("version") + version_field.add_field("id") + product_field = version_field.add_field("product") + product_field.add_field("id") + product_field.add_field("name") + product_field.add_field("productType") + product_attrib_field = product_field.add_field("attrib") + product_attrib_field.add_field("productGroup") + folder_field = product_field.add_field("folder") + folder_field.add_field("id") + folder_field.add_field("path") + return query + + +def get_representations_hierarchy(project_name, representation_ids): + """Find representations parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + + Returns: + dict[str, RepresentationParents]: Parent entities by + representation id. + + """ + if not representation_ids: + return {} + + repre_ids = set(representation_ids) + output = { + repre_id: RepresentationHierarchy(None, None, None, None) + for repre_id in representation_ids + } + + query = representations_parent_ids_qraphql_query() + query.set_variable_value("projectName", project_name) + query.set_variable_value("representationIds", list(repre_ids)) + + con = ayon_api.get_server_api_connection() + parsed_data = query.query(con) + for repre in parsed_data["project"]["representations"]: + repre_id = repre["id"] + version = repre.pop("version") + product = version.pop("product") + folder = product.pop("folder") + + output[repre_id] = RepresentationHierarchy( + folder, product, version, repre + ) + + return output +# --- END of ayon-python-api implementation --- + + +class ContainerItem: + def __init__( + self, + representation_id, + loader_name, + namespace, + object_name, + item_id + ): + self.representation_id = representation_id + self.loader_name = loader_name + self.object_name = object_name + self.namespace = namespace + self.item_id = item_id + + @classmethod + def from_container_data(cls, container): + return cls( + representation_id=container["representation"], + loader_name=container["loader"], + namespace=container["namespace"], + object_name=container["objectName"], + item_id=uuid.uuid4().hex, + ) + + +class RepresentationInfo: + def __init__( + self, + folder_id, + folder_path, + product_id, + product_name, + product_type, + product_group, + version_id, + representation_name, + ): + self.folder_id = folder_id + self.folder_path = folder_path + self.product_id = product_id + self.product_name = product_name + self.product_type = product_type + self.product_group = product_group + self.version_id = version_id + self.representation_name = representation_name + self._is_valid = None + + @property + def is_valid(self): + if self._is_valid is None: + self._is_valid = ( + self.folder_id is not None + and self.product_id is not None + and self.version_id is not None + and self.representation_name is not None + ) + return self._is_valid + + @classmethod + def new_invalid(cls): + return cls(None, None, None, None, None, None, None, None) + + +class VersionItem: + def __init__(self, version_id, product_id, version, status, is_latest): + self.version = version + self.version_id = version_id + self.product_id = product_id + self.version = version + self.status = status + self.is_latest = is_latest + + @property + def is_hero(self): + return self.version < 0 + + @classmethod + def from_entity(cls, version_entity, is_latest): + return cls( + version_id=version_entity["id"], + product_id=version_entity["productId"], + version=version_entity["version"], + status=version_entity["status"], + is_latest=is_latest, + ) + + +class ContainersModel: + def __init__(self, controller): + self._controller = controller + self._items_cache = None + self._containers_by_id = {} + self._container_items_by_id = {} + self._version_items_by_product_id = {} + self._repre_info_by_id = {} + + def reset(self): + self._items_cache = None + self._containers_by_id = {} + self._container_items_by_id = {} + self._version_items_by_product_id = {} + self._repre_info_by_id = {} + + def get_containers(self): + self._update_cache() + return list(self._containers_by_id.values()) + + def get_containers_by_item_ids(self, item_ids): + return { + item_id: self._containers_by_id.get(item_id) + for item_id in item_ids + } + + def get_container_items(self): + self._update_cache() + return list(self._items_cache) + + def get_container_items_by_id(self, item_ids): + return { + item_id: self._container_items_by_id.get(item_id) + for item_id in item_ids + } + + def get_representation_info_items(self, representation_ids): + output = {} + missing_repre_ids = set() + for repre_id in representation_ids: + try: + uuid.UUID(repre_id) + except ValueError: + output[repre_id] = RepresentationInfo.new_invalid() + continue + + repre_info = self._repre_info_by_id.get(repre_id) + if repre_info is None: + missing_repre_ids.add(repre_id) + else: + output[repre_id] = repre_info + + if not missing_repre_ids: + return output + + project_name = self._controller.get_current_project_name() + repre_hierarchy_by_id = get_representations_hierarchy( + project_name, missing_repre_ids + ) + for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): + kwargs = { + "folder_id": None, + "folder_path": None, + "product_id": None, + "product_name": None, + "product_type": None, + "product_group": None, + "version_id": None, + "representation_name": None, + } + folder = repre_hierarchy.folder + product = repre_hierarchy.product + version = repre_hierarchy.version + repre = repre_hierarchy.representation + if folder: + kwargs["folder_id"] = folder["id"] + kwargs["folder_path"] = folder["path"] + if product: + group = product["attrib"]["productGroup"] + kwargs["product_id"] = product["id"] + kwargs["product_name"] = product["name"] + kwargs["product_type"] = product["productType"] + kwargs["product_group"] = group + if version: + kwargs["version_id"] = version["id"] + if repre: + kwargs["representation_name"] = repre["name"] + + repre_info = RepresentationInfo(**kwargs) + self._repre_info_by_id[repre_id] = repre_info + output[repre_id] = repre_info + return output + + def get_version_items(self, product_ids): + if not product_ids: + return {} + + missing_ids = { + product_id + for product_id in product_ids + if product_id not in self._version_items_by_product_id + } + if missing_ids: + def version_sorted(entity): + return entity["version"] + + project_name = self._controller.get_current_project_name() + version_entities_by_product_id = { + product_id: [] + for product_id in missing_ids + } + + version_entities = list(ayon_api.get_versions( + project_name, + product_ids=missing_ids, + fields={"id", "version", "productId", "status"} + )) + version_entities.sort(key=version_sorted) + for version_entity in version_entities: + product_id = version_entity["productId"] + version_entities_by_product_id[product_id].append( + version_entity + ) + + for product_id, version_entities in ( + version_entities_by_product_id.items() + ): + last_version = abs(version_entities[-1]["version"]) + version_items_by_id = { + entity["id"]: VersionItem.from_entity( + entity, abs(entity["version"]) == last_version + ) + for entity in version_entities + } + self._version_items_by_product_id[product_id] = ( + version_items_by_id + ) + + return { + product_id: dict(self._version_items_by_product_id[product_id]) + for product_id in product_ids + } + + def _update_cache(self): + if self._items_cache is not None: + return + + host = self._controller.get_host() + if isinstance(host, ILoadHost): + containers = list(host.get_containers()) + elif hasattr(host, "ls"): + containers = list(host.ls()) + else: + containers = [] + + container_items = [] + containers_by_id = {} + container_items_by_id = {} + for container in containers: + try: + item = ContainerItem.from_container_data(container) + except Exception as e: + # skip item if required data are missing + self._controller.log_error( + f"Failed to create item: {e}" + ) + continue + + containers_by_id[item.item_id] = container + container_items_by_id[item.item_id] = item + container_items.append(item) + + + self._containers_by_id = containers_by_id + self._container_items_by_id = container_items_by_id + self._items_cache = container_items diff --git a/client/ayon_core/tools/sceneinventory/select_version_dialog.py b/client/ayon_core/tools/sceneinventory/select_version_dialog.py new file mode 100644 index 0000000000..1945d71a6d --- /dev/null +++ b/client/ayon_core/tools/sceneinventory/select_version_dialog.py @@ -0,0 +1,216 @@ +import uuid + +from qtpy import QtWidgets, QtCore, QtGui + +from ayon_core.tools.utils.delegates import StatusDelegate + +from .model import ( + ITEM_ID_ROLE, + STATUS_NAME_ROLE, + STATUS_SHORT_ROLE, + STATUS_COLOR_ROLE, + STATUS_ICON_ROLE, +) + + +class VersionOption: + def __init__( + self, + version, + label, + status_name, + status_short, + status_color + ): + self.version = version + self.label = label + self.status_name = status_name + self.status_short = status_short + self.status_color = status_color + + +class SelectVersionModel(QtGui.QStandardItemModel): + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + index = self.index(index.row(), 0, index.parent()) + return super().data(index, role) + + +class SelectVersionComboBox(QtWidgets.QComboBox): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + combo_model = SelectVersionModel(0, 2) + + self.setModel(combo_model) + + combo_view = QtWidgets.QTreeView(self) + combo_view.setHeaderHidden(True) + combo_view.setIndentation(0) + + self.setView(combo_view) + + header = combo_view.header() + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + + status_delegate = StatusDelegate( + STATUS_NAME_ROLE, + STATUS_SHORT_ROLE, + STATUS_COLOR_ROLE, + STATUS_ICON_ROLE, + ) + combo_view.setItemDelegateForColumn(1, status_delegate) + + self._combo_model = combo_model + self._combo_view = combo_view + self._status_delegate = status_delegate + self._items_by_id = {} + + def paintEvent(self, event): + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionComboBox() + self.initStyleOption(option) + painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option) + idx = self.currentIndex() + status_name = self.itemData(idx, STATUS_NAME_ROLE) + if status_name is None: + painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) + return + + painter.save() + text_field_rect = self.style().subControlRect( + QtWidgets.QStyle.CC_ComboBox, + option, + QtWidgets.QStyle.SC_ComboBoxEditField + ) + adj_rect = text_field_rect.adjusted(1, 0, -1, 0) + painter.drawText( + adj_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + option.currentText + ) + metrics = QtGui.QFontMetrics(self.font()) + text_width = metrics.width(option.currentText) + x_offset = text_width + 2 + diff_width = adj_rect.width() - x_offset + if diff_width <= 0: + return + + status_rect = adj_rect.adjusted(x_offset + 2, 0, 0, 0) + if diff_width < metrics.width(status_name): + status_name = self.itemData(idx, STATUS_SHORT_ROLE) + + color = QtGui.QColor(self.itemData(idx, STATUS_COLOR_ROLE)) + + pen = painter.pen() + pen.setColor(color) + painter.setPen(pen) + painter.drawText( + status_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + status_name + ) + + def set_current_index(self, index): + model = self._combo_view.model() + if index > model.rowCount(): + return + + self.setCurrentIndex(index) + + def get_item_by_id(self, item_id): + return self._items_by_id[item_id] + + def set_versions(self, version_options): + self._items_by_id = {} + model = self._combo_model + root_item = model.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + + new_items = [] + for version_option in version_options: + item_id = uuid.uuid4().hex + item = QtGui.QStandardItem(version_option.label) + item.setColumnCount(root_item.columnCount()) + item.setData( + version_option.status_name, STATUS_NAME_ROLE + ) + item.setData( + version_option.status_short, STATUS_SHORT_ROLE + ) + item.setData( + version_option.status_color, STATUS_COLOR_ROLE + ) + item.setData(item_id, ITEM_ID_ROLE) + + new_items.append(item) + self._items_by_id[item_id] = version_option + + if new_items: + root_item.appendRows(new_items) + + +class SelectVersionDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.setWindowTitle("Select version") + + label_widget = QtWidgets.QLabel("Set version number to", self) + versions_combobox = SelectVersionComboBox(self) + + btns_widget = QtWidgets.QWidget(self) + + confirm_btn = QtWidgets.QPushButton("OK", btns_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + btns_layout.addWidget(cancel_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(label_widget, 0) + main_layout.addWidget(versions_combobox, 0) + main_layout.addWidget(btns_widget, 0) + + confirm_btn.clicked.connect(self._on_confirm) + cancel_btn.clicked.connect(self._on_cancel) + + self._selected_item = None + self._cancelled = False + self._versions_combobox = versions_combobox + + def get_selected_item(self): + if self._cancelled: + return None + return self._selected_item + + def set_versions(self, version_options): + self._versions_combobox.set_versions(version_options) + + def select_index(self, index): + self._versions_combobox.set_current_index(index) + + @classmethod + def ask_for_version(cls, version_options, index=None, parent=None): + dialog = cls(parent) + dialog.set_versions(version_options) + if index is not None: + dialog.select_index(index) + dialog.exec_() + return dialog.get_selected_item() + + def _on_confirm(self): + self._cancelled = False + index = self._versions_combobox.currentIndex() + item_id = self._versions_combobox.itemData(index, ITEM_ID_ROLE) + self._selected_item = self._versions_combobox.get_item_by_id(item_id) + self.accept() + + def _on_cancel(self): + self._cancelled = True + self.reject() diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 5cbd4daf70..770d0d903d 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1,10 +1,7 @@ -import uuid import collections import logging -import itertools from functools import partial -import ayon_api from qtpy import QtWidgets, QtCore import qtawesome @@ -17,12 +14,27 @@ from ayon_core.pipeline import ( ) from ayon_core.tools.utils.lib import ( iter_model_rows, - format_version + format_version, + preserve_expanded_rows, + preserve_selection, ) +from ayon_core.tools.utils.delegates import StatusDelegate from .switch_dialog import SwitchAssetDialog -from .model import InventoryModel - +from .model import ( + InventoryModel, + FilterProxyModel, + ITEM_UNIQUE_NAME_ROLE, + OBJECT_NAME_ROLE, + ITEM_ID_ROLE, + IS_CONTAINER_ITEM_ROLE, + STATUS_NAME_ROLE, + STATUS_SHORT_ROLE, + STATUS_COLOR_ROLE, + STATUS_ICON_ROLE, +) +from .delegates import VersionDelegate +from .select_version_dialog import SelectVersionDialog, VersionOption DEFAULT_COLOR = "#fb9c15" @@ -43,185 +55,199 @@ class SceneInventoryView(QtWidgets.QTreeView): self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + model = InventoryModel(controller) + proxy_model = FilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + self.setModel(proxy_model) + + version_delegate = VersionDelegate() + status_delegate = StatusDelegate( + STATUS_NAME_ROLE, + STATUS_SHORT_ROLE, + STATUS_COLOR_ROLE, + STATUS_ICON_ROLE, + ) + for col, delegate in ( + (model.version_col, version_delegate), + (model.status_col, status_delegate), + ): + self.setItemDelegateForColumn(col, delegate) + + # set some nice default widths for the view + for col, width in model.width_by_column.items(): + self.setColumnWidth(col, width) + + sync_enabled = controller.is_sitesync_enabled() + self.setColumnHidden(model.active_site_col, not sync_enabled) + self.setColumnHidden(model.remote_site_col, not sync_enabled) + self.customContextMenuRequested.connect(self._show_right_mouse_menu) + self._model = model + self._proxy_model = proxy_model + self._version_delegate = version_delegate + self._status_delegate = status_delegate + self._hierarchy_view = False self._selected = None self._controller = controller + def refresh(self): + with preserve_expanded_rows( + tree_view=self, + role=ITEM_UNIQUE_NAME_ROLE + ): + with preserve_selection( + tree_view=self, + role=ITEM_UNIQUE_NAME_ROLE, + current_index=False + ): + kwargs = {} + # TODO do not touch view's inner attribute + if self._hierarchy_view: + kwargs["selected"] = self._selected + self._model.refresh(**kwargs) + + def set_hierarchy_view(self, enabled): + self._proxy_model.set_hierarchy_view(enabled) + self._model.set_hierarchy_view(enabled) + + def set_text_filter(self, text_filter): + if hasattr(self._proxy_model, "setFilterRegularExpression"): + self._proxy_model.setFilterRegularExpression(text_filter) + else: + self._proxy_model.setFilterRegExp(text_filter) + + def set_filter_outdated(self, enabled): + self._proxy_model.set_filter_outdated(enabled) + + def get_selected_indexes(self): + """Get the selected rows""" + indexes, _ = self._get_selected_indexes() + return indexes + + def get_selected_item_ids(self): + return self._get_item_ids_from_indexes( + self.get_selected_indexes() + ) + + def get_selected_container_indexes(self): + return self._get_container_indexes( + self.get_selected_indexes() + ) + + def _get_selected_indexes(self): + selection_model = self.selectionModel() + indexes = selection_model.selectedRows() + active = self.currentIndex() + active = active.sibling(active.row(), 0) + if active not in indexes: + indexes.append(active) + return indexes, active + + def _get_item_ids_from_indexes(self, indexes): + return { + index.data(ITEM_ID_ROLE) + for index in self._get_container_indexes(indexes) + } + def _set_hierarchy_view(self, enabled): if enabled == self._hierarchy_view: return self._hierarchy_view = enabled self.hierarchy_view_changed.emit(enabled) - def _enter_hierarchy(self, items): - self._selected = set(i["objectName"] for i in items) + def _enter_hierarchy(self, item_ids): + self._selected = set(item_ids) self._set_hierarchy_view(True) self.data_changed.emit() self.expandToDepth(1) - self.setStyleSheet(""" - QTreeView { - border-color: #fb9c15; - } - """) + self.setStyleSheet("border-color: #fb9c15;") def _leave_hierarchy(self): self._set_hierarchy_view(False) self.data_changed.emit() - self.setStyleSheet("QTreeView {}") + self.setStyleSheet("") - def _build_item_menu_for_selection(self, items, menu): - # Exclude items that are "NOT FOUND" since setting versions, updating - # and removal won't work for those items. - items = [item for item in items if not item.get("isNotFound")] - if not items: + def _build_item_menu_for_selection(self, menu, indexes, active_index): + item_ids = { + index.data(ITEM_ID_ROLE) + for index in indexes + } + item_ids.discard(None) + if not item_ids: return - # An item might not have a representation, for example when an item - # is listed as "NOT FOUND" - repre_ids = set() - for item in items: - repre_id = item["representation"] - try: - uuid.UUID(repre_id) - repre_ids.add(repre_id) - except ValueError: - pass - - project_name = self._controller.get_current_project_name() - repre_entities = ayon_api.get_representations( - project_name, - representation_ids=repre_ids, - fields={"versionId"} + container_items_by_id = self._controller.get_container_items_by_id( + item_ids ) - version_ids = { - repre_entity["versionId"] - for repre_entity in repre_entities + active_repre_id = None + if active_index is not None: + for index in self._get_container_indexes({active_index}): + item_id = index.data(ITEM_ID_ROLE) + container_item = container_items_by_id[item_id] + active_repre_id = container_item.representation_id + break + + repre_info_by_id = self._controller.get_representation_info_items({ + container_item.representation_id + for container_item in container_items_by_id.values() + }) + valid_repre_ids = { + repre_id + for repre_id, repre_info in repre_info_by_id.items() + if repre_info.is_valid } - loaded_versions = ayon_api.get_versions( - project_name, version_ids=version_ids - ) - - loaded_hero_versions = [] - versions_by_product_id = collections.defaultdict(list) + # Exclude items that are "NOT FOUND" since setting versions, updating + # and removal won't work for those items. + filtered_items = [] product_ids = set() - for version_entity in loaded_versions: - version = version_entity["version"] - if version < 0: - loaded_hero_versions.append(version_entity) - else: - product_id = version_entity["productId"] - versions_by_product_id[product_id].append(version_entity) - product_ids.add(product_id) + version_ids = set() + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + repre_info = repre_info_by_id.get(repre_id) + if repre_info and repre_info.is_valid: + filtered_items.append(container_item) + version_ids.add(repre_info.version_id) + product_ids.add(repre_info.product_id) - all_versions = ayon_api.get_versions( - project_name, product_ids=product_ids + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self._show_remove_warning_dialog(item_ids)) + + if not filtered_items: + # Keep remove action for invalid items + menu.addAction(remove_action) + return + + version_items_by_product_id = self._controller.get_version_items( + product_ids ) - hero_versions = [] - version_entities = [] - for version_entity in all_versions: - version = version_entity["version"] - if version < 0: - hero_versions.append(version_entity) - else: - version_entities.append(version_entity) - - has_loaded_hero_versions = len(loaded_hero_versions) > 0 - has_available_hero_version = len(hero_versions) > 0 has_outdated = False + has_loaded_hero_versions = False + has_available_hero_version = False + for version_items_by_id in version_items_by_product_id.values(): + for version_item in version_items_by_id.values(): + if version_item.is_hero: + has_available_hero_version = True - for version_entity in version_entities: - product_id = version_entity["productId"] - current_versions = versions_by_product_id[product_id] - for current_version in current_versions: - if current_version["version"] < version_entity["version"]: + if version_item.version_id not in version_ids: + continue + if version_item.is_hero: + has_loaded_hero_versions = True + + elif not version_item.is_latest: has_outdated = True - break - - if has_outdated: - break switch_to_versioned = None if has_loaded_hero_versions: - def _on_switch_to_versioned(items): - repre_ids = { - item["representation"] - for item in items - } - - repre_entities = ayon_api.get_representations( - project_name, - representation_ids=repre_ids, - fields={"id", "versionId"} - ) - - version_id_by_repre_id = {} - for repre_entity in repre_entities: - repre_id = repre_entity["id"] - version_id = repre_entity["versionId"] - version_id_by_repre_id[repre_id] = version_id - version_ids = set(version_id_by_repre_id.values()) - - src_version_entity_by_id = { - version_entity["id"]: version_entity - for version_entity in ayon_api.get_versions( - project_name, - version_ids, - fields={"productId", "version"} - ) - } - hero_versions_by_product_id = {} - for version_entity in src_version_entity_by_id.values(): - version = version_entity["version"] - if version < 0: - product_id = version_entity["productId"] - hero_versions_by_product_id[product_id] = abs(version) - - if not hero_versions_by_product_id: - return - - standard_versions = ayon_api.get_versions( - project_name, - product_ids=hero_versions_by_product_id.keys(), - versions=hero_versions_by_product_id.values() - ) - standard_version_by_product_id = { - product_id: {} - for product_id in hero_versions_by_product_id.keys() - } - for version_entity in standard_versions: - product_id = version_entity["productId"] - version = version_entity["version"] - standard_version_by_product_id[product_id][version] = ( - version_entity - ) - - # Specify version per item to update to - update_items = [] - update_versions = [] - for item in items: - repre_id = item["representation"] - version_id = version_id_by_repre_id.get(repre_id) - version_entity = src_version_entity_by_id.get(version_id) - if not version_entity or version_entity["version"] >= 0: - continue - product_id = version_entity["productId"] - version_entities_by_version = ( - standard_version_by_product_id[product_id] - ) - new_version = hero_versions_by_product_id.get(product_id) - new_version_entity = version_entities_by_version.get( - new_version - ) - if new_version_entity is not None: - update_items.append(item) - update_versions.append(new_version) - self._update_containers(update_items, update_versions) - update_icon = qtawesome.icon( "fa.asterisk", color=DEFAULT_COLOR @@ -232,7 +258,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) switch_to_versioned.triggered.connect( - lambda: _on_switch_to_versioned(items) + lambda: self._on_switch_to_versioned(item_ids) ) update_to_latest_action = None @@ -247,7 +273,9 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) update_to_latest_action.triggered.connect( - lambda: self._update_containers(items, version=-1) + lambda: self._update_containers_to_version( + item_ids, version=-1 + ) ) change_to_hero = None @@ -263,20 +291,23 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) change_to_hero.triggered.connect( - lambda: self._update_containers( - items, version=HeroVersionType(-1) + lambda: self._update_containers_to_version( + item_ids, version=HeroVersionType(-1) ) ) # set version - set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) - set_version_action = QtWidgets.QAction( - set_version_icon, - "Set version", - menu - ) - set_version_action.triggered.connect( - lambda: self._show_version_dialog(items)) + set_version_action = None + if active_repre_id is not None: + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self._show_version_dialog(item_ids, active_repre_id) + ) # switch folder switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) @@ -286,13 +317,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) switch_folder_action.triggered.connect( - lambda: self._show_switch_dialog(items)) - - # remove - remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) - remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) - remove_action.triggered.connect( - lambda: self._show_remove_warning_dialog(items)) + lambda: self._show_switch_dialog(item_ids)) # add the actions if switch_to_versioned: @@ -304,14 +329,15 @@ class SceneInventoryView(QtWidgets.QTreeView): if change_to_hero: menu.addAction(change_to_hero) - menu.addAction(set_version_action) + if set_version_action is not None: + menu.addAction(set_version_action) menu.addAction(switch_folder_action) menu.addSeparator() menu.addAction(remove_action) - self._handle_sitesync(menu, repre_ids) + self._handle_sitesync(menu, valid_repre_ids) def _handle_sitesync(self, menu, repre_ids): """Adds actions for download/upload when SyncServer is enabled @@ -327,6 +353,9 @@ class SceneInventoryView(QtWidgets.QTreeView): if not self._controller.is_sitesync_enabled(): return + if not repre_ids: + return + menu.addSeparator() download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) @@ -365,30 +394,35 @@ class SceneInventoryView(QtWidgets.QTreeView): self.data_changed.emit() - def _build_item_menu(self, items=None): + def _build_item_menu(self, indexes=None, active_index=None): """Create menu for the selected items""" - - if not items: - items = [] - menu = QtWidgets.QMenu(self) - # add the actions - self._build_item_menu_for_selection(items, menu) - # These two actions should be able to work without selection # expand all items - expandall_action = QtWidgets.QAction(menu, text="Expand all items") - expandall_action.triggered.connect(self.expandAll) + expand_all_action = QtWidgets.QAction(menu, text="Expand all items") + expand_all_action.triggered.connect(self.expandAll) # collapse all items collapse_action = QtWidgets.QAction(menu, text="Collapse all items") collapse_action.triggered.connect(self.collapseAll) - menu.addAction(expandall_action) + if not indexes: + indexes = [] + + item_ids = { + index.data(ITEM_ID_ROLE) + for index in indexes + } + item_ids.discard(None) + + # add the actions + self._build_item_menu_for_selection(menu, indexes, active_index) + + menu.addAction(expand_all_action) menu.addAction(collapse_action) - custom_actions = self._get_custom_actions(containers=items) + custom_actions = self._get_custom_actions(item_ids) if custom_actions: submenu = QtWidgets.QMenu("Actions", self) for action in custom_actions: @@ -396,7 +430,10 @@ class SceneInventoryView(QtWidgets.QTreeView): icon = qtawesome.icon("fa.%s" % action.icon, color=color) action_item = QtWidgets.QAction(icon, action.label, submenu) action_item.triggered.connect( - partial(self._process_custom_action, action, items)) + partial( + self._process_custom_action, action, item_ids + ) + ) submenu.addAction(action_item) @@ -421,9 +458,9 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) enter_hierarchy_action.triggered.connect( - lambda: self._enter_hierarchy(items)) + lambda: self._enter_hierarchy(item_ids)) - if items: + if indexes: menu.addAction(enter_hierarchy_action) if back_to_flat_action is not None: @@ -431,11 +468,11 @@ class SceneInventoryView(QtWidgets.QTreeView): return menu - def _get_custom_actions(self, containers): + def _get_custom_actions(self, item_ids): """Get the registered Inventory Actions Args: - containers(list): collection of containers + item_ids (Iterable[str]): collection of containers Returns: list: collection of filter and initialized actions @@ -448,29 +485,40 @@ class SceneInventoryView(QtWidgets.QTreeView): # Fedd an empty dict if no selection, this will ensure the compat # lookup always work, so plugin can interact with Scene Inventory # reversely. - containers = containers or [dict()] + if not item_ids: + containers = [dict()] + else: + containers_by_id = self._controller.get_containers_by_item_ids( + item_ids + ) + containers = list(containers_by_id.values()) # Check which action will be available in the menu Plugins = discover_inventory_actions() - compatible = [p() for p in Plugins if - any(p.is_compatible(c) for c in containers)] + compatible = [ + p() + for p in Plugins + if any(p.is_compatible(c) for c in containers) + ] return sorted(compatible, key=sorter) - def _process_custom_action(self, action, containers): + def _process_custom_action(self, action, item_ids): """Run action and if results are returned positive update the view If the result is list or dict, will select view items by the result. Args: action (InventoryAction): Inventory Action instance - containers (list): Data of currently selected items + item_ids (Iterable[str]): Data of currently selected items Returns: None """ - - result = action.process(containers) + containers_by_id = self._controller.get_containers_by_item_ids( + item_ids + ) + result = action.process(list(containers_by_id.values())) if result: self.data_changed.emit() @@ -498,17 +546,24 @@ class SceneInventoryView(QtWidgets.QTreeView): if options.get("clear", True): self.clearSelection() - object_names = set(object_names) - if ( - self._hierarchy_view - and not self._selected.issuperset(object_names) - ): - # If any container not in current cherry-picked view, update - # view before selecting them. - self._selected.update(object_names) - self.data_changed.emit() - model = self.model() + object_names = set(object_names) + if self._hierarchy_view: + item_ids = set() + for index in iter_model_rows(model): + if not index.data(IS_CONTAINER_ITEM_ROLE): + continue + if index.data(OBJECT_NAME_ROLE) in object_names: + item_id = index.data(ITEM_ID_ROLE) + if item_id: + item_ids.add(item_id) + + if not self._selected.issuperset(item_ids): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(item_ids) + self.data_changed.emit() + selection_model = self.selectionModel() select_mode = { @@ -517,12 +572,10 @@ class SceneInventoryView(QtWidgets.QTreeView): "toggle": QtCore.QItemSelectionModel.Toggle, }[options.get("mode", "select")] - for index in iter_model_rows(model, 0): - item = index.data(InventoryModel.ItemRole) - if item.get("isGroupNode"): + for index in iter_model_rows(model): + if not index.data(IS_CONTAINER_ITEM_ROLE): continue - - name = item.get("objectName") + name = index.data(OBJECT_NAME_ROLE) if name in object_names: self.scrollTo(index) # Ensure item is visible flags = select_mode | QtCore.QItemSelectionModel.Rows @@ -539,177 +592,194 @@ class SceneInventoryView(QtWidgets.QTreeView): globalpos = self.viewport().mapToGlobal(pos) if not self.selectionModel().hasSelection(): - print("No selection") # Build menu without selection, feed an empty list menu = self._build_item_menu() menu.exec_(globalpos) return - active = self.currentIndex() # index under mouse - active = active.sibling(active.row(), 0) # get first column - - # move index under mouse - indices = self.get_indices() - if active in indices: - indices.remove(active) - - indices.append(active) + indexes, active_index = self._get_selected_indexes() # Extend to the sub-items - all_indices = self._extend_to_children(indices) - items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices - if i.parent().isValid()] + all_indexes = self._extend_to_children(indexes) - if self._hierarchy_view: - # Ensure no group item - items = [n for n in items if not n.get("isGroupNode")] - - menu = self._build_item_menu(items) + menu = self._build_item_menu(all_indexes, active_index) menu.exec_(globalpos) - def get_indices(self): - """Get the selected rows""" - selection_model = self.selectionModel() - return selection_model.selectedRows() + def _get_container_indexes(self, indexes): + container_indexes = [] + indexes_queue = collections.deque() + indexes_queue.extend(indexes) + # Ignore already added containers + items_ids = set() + while indexes_queue: + index = indexes_queue.popleft() + if index.data(IS_CONTAINER_ITEM_ROLE): + item_id = index.data(ITEM_ID_ROLE) + if item_id in items_ids: + continue + items_ids.add(item_id) + container_indexes.append(index) + continue + model = index.model() + for row in range(model.rowCount(index)): + child = model.index(row, 0, parent=index) + indexes_queue.append(child) + return container_indexes - def _extend_to_children(self, indices): + def _extend_to_children(self, indexes): """Extend the indices to the children indices. Top-level indices are extended to its children indices. Sub-items are kept as is. Args: - indices (list): The indices to extend. + indexes (list): The indices to extend. Returns: list: The children indices """ - def get_children(i): - model = i.model() - rows = model.rowCount(parent=i) - for row in range(rows): - child = model.index(row, 0, parent=i) - yield child + def get_children(index): + model = index.model() + for row in range(model.rowCount(index)): + yield model.index(row, 0, parent=index) subitems = set() - for i in indices: - valid_parent = i.parent().isValid() - if valid_parent and i not in subitems: - subitems.add(i) + for index in indexes: + if index.parent().isValid() and index not in subitems: + subitems.add(index) if self._hierarchy_view: # Assume this is a group item - for child in get_children(i): + for child in get_children(index): subitems.add(child) else: # is top level item - for child in get_children(i): + for child in get_children(index): subitems.add(child) return list(subitems) - def _show_version_dialog(self, items): + def _show_version_dialog(self, item_ids, active_repre_id): """Create a dialog with the available versions for the selected file Args: - items (list): list of items to run the "set_version" for + item_ids (Iterable[str]): List of item ids to run the + "set_version" for. + active_repre_id (Union[str, None]): Active representation id. Returns: None + """ - - active = items[-1] - - project_name = self._controller.get_current_project_name() - # Get available versions for active representation - repre_entity = ayon_api.get_representation_by_id( - project_name, - active["representation"], - fields={"versionId"} + container_items_by_id = self._controller.get_container_items_by_id( + item_ids + ) + repre_ids = { + container_item.representation_id + for container_item in container_items_by_id.values() + } + repre_info_by_id = self._controller.get_representation_info_items( + repre_ids ) - repre_version_entity = ayon_api.get_version_by_id( - project_name, - repre_entity["versionId"], - fields={"productId"} + active_repre_info = repre_info_by_id[active_repre_id] + active_product_id = active_repre_info.product_id + active_version_id = active_repre_info.version_id + filtered_repre_info_by_id = { + repre_id: repre_info + for repre_id, repre_info in repre_info_by_id.items() + if repre_info.product_id == active_product_id + } + filtered_container_item_ids = { + item_id + for item_id, container_item in container_items_by_id.items() + if container_item.representation_id in filtered_repre_info_by_id + } + version_items_by_id = self._controller.get_version_items( + {active_product_id} + )[active_product_id] + + def version_sorter(item): + hero_value = 0 + version = item.version + if version < 0: + hero_value = 1 + version = abs(version) + return version, hero_value + + version_items = list(version_items_by_id.values()) + version_items.sort(key=version_sorter, reverse=True) + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items() + } + + version_options = [] + active_version_idx = 0 + for idx, version_item in enumerate(version_items): + version = version_item.version + label = format_version(version) + if version_item.version_id == active_version_id: + active_version_idx = idx + + status_name = version_item.status + status_short = None + status_color = None + status_item = status_items_by_name.get(status_name) + if status_item: + status_short = status_item.short + status_color = status_item.color + version_options.append( + VersionOption( + version, + label, + status_name, + status_short, + status_color, + ) + ) + + version_option = SelectVersionDialog.ask_for_version( + version_options, + active_version_idx, + parent=self ) - - version_entities = list(ayon_api.get_versions( - project_name, - product_ids={repre_version_entity["productId"]}, - )) - hero_version = None - standard_versions = [] - for version_entity in version_entities: - if version_entity["version"] < 0: - hero_version = version_entity - else: - standard_versions.append(version_entity) - standard_versions.sort(key=lambda item: item["version"]) - standard_versions.reverse() - - # Get index among the listed versions - current_item = None - current_version = active["version"] - if isinstance(current_version, HeroVersionType): - current_item = hero_version - else: - for version_entity in standard_versions: - if version_entity["version"] == current_version: - current_item = version_entity - break - - all_versions = [] - if hero_version: - all_versions.append(hero_version) - all_versions.extend(standard_versions) - - if current_item: - index = all_versions.index(current_item) - else: - index = 0 - - versions_by_label = dict() - labels = [] - for version_entity in all_versions: - label = format_version(version_entity["version"]) - labels.append(label) - versions_by_label[label] = version_entity["version"] - - label, state = QtWidgets.QInputDialog.getItem( - self, - "Set version..", - "Set version number to", - labels, - current=index, - editable=False - ) - if not state: + if version_option is None: return - if label: - version = versions_by_label[label] - if version < 0: - version = HeroVersionType(version) - self._update_containers(items, version) + version = version_option.version + if version < 0: + version = HeroVersionType(version) - def _show_switch_dialog(self, items): + self._update_containers_to_version( + filtered_container_item_ids, version + ) + + def _show_switch_dialog(self, item_ids): """Display Switch dialog""" - dialog = SwitchAssetDialog(self._controller, self, items) + containers_by_id = self._controller.get_containers_by_item_ids( + item_ids + ) + dialog = SwitchAssetDialog( + self._controller, self, list(containers_by_id.values()) + ) dialog.switched.connect(self.data_changed.emit) dialog.show() - def _show_remove_warning_dialog(self, items): + def _show_remove_warning_dialog(self, item_ids): """Prompt a dialog to inform the user the action will remove items""" - + containers_by_id = self._controller.get_containers_by_item_ids( + item_ids + ) + containers = list(containers_by_id.values()) accept = QtWidgets.QMessageBox.Ok buttons = accept | QtWidgets.QMessageBox.Cancel state = QtWidgets.QMessageBox.question( self, "Are you sure?", - "Are you sure you want to remove {} item(s)".format(len(items)), + f"Are you sure you want to remove {len(containers)} item(s)", buttons=buttons, defaultButton=accept ) @@ -717,15 +787,15 @@ class SceneInventoryView(QtWidgets.QTreeView): if state != accept: return - for item in items: - remove_container(item) + for container in containers: + remove_container(container) self.data_changed.emit() - def _show_version_error_dialog(self, version, items): + def _show_version_error_dialog(self, version, item_ids): """Shows QMessageBox when version switch doesn't work - Args: - version: str or int or None + Args: + version: str or int or None """ if version == -1: version_str = "latest" @@ -745,7 +815,7 @@ class SceneInventoryView(QtWidgets.QTreeView): "Switch Folder", QtWidgets.QMessageBox.ActionRole ) - switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) + switch_btn.clicked.connect(lambda: self._show_switch_dialog(item_ids)) dialog.addButton(QtWidgets.QMessageBox.Cancel) @@ -760,69 +830,115 @@ class SceneInventoryView(QtWidgets.QTreeView): def update_all(self): """Update all items that are currently 'outdated' in the view""" # Get the source model through the proxy model - model = self.model().sourceModel() - - # Get all items from outdated groups - outdated_items = [] - for index in iter_model_rows(model, - column=0, - include_root=False): - item = index.data(model.ItemRole) - - if not item.get("isGroupNode"): - continue - - # Only the group nodes contain the "highest_version" data and as - # such we find only the groups and take its children. - if not model.outdated(item): - continue - - # Collect all children which we want to update - children = item.children() - outdated_items.extend(children) - - if not outdated_items: + item_ids = self._model.get_outdated_item_ids() + if not item_ids: log.info("Nothing to update.") return # Trigger update to latest - self._update_containers(outdated_items, version=-1) + self._update_containers_to_version(item_ids, version=-1) - def _update_containers(self, items, version): + def _on_switch_to_versioned(self, item_ids): + containers_items_by_id = self._controller.get_container_items_by_id( + item_ids + ) + repre_ids = { + container_item.representation_id + for container_item in containers_items_by_id.values() + } + repre_info_by_id = self._controller.get_representation_info_items( + repre_ids + ) + product_ids = { + repre_info.product_id + for repre_info in repre_info_by_id.values() + if repre_info.is_valid + } + version_items_by_product_id = self._controller.get_version_items( + product_ids + ) + + update_containers = [] + update_versions = [] + for item_id, container_item in containers_items_by_id.items(): + repre_id = container_item.representation_id + repre_info = repre_info_by_id[repre_id] + product_id = repre_info.product_id + version_items_id = version_items_by_product_id[product_id] + version_item = version_items_id.get(repre_info.version_id, {}) + if not version_item or not version_item.is_hero: + continue + version = abs(version_item.version) + version_found = False + for version_item in version_items_id.values(): + if version_item.is_hero: + continue + if version_item.version == version: + version_found = True + break + + if not version_found: + continue + + update_containers.append(container_item.item_id) + update_versions.append(version) + + # Specify version per item to update to + self._update_containers(update_containers, update_versions) + + def _update_containers(self, item_ids, versions): """Helper to update items to given version (or version per item) If at least one item is specified this will always try to refresh the inventory even if errors occurred on any of the items. Arguments: - items (list): Items to update - version (int or list): Version to set to. + item_ids (Iterable[str]): Items to update + versions (Iterable[Union[int, HeroVersion]]): Version to set to. This can be a list specifying a version for each item. Like `update_container` version -1 sets the latest version and HeroTypeVersion instances set the hero version. """ - if isinstance(version, (list, tuple)): - # We allow a unique version to be specified per item. In that case - # the length must match with the items - assert len(items) == len(version), ( - "Number of items mismatches number of versions: " - "{} items - {} versions".format(len(items), len(version)) - ) - versions = version - else: - # Repeat the same version infinitely - versions = itertools.repeat(version) + # We allow a unique version to be specified per item. In that case + # the length must match with the items + assert len(item_ids) == len(versions), ( + "Number of items mismatches number of versions: " + f"{len(item_ids)} items - {len(versions)} versions" + ) # Trigger update to latest + containers_by_id = self._controller.get_containers_by_item_ids( + item_ids + ) try: - for item, item_version in zip(items, versions): + for item_id, item_version in zip(item_ids, versions): + container = containers_by_id[item_id] try: - update_container(item, item_version) + update_container(container, item_version) except AssertionError: - self._show_version_error_dialog(item_version, [item]) log.warning("Update failed", exc_info=True) + self._show_version_error_dialog( + item_version, [item_id] + ) finally: # Always update the scene inventory view, even if errors occurred self.data_changed.emit() + + def _update_containers_to_version(self, item_ids, version): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + item_ids (Iterable[str]): Items to update + version (Union[int, HeroVersion]): Version to set to. + This can be a list specifying a version for each item. + Like `update_container` version -1 sets the latest version + and HeroTypeVersion instances set the hero version. + + """ + versions = [version for _ in range(len(item_ids))] + self._update_containers(item_ids, versions) diff --git a/client/ayon_core/tools/sceneinventory/window.py b/client/ayon_core/tools/sceneinventory/window.py index 555db3a17c..58ff0c3b6d 100644 --- a/client/ayon_core/tools/sceneinventory/window.py +++ b/client/ayon_core/tools/sceneinventory/window.py @@ -2,17 +2,10 @@ from qtpy import QtWidgets, QtCore, QtGui import qtawesome from ayon_core import style, resources -from ayon_core.tools.utils.lib import ( - preserve_expanded_rows, - preserve_selection, -) +from ayon_core.tools.utils import PlaceholderLineEdit + from ayon_core.tools.sceneinventory import SceneInventoryController -from .delegates import VersionDelegate -from .model import ( - InventoryModel, - FilterProxyModel -) from .view import SceneInventoryView @@ -20,7 +13,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" def __init__(self, controller=None, parent=None): - super(SceneInventoryWindow, self).__init__(parent) + super().__init__(parent) if controller is None: controller = SceneInventoryController() @@ -33,10 +26,9 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.resize(1100, 480) - # region control - filter_label = QtWidgets.QLabel("Search", self) - text_filter = QtWidgets.QLineEdit(self) + text_filter = PlaceholderLineEdit(self) + text_filter.setPlaceholderText("Filter by name...") outdated_only_checkbox = QtWidgets.QCheckBox( "Filter to outdated", self @@ -44,52 +36,30 @@ class SceneInventoryWindow(QtWidgets.QDialog): outdated_only_checkbox.setToolTip("Show outdated files only") outdated_only_checkbox.setChecked(False) - icon = qtawesome.icon("fa.arrow-up", color="white") + update_all_icon = qtawesome.icon("fa.arrow-up", color="white") update_all_button = QtWidgets.QPushButton(self) update_all_button.setToolTip("Update all outdated to latest version") - update_all_button.setIcon(icon) + update_all_button.setIcon(update_all_icon) - icon = qtawesome.icon("fa.refresh", color="white") + refresh_icon = qtawesome.icon("fa.refresh", color="white") refresh_button = QtWidgets.QPushButton(self) refresh_button.setToolTip("Refresh") - refresh_button.setIcon(icon) + refresh_button.setIcon(refresh_icon) - control_layout = QtWidgets.QHBoxLayout() - control_layout.addWidget(filter_label) - control_layout.addWidget(text_filter) - control_layout.addWidget(outdated_only_checkbox) - control_layout.addWidget(update_all_button) - control_layout.addWidget(refresh_button) - - model = InventoryModel(controller) - proxy = FilterProxyModel() - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + headers_widget = QtWidgets.QWidget(self) + headers_layout = QtWidgets.QHBoxLayout(headers_widget) + headers_layout.setContentsMargins(0, 0, 0, 0) + headers_layout.addWidget(filter_label, 0) + headers_layout.addWidget(text_filter, 1) + headers_layout.addWidget(outdated_only_checkbox, 0) + headers_layout.addWidget(update_all_button, 0) + headers_layout.addWidget(refresh_button, 0) view = SceneInventoryView(controller, self) - view.setModel(proxy) - sync_enabled = controller.is_sitesync_enabled() - view.setColumnHidden(model.active_site_col, not sync_enabled) - view.setColumnHidden(model.remote_site_col, not sync_enabled) - - # set some nice default widths for the view - view.setColumnWidth(0, 250) # name - view.setColumnWidth(1, 55) # version - view.setColumnWidth(2, 55) # count - view.setColumnWidth(3, 150) # product type - view.setColumnWidth(4, 120) # group - view.setColumnWidth(5, 150) # loader - - # apply delegates - version_delegate = VersionDelegate(controller, self) - column = model.Columns.index("version") - view.setItemDelegateForColumn(column, version_delegate) - - layout = QtWidgets.QVBoxLayout(self) - layout.addLayout(control_layout) - layout.addWidget(view) + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(headers_widget, 0) + main_layout.addWidget(view, 1) show_timer = QtCore.QTimer() show_timer.setInterval(0) @@ -114,12 +84,8 @@ class SceneInventoryWindow(QtWidgets.QDialog): self._update_all_button = update_all_button self._outdated_only_checkbox = outdated_only_checkbox self._view = view - self._model = model - self._proxy = proxy - self._version_delegate = version_delegate self._first_show = True - self._first_refresh = True def showEvent(self, event): super(SceneInventoryWindow, self).showEvent(event) @@ -139,29 +105,16 @@ class SceneInventoryWindow(QtWidgets.QDialog): whilst trying to name an instance. """ + pass def _on_refresh_request(self): """Signal callback to trigger 'refresh' without any arguments.""" self.refresh() - def refresh(self, containers=None): - self._first_refresh = False + def refresh(self): self._controller.reset() - with preserve_expanded_rows( - tree_view=self._view, - role=self._model.UniqueRole - ): - with preserve_selection( - tree_view=self._view, - role=self._model.UniqueRole, - current_index=False - ): - kwargs = {"containers": containers} - # TODO do not touch view's inner attribute - if self._view._hierarchy_view: - kwargs["selected"] = self._view._selected - self._model.refresh(**kwargs) + self._view.refresh() def _on_show_timer(self): if self._show_counter < 3: @@ -171,17 +124,13 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.refresh() def _on_hierarchy_view_change(self, enabled): - self._proxy.set_hierarchy_view(enabled) - self._model.set_hierarchy_view(enabled) + self._view.set_hierarchy_view(enabled) def _on_text_filter_change(self, text_filter): - if hasattr(self._proxy, "setFilterRegExp"): - self._proxy.setFilterRegExp(text_filter) - else: - self._proxy.setFilterRegularExpression(text_filter) + self._view.set_text_filter(text_filter) def _on_outdated_state_change(self): - self._proxy.set_filter_outdated( + self._view.set_filter_outdated( self._outdated_only_checkbox.isChecked() ) diff --git a/client/ayon_core/tools/stdout_broker/__init__.py b/client/ayon_core/tools/stdout_broker/__init__.py index e69de29bb2..e104c60573 100644 --- a/client/ayon_core/tools/stdout_broker/__init__.py +++ b/client/ayon_core/tools/stdout_broker/__init__.py @@ -0,0 +1,5 @@ +from .broker import StdOutBroker + +__all__ = ( + "StdOutBroker", +) diff --git a/client/ayon_core/tools/stdout_broker/app.py b/client/ayon_core/tools/stdout_broker/app.py index 15447b608b..ae73db1bb9 100644 --- a/client/ayon_core/tools/stdout_broker/app.py +++ b/client/ayon_core/tools/stdout_broker/app.py @@ -1,173 +1,12 @@ -import os -import sys -import threading -import collections -import websocket -import json -from datetime import datetime +import warnings +from .broker import StdOutBroker -from ayon_core.lib import Logger -from openpype_modules.webserver.host_console_listener import MsgAction +warnings.warn( + ( + "Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'" + " is deprecated. Please use 'ayon_core.tools.stdout_broker' instead." + ), + DeprecationWarning +) -log = Logger.get_logger(__name__) - - -class StdOutBroker: - """ - Application showing console in Services tray for non python hosts - instead of cmd window. - """ - MAX_LINES = 10000 - TIMER_TIMEOUT = 0.200 - - def __init__(self, host_name): - self.host_name = host_name - self.webserver_client = None - - self.original_stdout_write = None - self.original_stderr_write = None - self.log_queue = collections.deque() - - date_str = datetime.now().strftime("%d%m%Y%H%M%S") - self.host_id = "{}_{}".format(self.host_name, date_str) - - self._std_available = False - self._is_running = False - self._catch_std_outputs() - - self._timer = None - - @property - def send_to_tray(self): - """Checks if connected to tray and have access to logs.""" - return self.webserver_client and self._std_available - - def start(self): - """Start app, create and start timer""" - if not self._std_available or self._is_running: - return - self._is_running = True - self._create_timer() - self._connect_to_tray() - - def stop(self): - """Disconnect from Tray, process last logs""" - if not self._is_running: - return - self._is_running = False - self._process_queue() - self._disconnect_from_tray() - - def host_connected(self): - """Send to Tray console that host is ready - icon change. """ - log.info("Host {} connected".format(self.host_id)) - - payload = { - "host": self.host_id, - "action": MsgAction.INITIALIZED, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - self._send(payload) - - def _create_timer(self): - timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback) - timer.start() - self._timer = timer - - def _timer_callback(self): - if not self._is_running: - return - self._process_queue() - self._create_timer() - - def _connect_to_tray(self): - """Connect to Tray webserver to pass console output. """ - if not self._std_available: # not content to log - return - ws = websocket.WebSocket() - webserver_url = os.environ.get("AYON_WEBSERVER_URL") - - if not webserver_url: - print("Unknown webserver url, cannot connect to pass log") - return - - webserver_url = webserver_url.replace("http", "ws") - ws.connect("{}/ws/host_listener".format(webserver_url)) - self.webserver_client = ws - - payload = { - "host": self.host_id, - "action": MsgAction.CONNECTING, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - self._send(payload) - - def _disconnect_from_tray(self): - """Send to Tray that host is closing - remove from Services. """ - print("Host {} closing".format(self.host_name)) - if not self.webserver_client: - return - - payload = { - "host": self.host_id, - "action": MsgAction.CLOSE, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - - self._send(payload) - self.webserver_client.close() - - def _catch_std_outputs(self): - """Redirects standard out and error to own functions""" - if sys.stdout: - self.original_stdout_write = sys.stdout.write - sys.stdout.write = self._my_stdout_write - self._std_available = True - - if sys.stderr: - self.original_stderr_write = sys.stderr.write - sys.stderr.write = self._my_stderr_write - self._std_available = True - - def _my_stdout_write(self, text): - """Appends outputted text to queue, keep writing to original stdout""" - if self.original_stdout_write is not None: - self.original_stdout_write(text) - if self.send_to_tray: - self.log_queue.append(text) - - def _my_stderr_write(self, text): - """Appends outputted text to queue, keep writing to original stderr""" - if self.original_stderr_write is not None: - self.original_stderr_write(text) - if self.send_to_tray: - self.log_queue.append(text) - - def _process_queue(self): - """Sends lines and purges queue""" - if not self.send_to_tray: - return - - lines = tuple(self.log_queue) - self.log_queue.clear() - if lines: - payload = { - "host": self.host_id, - "action": MsgAction.ADD, - "text": "\n".join(lines) - } - - self._send(payload) - - def _send(self, payload): - """Worker method to send to existing websocket connection.""" - if not self.send_to_tray: - return - - try: - self.webserver_client.send(json.dumps(payload)) - except ConnectionResetError: # Tray closed - self._connect_to_tray() +__all__ = ("StdOutBroker", ) diff --git a/client/ayon_core/tools/stdout_broker/broker.py b/client/ayon_core/tools/stdout_broker/broker.py new file mode 100644 index 0000000000..291936008b --- /dev/null +++ b/client/ayon_core/tools/stdout_broker/broker.py @@ -0,0 +1,174 @@ +import os +import sys +import threading +import collections +import json +from datetime import datetime + +import websocket + +from ayon_core.lib import Logger +from ayon_core.modules.webserver import HostMsgAction + +log = Logger.get_logger(__name__) + + +class StdOutBroker: + """ + Application showing console in Services tray for non python hosts + instead of cmd window. + """ + MAX_LINES = 10000 + TIMER_TIMEOUT = 0.200 + + def __init__(self, host_name): + self.host_name = host_name + self.webserver_client = None + + self.original_stdout_write = None + self.original_stderr_write = None + self.log_queue = collections.deque() + + date_str = datetime.now().strftime("%d%m%Y%H%M%S") + self.host_id = "{}_{}".format(self.host_name, date_str) + + self._std_available = False + self._is_running = False + self._catch_std_outputs() + + self._timer = None + + @property + def send_to_tray(self): + """Checks if connected to tray and have access to logs.""" + return self.webserver_client and self._std_available + + def start(self): + """Start app, create and start timer""" + if not self._std_available or self._is_running: + return + self._is_running = True + self._create_timer() + self._connect_to_tray() + + def stop(self): + """Disconnect from Tray, process last logs""" + if not self._is_running: + return + self._is_running = False + self._process_queue() + self._disconnect_from_tray() + + def host_connected(self): + """Send to Tray console that host is ready - icon change. """ + log.info("Host {} connected".format(self.host_id)) + + payload = { + "host": self.host_id, + "action": HostMsgAction.INITIALIZED, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + self._send(payload) + + def _create_timer(self): + timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback) + timer.start() + self._timer = timer + + def _timer_callback(self): + if not self._is_running: + return + self._process_queue() + self._create_timer() + + def _connect_to_tray(self): + """Connect to Tray webserver to pass console output. """ + if not self._std_available: # not content to log + return + ws = websocket.WebSocket() + webserver_url = os.environ.get("AYON_WEBSERVER_URL") + + if not webserver_url: + print("Unknown webserver url, cannot connect to pass log") + return + + webserver_url = webserver_url.replace("http", "ws") + ws.connect("{}/ws/host_listener".format(webserver_url)) + self.webserver_client = ws + + payload = { + "host": self.host_id, + "action": HostMsgAction.CONNECTING, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + self._send(payload) + + def _disconnect_from_tray(self): + """Send to Tray that host is closing - remove from Services. """ + print("Host {} closing".format(self.host_name)) + if not self.webserver_client: + return + + payload = { + "host": self.host_id, + "action": HostMsgAction.CLOSE, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + + self._send(payload) + self.webserver_client.close() + + def _catch_std_outputs(self): + """Redirects standard out and error to own functions""" + if sys.stdout: + self.original_stdout_write = sys.stdout.write + sys.stdout.write = self._my_stdout_write + self._std_available = True + + if sys.stderr: + self.original_stderr_write = sys.stderr.write + sys.stderr.write = self._my_stderr_write + self._std_available = True + + def _my_stdout_write(self, text): + """Appends outputted text to queue, keep writing to original stdout""" + if self.original_stdout_write is not None: + self.original_stdout_write(text) + if self.send_to_tray: + self.log_queue.append(text) + + def _my_stderr_write(self, text): + """Appends outputted text to queue, keep writing to original stderr""" + if self.original_stderr_write is not None: + self.original_stderr_write(text) + if self.send_to_tray: + self.log_queue.append(text) + + def _process_queue(self): + """Sends lines and purges queue""" + if not self.send_to_tray: + return + + lines = tuple(self.log_queue) + self.log_queue.clear() + if lines: + payload = { + "host": self.host_id, + "action": HostMsgAction.ADD, + "text": "\n".join(lines) + } + + self._send(payload) + + def _send(self, payload): + """Worker method to send to existing websocket connection.""" + if not self.send_to_tray: + return + + try: + self.webserver_client.send(json.dumps(payload)) + except ConnectionResetError: # Tray closed + self._connect_to_tray() diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/tray.py index 957518afe4..eca87eb11d 100644 --- a/client/ayon_core/tools/tray/tray.py +++ b/client/ayon_core/tools/tray/tray.py @@ -447,8 +447,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def initialize_addons(self): self._initializing_addons = True - self.tray_man.initialize_addons() - self._initializing_addons = False + try: + self.tray_man.initialize_addons() + finally: + self._initializing_addons = False def _click_timer_timeout(self): self._click_timer.stop() diff --git a/client/ayon_core/tools/utils/delegates.py b/client/ayon_core/tools/utils/delegates.py index 1147074b77..b296f952e0 100644 --- a/client/ayon_core/tools/utils/delegates.py +++ b/client/ayon_core/tools/utils/delegates.py @@ -2,7 +2,7 @@ import time from datetime import datetime import logging -from qtpy import QtWidgets +from qtpy import QtWidgets, QtGui log = logging.getLogger(__name__) @@ -106,3 +106,80 @@ class PrettyTimeDelegate(QtWidgets.QStyledItemDelegate): def displayText(self, value, locale): if value is not None: return pretty_timestamp(value) + + +class StatusDelegate(QtWidgets.QStyledItemDelegate): + """Delegate showing status name and short name.""" + def __init__( + self, + status_name_role, + status_short_name_role, + status_color_role, + status_icon_role, + *args, **kwargs + ): + super().__init__(*args, **kwargs) + self.status_name_role = status_name_role + self.status_short_name_role = status_short_name_role + self.status_color_role = status_color_role + self.status_icon_role = status_icon_role + + def paint(self, painter, option, index): + if option.widget: + style = option.widget.style() + else: + style = QtWidgets.QApplication.style() + + style.drawControl( + QtWidgets.QCommonStyle.CE_ItemViewItem, + option, + painter, + option.widget + ) + + painter.save() + + text_rect = style.subElementRect( + QtWidgets.QCommonStyle.SE_ItemViewItemText, + option + ) + text_margin = style.proxy().pixelMetric( + QtWidgets.QCommonStyle.PM_FocusFrameHMargin, + option, + option.widget + ) + 1 + padded_text_rect = text_rect.adjusted( + text_margin, 0, - text_margin, 0 + ) + + fm = QtGui.QFontMetrics(option.font) + text = self._get_status_name(index) + if padded_text_rect.width() < fm.width(text): + text = self._get_status_short_name(index) + + fg_color = self._get_status_color(index) + pen = painter.pen() + pen.setColor(fg_color) + painter.setPen(pen) + + painter.drawText( + padded_text_rect, + option.displayAlignment, + text + ) + + painter.restore() + + def _get_status_name(self, index): + return index.data(self.status_name_role) + + def _get_status_short_name(self, index): + return index.data(self.status_short_name_role) + + def _get_status_color(self, index): + return QtGui.QColor(index.data(self.status_color_role)) + + def _get_status_icon(self, index): + if self.status_icon_role is not None: + return index.data(self.status_icon_role) + return None diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 2ad640de37..6aae68bf8a 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -370,6 +370,8 @@ class FoldersWidget(QtWidgets.QWidget): """ self._folders_proxy_model.setFilterFixedString(name) + if name: + self._folders_view.expandAll() def refresh(self): """Refresh folders model. diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index d56b370d75..323b5c07e1 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -1,6 +1,7 @@ import os import sys import contextlib +import collections from functools import partial from qtpy import QtWidgets, QtCore, QtGui @@ -196,16 +197,16 @@ def get_openpype_qt_app(): return get_ayon_qt_app() -def iter_model_rows(model, column, include_root=False): +def iter_model_rows(model, column=0, include_root=False): """Iterate over all row indices in a model""" - indices = [QtCore.QModelIndex()] # start iteration at root - - for index in indices: + indexes_queue = collections.deque() + # start iteration at root + indexes_queue.append(QtCore.QModelIndex()) + while indexes_queue: + index = indexes_queue.popleft() # Add children to the iterations - child_rows = model.rowCount(index) - for child_row in range(child_rows): - child_index = model.index(child_row, column, index) - indices.append(child_index) + for child_row in range(model.rowCount(index)): + indexes_queue.append(model.index(child_row, column, index)) if not include_root and not index.isValid(): continue diff --git a/client/ayon_core/tools/workfiles/abstract.py b/client/ayon_core/tools/workfiles/abstract.py index c9eb9004e3..330b413300 100644 --- a/client/ayon_core/tools/workfiles/abstract.py +++ b/client/ayon_core/tools/workfiles/abstract.py @@ -13,8 +13,10 @@ class WorkfileInfo: task_id (str): Task id. filepath (str): Filepath. filesize (int): File size. - creation_time (int): Creation time (timestamp). - modification_time (int): Modification time (timestamp). + creation_time (float): Creation time (timestamp). + modification_time (float): Modification time (timestamp). + created_by (Union[str, none]): User who created the file. + updated_by (Union[str, none]): User who last updated the file. note (str): Note. """ @@ -26,6 +28,8 @@ class WorkfileInfo: filesize, creation_time, modification_time, + created_by, + updated_by, note, ): self.folder_id = folder_id @@ -34,6 +38,8 @@ class WorkfileInfo: self.filesize = filesize self.creation_time = creation_time self.modification_time = modification_time + self.created_by = created_by + self.updated_by = updated_by self.note = note def to_data(self): @@ -50,6 +56,8 @@ class WorkfileInfo: "filesize": self.filesize, "creation_time": self.creation_time, "modification_time": self.modification_time, + "created_by": self.created_by, + "updated_by": self.updated_by, "note": self.note, } @@ -212,6 +220,7 @@ class FileItem: dirpath (str): Directory path of file. filename (str): Filename. modified (float): Modified timestamp. + created_by (Optional[str]): Username. representation_id (Optional[str]): Representation id of published workfile. filepath (Optional[str]): Prepared filepath. @@ -223,6 +232,8 @@ class FileItem: dirpath, filename, modified, + created_by=None, + updated_by=None, representation_id=None, filepath=None, exists=None @@ -230,6 +241,8 @@ class FileItem: self.filename = filename self.dirpath = dirpath self.modified = modified + self.created_by = created_by + self.updated_by = updated_by self.representation_id = representation_id self._filepath = filepath self._exists = exists @@ -269,6 +282,7 @@ class FileItem: "filename": self.filename, "dirpath": self.dirpath, "modified": self.modified, + "created_by": self.created_by, "representation_id": self.representation_id, "filepath": self.filepath, "exists": self.exists, @@ -522,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass + @abstractmethod + def get_user_items_by_name(self): + """Get user items available on AYON server. + + Returns: + Dict[str, UserItem]: User items by username. + + """ + pass + # Host information @abstractmethod def get_workfile_extensions(self): @@ -810,12 +834,13 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): """Get workarea file items. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task name. + sender (Optional[str]): Who requested workarea file items. Returns: list[FileItem]: List of workarea file items. @@ -881,12 +906,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): """Workfile info from database. Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. Returns: @@ -897,7 +922,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): pass @abstractmethod - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): """Save workfile info to database. At this moment the only information which can be saved about @@ -908,7 +933,7 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): Args: folder_id (str): Folder id. - task_id (str): Task id. + task_name (str): Task id. filepath (str): Workfile path. note (Union[str, None]): Note. """ diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 3048e6be94..31bdb2bab6 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -19,6 +19,7 @@ from ayon_core.tools.common_models import ( HierarchyModel, HierarchyExpectedSelection, ProjectsModel, + UsersModel, ) from .abstract import ( @@ -161,6 +162,7 @@ class BaseWorkfileController( self._save_is_enabled = True # Expected selected folder and task + self._users_model = self._create_users_model() self._expected_selection = self._create_expected_selection_obj() self._selection_model = self._create_selection_model() self._projects_model = self._create_projects_model() @@ -176,6 +178,12 @@ class BaseWorkfileController( def is_host_valid(self): return self._host_is_valid + def _create_users_model(self): + return UsersModel(self) + + def _create_workfiles_model(self): + return WorkfilesModel(self) + def _create_expected_selection_obj(self): return WorkfilesToolExpectedSelection(self) @@ -188,9 +196,6 @@ class BaseWorkfileController( def _create_hierarchy_model(self): return HierarchyModel(self) - def _create_workfiles_model(self): - return WorkfilesModel(self) - @property def event_system(self): """Inner event system for workfiles tool controller. @@ -272,6 +277,10 @@ class BaseWorkfileController( {"enabled": enabled} ) + def get_user_items_by_name(self): + project_name = self.get_current_project_name() + return self._users_model.get_user_items_by_name(project_name) + # Host information def get_workfile_extensions(self): host = self._host @@ -402,9 +411,11 @@ class BaseWorkfileController( return self._workfiles_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_name, sender=None): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workarea_file_items( - folder_id, task_id) + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workfiles_model.get_workarea_save_as_data( @@ -439,12 +450,14 @@ class BaseWorkfileController( return self._workfiles_model.get_published_file_items( folder_id, task_name) - def get_workfile_info(self, folder_id, task_id, filepath): + def get_workfile_info(self, folder_id, task_name, filepath): + task_id = self._get_task_id(folder_id, task_name) return self._workfiles_model.get_workfile_info( folder_id, task_id, filepath ) - def save_workfile_info(self, folder_id, task_id, filepath, note): + def save_workfile_info(self, folder_id, task_name, filepath, note): + task_id = self._get_task_id(folder_id, task_name) self._workfiles_model.save_workfile_info( folder_id, task_id, filepath, note ) @@ -619,6 +632,17 @@ class BaseWorkfileController( def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") + def _get_task_id(self, folder_id, task_name, sender=None): + task_item = self._hierarchy_model.get_task_item_by_name( + self.get_current_project_name(), + folder_id, + task_name, + sender + ) + if not task_item: + return None + return task_item.id + # Expected selection # - expected selection is used to restore selection after refresh # or when current context should be used @@ -714,7 +738,7 @@ class BaseWorkfileController( self._host_save_workfile(dst_filepath) # Make sure workfile info exists - self.save_workfile_info(folder_id, task_id, dst_filepath, None) + self.save_workfile_info(folder_id, task_name, dst_filepath, None) # Create extra folders create_workdir_extra_folders( diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 5f59b99b22..a268a9bd0e 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -1,11 +1,13 @@ import os import re import copy +import uuid import arrow import ayon_api from ayon_api.operations import OperationsSession +from ayon_core.lib import get_ayon_username from ayon_core.pipeline.template_data import ( get_template_data, get_task_template_data, @@ -23,6 +25,8 @@ from ayon_core.tools.workfiles.abstract import ( WorkfileInfo, ) +_NOT_SET = object() + class CommentMatcher(object): """Use anatomy and work file data to parse comments from filenames. @@ -170,7 +174,7 @@ class WorkareaModel: folder_mapping[task_id] = workdir return workdir - def get_file_items(self, folder_id, task_id): + def get_file_items(self, folder_id, task_id, task_name): items = [] if not folder_id or not task_id: return items @@ -188,10 +192,17 @@ class WorkareaModel: if ext not in self._extensions: continue - modified = os.path.getmtime(filepath) - items.append( - FileItem(workdir, filename, modified) + workfile_info = self._controller.get_workfile_info( + folder_id, task_name, filepath ) + modified = os.path.getmtime(filepath) + items.append(FileItem( + workdir, + filename, + modified, + workfile_info.created_by, + workfile_info.updated_by, + )) return items def _get_template_key(self, fill_data): @@ -439,6 +450,7 @@ class WorkfileEntitiesModel: self._controller = controller self._cache = {} self._items = {} + self._current_username = _NOT_SET def _get_workfile_info_identifier( self, folder_id, task_id, rootless_path @@ -459,8 +471,12 @@ class WorkfileEntitiesModel: self, folder_id, task_id, workfile_info, filepath ): note = "" + created_by = None + updated_by = None if workfile_info: note = workfile_info["attrib"].get("description") or "" + created_by = workfile_info.get("createdBy") + updated_by = workfile_info.get("updatedBy") filestat = os.stat(filepath) return WorkfileInfo( @@ -470,6 +486,8 @@ class WorkfileEntitiesModel: filesize=filestat.st_size, creation_time=filestat.st_ctime, modification_time=filestat.st_mtime, + created_by=created_by, + updated_by=updated_by, note=note ) @@ -481,7 +499,7 @@ class WorkfileEntitiesModel: for workfile_info in ayon_api.get_workfiles_info( self._controller.get_current_project_name(), task_ids=[task_id], - fields=["id", "path", "attrib"], + fields=["id", "path", "attrib", "createdBy", "updatedBy"], ): workfile_identifier = self._get_workfile_info_identifier( folder_id, task_id, workfile_info["path"] @@ -525,18 +543,32 @@ class WorkfileEntitiesModel: self._items.pop(identifier, None) return - if note is None: - return - old_note = workfile_info.get("attrib", {}).get("note") new_workfile_info = copy.deepcopy(workfile_info) - attrib = new_workfile_info.setdefault("attrib", {}) - attrib["description"] = note + update_data = {} + if note is not None and old_note != note: + update_data["attrib"] = {"description": note} + attrib = new_workfile_info.setdefault("attrib", {}) + attrib["description"] = note + + username = self._get_current_username() + # Automatically fix 'createdBy' and 'updatedBy' fields + # NOTE both fields were not automatically filled by server + # until 1.1.3 release. + if workfile_info.get("createdBy") is None: + update_data["createdBy"] = username + new_workfile_info["createdBy"] = username + + if workfile_info.get("updatedBy") != username: + update_data["updatedBy"] = username + new_workfile_info["updatedBy"] = username + + if not update_data: + return + self._cache[identifier] = new_workfile_info self._items.pop(identifier, None) - if old_note == note: - return project_name = self._controller.get_current_project_name() @@ -545,7 +577,7 @@ class WorkfileEntitiesModel: project_name, "workfile", workfile_info["id"], - {"attrib": {"description": note}}, + update_data, ) session.commit() @@ -554,13 +586,19 @@ class WorkfileEntitiesModel: project_name = self._controller.get_current_project_name() + username = self._get_current_username() workfile_info = { + "id": uuid.uuid4().hex, "path": rootless_path, "taskId": task_id, "attrib": { "extension": extension, "description": note - } + }, + # TODO remove 'createdBy' and 'updatedBy' fields when server is + # or above 1.1.3 . + "createdBy": username, + "updatedBy": username, } session = OperationsSession() @@ -568,6 +606,11 @@ class WorkfileEntitiesModel: session.commit() return workfile_info + def _get_current_username(self): + if self._current_username is _NOT_SET: + self._current_username = get_ayon_username() + return self._current_username + class PublishWorkfilesModel: """Model for handling of published workfiles. @@ -599,7 +642,7 @@ class PublishWorkfilesModel: return self._cached_repre_extensions def _file_item_from_representation( - self, repre_entity, project_anatomy, task_name=None + self, repre_entity, project_anatomy, author, task_name=None ): if task_name is not None: task_info = repre_entity["context"].get("task") @@ -634,6 +677,8 @@ class PublishWorkfilesModel: dirpath, filename, created_at.float_timestamp, + author, + None, repre_entity["id"] ) @@ -643,9 +688,9 @@ class PublishWorkfilesModel: # Get subset docs of folder product_entities = ayon_api.get_products( project_name, - folder_ids=[folder_id], - product_types=["workfile"], - fields=["id", "name"] + folder_ids={folder_id}, + product_types={"workfile"}, + fields={"id", "name"} ) output = [] @@ -657,25 +702,33 @@ class PublishWorkfilesModel: version_entities = ayon_api.get_versions( project_name, product_ids=product_ids, - fields=["id", "productId"] + fields={"id", "author"} ) - version_ids = {version["id"] for version in version_entities} - if not version_ids: + versions_by_id = { + version["id"]: version + for version in version_entities + } + if not versions_by_id: return output # Query representations of filtered versions and add filter for # extension repre_entities = ayon_api.get_representations( project_name, - version_ids=version_ids + version_ids=set(versions_by_id) ) project_anatomy = self._controller.project_anatomy # Filter queried representations by task name if task is set file_items = [] for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = versions_by_id[version_id] file_item = self._file_item_from_representation( - repre_entity, project_anatomy, task_name + repre_entity, + project_anatomy, + version_entity["author"], + task_name, ) if file_item is not None: file_items.append(file_item) @@ -719,19 +772,21 @@ class WorkfilesModel: return self._workarea_model.get_workarea_dir_by_context( folder_id, task_id) - def get_workarea_file_items(self, folder_id, task_id): + def get_workarea_file_items(self, folder_id, task_id, task_name): """Workfile items for passed context from workarea. Args: folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. + task_name (Union[str, None]): Task name. Returns: list[FileItem]: List of file items matching workarea of passed context. """ - - return self._workarea_model.get_file_items(folder_id, task_id) + return self._workarea_model.get_file_items( + folder_id, task_id, task_name + ) def get_workarea_save_as_data(self, folder_id, task_id): return self._workarea_model.get_workarea_save_as_data( diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py index bf36d790e9..2ce8569a9b 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_published.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_published.py @@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame REPRE_ID_ROLE = QtCore.Qt.UserRole + 1 FILEPATH_ROLE = QtCore.Qt.UserRole + 2 -DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 +AUTHOR_ROLE = QtCore.Qt.UserRole + 3 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4 class PublishedFilesModel(QtGui.QStandardItemModel): @@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel): controller (AbstractWorkfilesFrontend): The control object. """ + columns = [ + "Name", + "Author", + "Date Modified", + ] + date_modified_col = columns.index("Date Modified") + def __init__(self, controller): super(PublishedFilesModel, self).__init__() - self.setColumnCount(2) - - self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") - self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + self.setColumnCount(len(self.columns)) + for idx, label in enumerate(self.columns): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) controller.register_event_callback( "selection.task.changed", @@ -185,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel): self._remove_empty_item() self._remove_missing_context_item() + user_items_by_name = self._controller.get_user_items_by_name() + items_to_remove = set(self._items_by_id.keys()) new_items = [] for file_item in file_items: @@ -205,8 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel): else: flags = QtCore.Qt.NoItemFlags + author = file_item.created_by + user_item = user_items_by_name.get(author) + if user_item is not None and user_item.full_name: + author = user_item.full_name + item.setFlags(flags) + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(author, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_id[repre_id] = item @@ -225,22 +241,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel): # Use flags of first column for all columns if index.column() != 0: index = self.index(index.row(), 0, index.parent()) - return super(PublishedFilesModel, self).flags(index) + return super().flags(index) def data(self, index, role=None): if role is None: role = QtCore.Qt.DisplayRole # Handle roles for first column - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None + col = index.column() + if col != 1: + return super().data(index, role) - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if col == 1: + role = AUTHOR_ROLE + elif col == 2: role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) + else: + return None + index = self.index(index.row(), 0, index.parent()) - return super(PublishedFilesModel, self).data(index, role) + return super().data(index, role) class SelectContextOverlay(BaseOverlayFrame): @@ -295,7 +319,7 @@ class PublishedFilesWidget(QtWidgets.QWidget): view.setModel(proxy_model) time_delegate = PrettyTimeDelegate() - view.setItemDelegateForColumn(1, time_delegate) + view.setItemDelegateForColumn(model.date_modified_col, time_delegate) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py index fe6abee951..7f76b6a8ab 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget_workarea.py @@ -10,7 +10,8 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate FILENAME_ROLE = QtCore.Qt.UserRole + 1 FILEPATH_ROLE = QtCore.Qt.UserRole + 2 -DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 +AUTHOR_ROLE = QtCore.Qt.UserRole + 3 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4 class WorkAreaFilesModel(QtGui.QStandardItemModel): @@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): """ refreshed = QtCore.Signal() + columns = [ + "Name", + "Author", + "Date Modified", + ] + date_modified_col = columns.index("Date Modified") def __init__(self, controller): super(WorkAreaFilesModel, self).__init__() - self.setColumnCount(2) + self.setColumnCount(len(self.columns)) - self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") - self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + for idx, label in enumerate(self.columns): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) controller.register_event_callback( "selection.folder.changed", @@ -59,7 +66,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): self._empty_item_used = False self._published_mode = False self._selected_folder_id = None - self._selected_task_id = None + self._selected_task_name = None self._add_missing_context_item() @@ -146,7 +153,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] - self._selected_task_id = event["task_id"] + self._selected_task_name = event["task_name"] if not self._published_mode: self._fill_items() @@ -172,13 +179,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): def _fill_items_impl(self): folder_id = self._selected_folder_id - task_id = self._selected_task_id - if not folder_id or not task_id: + task_name = self._selected_task_name + if not folder_id or not task_name: self._add_missing_context_item() return file_items = self._controller.get_workarea_file_items( - folder_id, task_id + folder_id, task_name ) root_item = self.invisibleRootItem() if not file_items: @@ -186,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): return self._remove_empty_item() self._remove_missing_context_item() + user_items_by_name = self._controller.get_user_items_by_name() items_to_remove = set(self._items_by_filename.keys()) new_items = [] @@ -205,7 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): item.setData(file_item.filename, QtCore.Qt.DisplayRole) item.setData(file_item.filename, FILENAME_ROLE) + updated_by = file_item.updated_by + user_item = user_items_by_name.get(updated_by) + if user_item is not None and user_item.full_name: + updated_by = user_item.full_name + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(updated_by, AUTHOR_ROLE) item.setData(file_item.modified, DATE_MODIFIED_ROLE) self._items_by_filename[file_item.filename] = item @@ -224,22 +238,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel): # Use flags of first column for all columns if index.column() != 0: index = self.index(index.row(), 0, index.parent()) - return super(WorkAreaFilesModel, self).flags(index) + return super().flags(index) def data(self, index, role=None): if role is None: role = QtCore.Qt.DisplayRole # Handle roles for first column - if index.column() == 1: - if role == QtCore.Qt.DecorationRole: - return None + col = index.column() + if col == 0: + return super().data(index, role) - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if col == 1: + role = AUTHOR_ROLE + elif col == 2: role = DATE_MODIFIED_ROLE - index = self.index(index.row(), 0, index.parent()) + else: + return None + index = self.index(index.row(), 0, index.parent()) - return super(WorkAreaFilesModel, self).data(index, role) + return super().data(index, role) def set_published_mode(self, published_mode): if self._published_mode == published_mode: @@ -279,7 +301,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget): view.setModel(proxy_model) time_delegate = PrettyTimeDelegate() - view.setItemDelegateForColumn(1, time_delegate) + view.setItemDelegateForColumn(model.date_modified_col, time_delegate) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. diff --git a/client/ayon_core/tools/workfiles/widgets/side_panel.py b/client/ayon_core/tools/workfiles/widgets/side_panel.py index 5085f4701e..7ba60b5544 100644 --- a/client/ayon_core/tools/workfiles/widgets/side_panel.py +++ b/client/ayon_core/tools/workfiles/widgets/side_panel.py @@ -75,7 +75,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save = btn_note_save self._folder_id = None - self._task_id = None + self._task_name = None self._filepath = None self._orig_note = "" self._controller = controller @@ -93,10 +93,10 @@ class SidePanelWidget(QtWidgets.QWidget): def _on_selection_change(self, event): folder_id = event["folder_id"] - task_id = event["task_id"] + task_name = event["task_name"] filepath = event["path"] - self._set_context(folder_id, task_id, filepath) + self._set_context(folder_id, task_name, filepath) def _on_note_change(self): text = self._note_input.toPlainText() @@ -106,19 +106,19 @@ class SidePanelWidget(QtWidgets.QWidget): note = self._note_input.toPlainText() self._controller.save_workfile_info( self._folder_id, - self._task_id, + self._task_name, self._filepath, note ) self._orig_note = note self._btn_note_save.setEnabled(False) - def _set_context(self, folder_id, task_id, filepath): + def _set_context(self, folder_id, task_name, filepath): workfile_info = None # Check if folder, task and file are selected - if bool(folder_id) and bool(task_id) and bool(filepath): + if bool(folder_id) and bool(task_name) and bool(filepath): workfile_info = self._controller.get_workfile_info( - folder_id, task_id, filepath + folder_id, task_name, filepath ) enabled = workfile_info is not None @@ -127,7 +127,7 @@ class SidePanelWidget(QtWidgets.QWidget): self._btn_note_save.setEnabled(enabled) self._folder_id = folder_id - self._task_id = task_id + self._task_name = task_name self._filepath = filepath # Disable inputs and remove texts if any required arguments are @@ -147,13 +147,38 @@ class SidePanelWidget(QtWidgets.QWidget): workfile_info.creation_time) modification_time = datetime.datetime.fromtimestamp( workfile_info.modification_time) + + user_items_by_name = self._controller.get_user_items_by_name() + + def convert_username(username): + user_item = user_items_by_name.get(username) + if user_item is not None and user_item.full_name: + return user_item.full_name + return username + + created_lines = [ + creation_time.strftime(datetime_format) + ] + if workfile_info.created_by: + created_lines.insert( + 0, convert_username(workfile_info.created_by) + ) + + modified_lines = [ + modification_time.strftime(datetime_format) + ] + if workfile_info.updated_by: + modified_lines.insert( + 0, convert_username(workfile_info.updated_by) + ) + lines = ( "Size:", size_value, "Created:", - creation_time.strftime(datetime_format), + "
".join(created_lines), "Modified:", - modification_time.strftime(datetime_format) + "
".join(modified_lines), ) self._orig_note = note self._note_input.setPlainText(note) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 1cfae7ec90..8bcff66f50 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): split_widget.addWidget(tasks_widget) split_widget.addWidget(col_3_widget) split_widget.addWidget(side_panel) - split_widget.setSizes([255, 160, 455, 175]) + split_widget.setSizes([255, 175, 550, 190]) body_layout.addWidget(split_widget) @@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): # Force focus on the open button by default, required for Houdini. self._files_widget.setFocus() - self.resize(1200, 600) + self.resize(1260, 600) def _create_col_1_widget(self, controller, parent): col_widget = QtWidgets.QWidget(parent) diff --git a/client/ayon_core/vendor/python/common/README.md b/client/ayon_core/vendor/python/README.md similarity index 100% rename from client/ayon_core/vendor/python/common/README.md rename to client/ayon_core/vendor/python/README.md diff --git a/client/ayon_core/vendor/python/python_2/README.md b/client/ayon_core/vendor/python/python_2/README.md deleted file mode 100644 index f101ddbf54..0000000000 --- a/client/ayon_core/vendor/python/python_2/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Info - -Only **Python 2** specific modules are here. \ No newline at end of file diff --git a/client/ayon_core/vendor/python/python_2/arrow/__init__.py b/client/ayon_core/vendor/python/python_2/arrow/__init__.py deleted file mode 100644 index 2883527be8..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from ._version import __version__ -from .api import get, now, utcnow -from .arrow import Arrow -from .factory import ArrowFactory -from .formatter import ( - FORMAT_ATOM, - FORMAT_COOKIE, - FORMAT_RFC822, - FORMAT_RFC850, - FORMAT_RFC1036, - FORMAT_RFC1123, - FORMAT_RFC2822, - FORMAT_RFC3339, - FORMAT_RSS, - FORMAT_W3C, -) -from .parser import ParserError diff --git a/client/ayon_core/vendor/python/python_2/arrow/_version.py b/client/ayon_core/vendor/python/python_2/arrow/_version.py deleted file mode 100644 index fd86b3ee91..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.17.0" diff --git a/client/ayon_core/vendor/python/python_2/arrow/api.py b/client/ayon_core/vendor/python/python_2/arrow/api.py deleted file mode 100644 index a6b7be3de2..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/api.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Provides the default implementation of :class:`ArrowFactory ` -methods for use as a module API. - -""" - -from __future__ import absolute_import - -from arrow.factory import ArrowFactory - -# internal default factory. -_factory = ArrowFactory() - - -def get(*args, **kwargs): - """Calls the default :class:`ArrowFactory ` ``get`` method.""" - - return _factory.get(*args, **kwargs) - - -get.__doc__ = _factory.get.__doc__ - - -def utcnow(): - """Calls the default :class:`ArrowFactory ` ``utcnow`` method.""" - - return _factory.utcnow() - - -utcnow.__doc__ = _factory.utcnow.__doc__ - - -def now(tz=None): - """Calls the default :class:`ArrowFactory ` ``now`` method.""" - - return _factory.now(tz) - - -now.__doc__ = _factory.now.__doc__ - - -def factory(type): - """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` - or derived type. - - :param type: the type, :class:`Arrow ` or derived. - - """ - - return ArrowFactory(type) - - -__all__ = ["get", "utcnow", "now", "factory"] diff --git a/client/ayon_core/vendor/python/python_2/arrow/arrow.py b/client/ayon_core/vendor/python/python_2/arrow/arrow.py deleted file mode 100644 index 4fe9541789..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/arrow.py +++ /dev/null @@ -1,1584 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Provides the :class:`Arrow ` class, an enhanced ``datetime`` -replacement. - -""" - -from __future__ import absolute_import - -import calendar -import sys -import warnings -from datetime import datetime, timedelta -from datetime import tzinfo as dt_tzinfo -from math import trunc - -from dateutil import tz as dateutil_tz -from dateutil.relativedelta import relativedelta - -from arrow import formatter, locales, parser, util - -if sys.version_info[:2] < (3, 6): # pragma: no cover - with warnings.catch_warnings(): - warnings.simplefilter("default", DeprecationWarning) - warnings.warn( - "Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to " - "Python 3.6+ to continue receiving updates for Arrow.", - DeprecationWarning, - ) - - -class Arrow(object): - """An :class:`Arrow ` object. - - Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing - additional functionality. - - :param year: the calendar year. - :param month: the calendar month. - :param day: the calendar day. - :param hour: (optional) the hour. Defaults to 0. - :param minute: (optional) the minute, Defaults to 0. - :param second: (optional) the second, Defaults to 0. - :param microsecond: (optional) the microsecond. Defaults to 0. - :param tzinfo: (optional) A timezone expression. Defaults to UTC. - :param fold: (optional) 0 or 1, used to disambiguate repeated times. Defaults to 0. - - .. _tz-expr: - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO 8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - Usage:: - - >>> import arrow - >>> arrow.Arrow(2013, 5, 5, 12, 30, 45) - - - """ - - resolution = datetime.resolution - - _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] - _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] - _MONTHS_PER_QUARTER = 3 - _SECS_PER_MINUTE = float(60) - _SECS_PER_HOUR = float(60 * 60) - _SECS_PER_DAY = float(60 * 60 * 24) - _SECS_PER_WEEK = float(60 * 60 * 24 * 7) - _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5) - _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25) - - def __init__( - self, - year, - month, - day, - hour=0, - minute=0, - second=0, - microsecond=0, - tzinfo=None, - **kwargs - ): - if tzinfo is None: - tzinfo = dateutil_tz.tzutc() - # detect that tzinfo is a pytz object (issue #626) - elif ( - isinstance(tzinfo, dt_tzinfo) - and hasattr(tzinfo, "localize") - and hasattr(tzinfo, "zone") - and tzinfo.zone - ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) - elif util.isstr(tzinfo): - tzinfo = parser.TzinfoParser.parse(tzinfo) - - fold = kwargs.get("fold", 0) - - # use enfold here to cover direct arrow.Arrow init on 2.7/3.5 - self._datetime = dateutil_tz.enfold( - datetime(year, month, day, hour, minute, second, microsecond, tzinfo), - fold=fold, - ) - - # factories: single object, both original and from datetime. - - @classmethod - def now(cls, tzinfo=None): - """Constructs an :class:`Arrow ` object, representing "now" in the given - timezone. - - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - - Usage:: - - >>> arrow.now('Asia/Baku') - - - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() - - dt = datetime.now(tzinfo) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def utcnow(cls): - """Constructs an :class:`Arrow ` object, representing "now" in UTC - time. - - Usage:: - - >>> arrow.utcnow() - - - """ - - dt = datetime.now(dateutil_tz.tzutc()) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromtimestamp(cls, timestamp, tzinfo=None): - """Constructs an :class:`Arrow ` object from a timestamp, converted to - the given timezone. - - :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() - elif util.isstr(tzinfo): - tzinfo = parser.TzinfoParser.parse(tzinfo) - - if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) - - timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.fromtimestamp(timestamp, tzinfo) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def utcfromtimestamp(cls, timestamp): - """Constructs an :class:`Arrow ` object from a timestamp, in UTC time. - - :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - - """ - - if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) - - timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.utcfromtimestamp(timestamp) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dateutil_tz.tzutc(), - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromdatetime(cls, dt, tzinfo=None): - """Constructs an :class:`Arrow ` object from a ``datetime`` and - optional replacement timezone. - - :param dt: the ``datetime`` - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s - timezone, or UTC if naive. - - If you only want to replace the timezone of naive datetimes:: - - >>> dt - datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc()) - >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific') - - - """ - - if tzinfo is None: - if dt.tzinfo is None: - tzinfo = dateutil_tz.tzutc() - else: - tzinfo = dt.tzinfo - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromdate(cls, date, tzinfo=None): - """Constructs an :class:`Arrow ` object from a ``date`` and optional - replacement timezone. Time values are set to 0. - - :param date: the ``date`` - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzutc() - - return cls(date.year, date.month, date.day, tzinfo=tzinfo) - - @classmethod - def strptime(cls, date_str, fmt, tzinfo=None): - """Constructs an :class:`Arrow ` object from a date string and format, - in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. - - :param date_str: the date string. - :param fmt: the format string. - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed - timezone if ``fmt`` contains a timezone directive, otherwise UTC. - - Usage:: - - >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') - - - """ - - dt = datetime.strptime(date_str, fmt) - if tzinfo is None: - tzinfo = dt.tzinfo - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo, - fold=getattr(dt, "fold", 0), - ) - - # factories: ranges and spans - - @classmethod - def range(cls, frame, start, end=None, tz=None, limit=None): - """Returns an iterator of :class:`Arrow ` objects, representing - points in time between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to - ``start``'s timezone, or UTC if ``start`` is naive. - :param limit: (optional) A maximum number of tuples to return. - - **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to - return the entire range. Call with ``limit`` alone to return a maximum # of results from - the start. Call with both to cap a range at a maximum # of results. - - **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before - iterating. As such, either call with naive objects and ``tz``, or aware objects from the - same timezone and no ``tz``. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - Usage:: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - - - - **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator:: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 13, 30) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - """ - - _, frame_relative, relative_steps = cls._get_frames(frame) - - tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - - start = cls._get_datetime(start).replace(tzinfo=tzinfo) - end, limit = cls._get_iteration_params(end, limit) - end = cls._get_datetime(end).replace(tzinfo=tzinfo) - - current = cls.fromdatetime(start) - original_day = start.day - day_is_clipped = False - i = 0 - - while current <= end and i < limit: - i += 1 - yield current - - values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo).shift( - **{frame_relative: relative_steps} - ) - - if frame in ["month", "quarter", "year"] and current.day < original_day: - day_is_clipped = True - - if day_is_clipped and not cls._is_last_day_of_month(current): - current = current.replace(day=original_day) - - def span(self, frame, count=1, bounds="[)"): - """Returns two new :class:`Arrow ` objects, representing the timespan - of the :class:`Arrow ` object in a given timeframe. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param count: (optional) the number of frames to span. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the span. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Usage:: - - >>> arrow.utcnow() - - - >>> arrow.utcnow().span('hour') - (, ) - - >>> arrow.utcnow().span('day') - (, ) - - >>> arrow.utcnow().span('day', count=2) - (, ) - - >>> arrow.utcnow().span('day', bounds='[]') - (, ) - - """ - - util.validate_bounds(bounds) - - frame_absolute, frame_relative, relative_steps = self._get_frames(frame) - - if frame_absolute == "week": - attr = "day" - elif frame_absolute == "quarter": - attr = "month" - else: - attr = frame_absolute - - index = self._ATTRS.index(attr) - frames = self._ATTRS[: index + 1] - - values = [getattr(self, f) for f in frames] - - for _ in range(3 - len(values)): - values.append(1) - - floor = self.__class__(*values, tzinfo=self.tzinfo) - - if frame_absolute == "week": - floor = floor.shift(days=-(self.isoweekday() - 1)) - elif frame_absolute == "quarter": - floor = floor.shift(months=-((self.month - 1) % 3)) - - ceil = floor.shift(**{frame_relative: count * relative_steps}) - - if bounds[0] == "(": - floor = floor.shift(microseconds=+1) - - if bounds[1] == ")": - ceil = ceil.shift(microseconds=-1) - - return floor, ceil - - def floor(self, frame): - """Returns a new :class:`Arrow ` object, representing the "floor" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the first element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().floor('hour') - - """ - - return self.span(frame)[0] - - def ceil(self, frame): - """Returns a new :class:`Arrow ` object, representing the "ceiling" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the second element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().ceil('hour') - - """ - - return self.span(frame)[1] - - @classmethod - def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): - """Returns an iterator of tuples, each :class:`Arrow ` objects, - representing a series of timespans between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to - ``start``'s timezone, or UTC if ``start`` is naive. - :param limit: (optional) A maximum number of tuples to return. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in each span in the range. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to - return the entire range. Call with ``limit`` alone to return a maximum # of results from - the start. Call with both to cap a range at a maximum # of results. - - **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before - iterating. As such, either call with naive objects and ``tz``, or aware objects from the - same timezone and no ``tz``. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned - iterator of timespans. - - Usage: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): - ... print(r) - ... - (, ) - (, ) - (, ) - (, ) - (, ) - (, ) - - """ - - tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - start = cls.fromdatetime(start, tzinfo).span(frame)[0] - _range = cls.range(frame, start, end, tz, limit) - return (r.span(frame, bounds=bounds) for r in _range) - - @classmethod - def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): - """Returns an iterator of tuples, each :class:`Arrow ` objects, - representing a series of intervals between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param interval: (optional) Time interval for the given time frame. - :param tz: (optional) A timezone expression. Defaults to UTC. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the intervals. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - Supported frame values: year, quarter, month, week, day, hour, minute, second - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO 8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - Usage: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.interval('hour', start, end, 2): - ... print r - ... - (, ) - (, ) - (, ) - """ - if interval < 1: - raise ValueError("interval has to be a positive integer") - - spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds)) - while True: - try: - intvlStart, intvlEnd = next(spanRange) - for _ in range(interval - 1): - _, intvlEnd = next(spanRange) - yield intvlStart, intvlEnd - except StopIteration: - return - - # representations - - def __repr__(self): - return "<{} [{}]>".format(self.__class__.__name__, self.__str__()) - - def __str__(self): - return self._datetime.isoformat() - - def __format__(self, formatstr): - - if len(formatstr) > 0: - return self.format(formatstr) - - return str(self) - - def __hash__(self): - return self._datetime.__hash__() - - # attributes and properties - - def __getattr__(self, name): - - if name == "week": - return self.isocalendar()[1] - - if name == "quarter": - return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 - - if not name.startswith("_"): - value = getattr(self._datetime, name, None) - - if value is not None: - return value - - return object.__getattribute__(self, name) - - @property - def tzinfo(self): - """Gets the ``tzinfo`` of the :class:`Arrow ` object. - - Usage:: - - >>> arw=arrow.utcnow() - >>> arw.tzinfo - tzutc() - - """ - - return self._datetime.tzinfo - - @tzinfo.setter - def tzinfo(self, tzinfo): - """ Sets the ``tzinfo`` of the :class:`Arrow ` object. """ - - self._datetime = self._datetime.replace(tzinfo=tzinfo) - - @property - def datetime(self): - """Returns a datetime representation of the :class:`Arrow ` object. - - Usage:: - - >>> arw=arrow.utcnow() - >>> arw.datetime - datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) - - """ - - return self._datetime - - @property - def naive(self): - """Returns a naive datetime representation of the :class:`Arrow ` - object. - - Usage:: - - >>> nairobi = arrow.now('Africa/Nairobi') - >>> nairobi - - >>> nairobi.naive - datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) - - """ - - return self._datetime.replace(tzinfo=None) - - @property - def timestamp(self): - """Returns a timestamp representation of the :class:`Arrow ` object, in - UTC time. - - Usage:: - - >>> arrow.utcnow().timestamp - 1548260567 - - """ - - warnings.warn( - "For compatibility with the datetime.timestamp() method this property will be replaced with a method in " - "the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as " - "possible.", - DeprecationWarning, - ) - return calendar.timegm(self._datetime.utctimetuple()) - - @property - def int_timestamp(self): - """Returns a timestamp representation of the :class:`Arrow ` object, in - UTC time. - - Usage:: - - >>> arrow.utcnow().int_timestamp - 1548260567 - - """ - - return calendar.timegm(self._datetime.utctimetuple()) - - @property - def float_timestamp(self): - """Returns a floating-point representation of the :class:`Arrow ` - object, in UTC time. - - Usage:: - - >>> arrow.utcnow().float_timestamp - 1548260516.830896 - - """ - - # IDEA get rid of this in 1.0.0 and wrap datetime.timestamp() - # Or for compatibility retain this but make it call the timestamp method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - return self.timestamp + float(self.microsecond) / 1000000 - - @property - def fold(self): - """ Returns the ``fold`` value of the :class:`Arrow ` object. """ - - # in python < 3.6 _datetime will be a _DatetimeWithFold if fold=1 and a datetime with no fold attribute - # otherwise, so we need to return zero to cover the latter case - return getattr(self._datetime, "fold", 0) - - @property - def ambiguous(self): - """ Returns a boolean indicating whether the :class:`Arrow ` object is ambiguous.""" - - return dateutil_tz.datetime_ambiguous(self._datetime) - - @property - def imaginary(self): - """Indicates whether the :class: `Arrow ` object exists in the current timezone.""" - - return not dateutil_tz.datetime_exists(self._datetime) - - # mutation and duplication. - - def clone(self): - """Returns a new :class:`Arrow ` object, cloned from the current one. - - Usage: - - >>> arw = arrow.utcnow() - >>> cloned = arw.clone() - - """ - - return self.fromdatetime(self._datetime) - - def replace(self, **kwargs): - """Returns a new :class:`Arrow ` object with attributes updated - according to inputs. - - Use property names to set their value absolutely:: - - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.replace(year=2014, month=6) - - - You can also replace the timezone without conversion, using a - :ref:`timezone expression `:: - - >>> arw.replace(tzinfo=tz.tzlocal()) - - - """ - - absolute_kwargs = {} - - for key, value in kwargs.items(): - - if key in self._ATTRS: - absolute_kwargs[key] = value - elif key in ["week", "quarter"]: - raise AttributeError("setting absolute {} is not supported".format(key)) - elif key not in ["tzinfo", "fold"]: - raise AttributeError('unknown attribute: "{}"'.format(key)) - - current = self._datetime.replace(**absolute_kwargs) - - tzinfo = kwargs.get("tzinfo") - - if tzinfo is not None: - tzinfo = self._get_tzinfo(tzinfo) - current = current.replace(tzinfo=tzinfo) - - fold = kwargs.get("fold") - - # TODO revisit this once we drop support for 2.7/3.5 - if fold is not None: - current = dateutil_tz.enfold(current, fold=fold) - - return self.fromdatetime(current) - - def shift(self, **kwargs): - """Returns a new :class:`Arrow ` object with attributes updated - according to inputs. - - Use pluralized property names to relatively shift their current value: - - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.shift(years=1, months=-1) - - - Day-of-the-week relative shifting can use either Python's weekday numbers - (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's - day instances (MO, TU .. SU). When using weekday numbers, the returned - date will always be greater than or equal to the starting date. - - Using the above code (which is a Saturday) and asking it to shift to Saturday: - - >>> arw.shift(weekday=5) - - - While asking for a Monday: - - >>> arw.shift(weekday=0) - - - """ - - relative_kwargs = {} - additional_attrs = ["weeks", "quarters", "weekday"] - - for key, value in kwargs.items(): - - if key in self._ATTRS_PLURAL or key in additional_attrs: - relative_kwargs[key] = value - else: - raise AttributeError( - "Invalid shift time frame. Please select one of the following: {}.".format( - ", ".join(self._ATTRS_PLURAL + additional_attrs) - ) - ) - - # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault("months", 0) - relative_kwargs["months"] += ( - relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER - ) - - current = self._datetime + relativedelta(**relative_kwargs) - - if not dateutil_tz.datetime_exists(current): - current = dateutil_tz.resolve_imaginary(current) - - return self.fromdatetime(current) - - def to(self, tz): - """Returns a new :class:`Arrow ` object, converted - to the target timezone. - - :param tz: A :ref:`timezone expression `. - - Usage:: - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc.to('US/Pacific') - - - >>> utc.to(tz.tzlocal()) - - - >>> utc.to('-07:00') - - - >>> utc.to('local') - - - >>> utc.to('local').to('utc') - - - """ - - if not isinstance(tz, dt_tzinfo): - tz = parser.TzinfoParser.parse(tz) - - dt = self._datetime.astimezone(tz) - - return self.__class__( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - # string output and formatting - - def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): - """Returns a string representation of the :class:`Arrow ` object, - formatted according to a format string. - - :param fmt: the format string. - - Usage:: - - >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-09 03:56:47 -00:00' - - >>> arrow.utcnow().format('X') - '1368071882' - - >>> arrow.utcnow().format('MMMM DD, YYYY') - 'May 09, 2013' - - >>> arrow.utcnow().format() - '2013-05-09 03:56:47 -00:00' - - """ - - return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) - - def humanize( - self, other=None, locale="en_us", only_distance=False, granularity="auto" - ): - """Returns a localized, humanized representation of a relative difference in time. - - :param other: (optional) an :class:`Arrow ` or ``datetime`` object. - Defaults to now in the current :class:`Arrow ` object's timezone. - :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. - :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', - 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings - - Usage:: - - >>> earlier = arrow.utcnow().shift(hours=-2) - >>> earlier.humanize() - '2 hours ago' - - >>> later = earlier.shift(hours=4) - >>> later.humanize(earlier) - 'in 4 hours' - - """ - - locale_name = locale - locale = locales.get_locale(locale) - - if other is None: - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) - dt = utc.astimezone(self._datetime.tzinfo) - - elif isinstance(other, Arrow): - dt = other._datetime - - elif isinstance(other, datetime): - if other.tzinfo is None: - dt = other.replace(tzinfo=self._datetime.tzinfo) - else: - dt = other.astimezone(self._datetime.tzinfo) - - else: - raise TypeError( - "Invalid 'other' argument of type '{}'. " - "Argument must be of type None, Arrow, or datetime.".format( - type(other).__name__ - ) - ) - - if isinstance(granularity, list) and len(granularity) == 1: - granularity = granularity[0] - - delta = int(round(util.total_seconds(self._datetime - dt))) - sign = -1 if delta < 0 else 1 - diff = abs(delta) - delta = diff - - try: - if granularity == "auto": - if diff < 10: - return locale.describe("now", only_distance=only_distance) - - if diff < 45: - seconds = sign * delta - return locale.describe( - "seconds", seconds, only_distance=only_distance - ) - - elif diff < 90: - return locale.describe("minute", sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) - return locale.describe( - "minutes", minutes, only_distance=only_distance - ) - - elif diff < 5400: - return locale.describe("hour", sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) - return locale.describe("hours", hours, only_distance=only_distance) - - # anything less than 48 hours should be 1 day - elif diff < 172800: - return locale.describe("day", sign, only_distance=only_distance) - elif diff < 554400: - days = sign * int(max(delta / 86400, 2)) - return locale.describe("days", days, only_distance=only_distance) - - elif diff < 907200: - return locale.describe("week", sign, only_distance=only_distance) - elif diff < 2419200: - weeks = sign * int(max(delta / 604800, 2)) - return locale.describe("weeks", weeks, only_distance=only_distance) - - elif diff < 3888000: - return locale.describe("month", sign, only_distance=only_distance) - elif diff < 29808000: - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month - - months = sign * int(max(abs(other_months - self_months), 2)) - - return locale.describe( - "months", months, only_distance=only_distance - ) - - elif diff < 47260800: - return locale.describe("year", sign, only_distance=only_distance) - else: - years = sign * int(max(delta / 31536000, 2)) - return locale.describe("years", years, only_distance=only_distance) - - elif util.isstr(granularity): - if granularity == "second": - delta = sign * delta - if abs(delta) < 2: - return locale.describe("now", only_distance=only_distance) - elif granularity == "minute": - delta = sign * delta / self._SECS_PER_MINUTE - elif granularity == "hour": - delta = sign * delta / self._SECS_PER_HOUR - elif granularity == "day": - delta = sign * delta / self._SECS_PER_DAY - elif granularity == "week": - delta = sign * delta / self._SECS_PER_WEEK - elif granularity == "month": - delta = sign * delta / self._SECS_PER_MONTH - elif granularity == "year": - delta = sign * delta / self._SECS_PER_YEAR - else: - raise AttributeError( - "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" - ) - - if trunc(abs(delta)) != 1: - granularity += "s" - return locale.describe(granularity, delta, only_distance=only_distance) - - else: - timeframes = [] - if "year" in granularity: - years = sign * delta / self._SECS_PER_YEAR - delta %= self._SECS_PER_YEAR - timeframes.append(["year", years]) - - if "month" in granularity: - months = sign * delta / self._SECS_PER_MONTH - delta %= self._SECS_PER_MONTH - timeframes.append(["month", months]) - - if "week" in granularity: - weeks = sign * delta / self._SECS_PER_WEEK - delta %= self._SECS_PER_WEEK - timeframes.append(["week", weeks]) - - if "day" in granularity: - days = sign * delta / self._SECS_PER_DAY - delta %= self._SECS_PER_DAY - timeframes.append(["day", days]) - - if "hour" in granularity: - hours = sign * delta / self._SECS_PER_HOUR - delta %= self._SECS_PER_HOUR - timeframes.append(["hour", hours]) - - if "minute" in granularity: - minutes = sign * delta / self._SECS_PER_MINUTE - delta %= self._SECS_PER_MINUTE - timeframes.append(["minute", minutes]) - - if "second" in granularity: - seconds = sign * delta - timeframes.append(["second", seconds]) - - if len(timeframes) < len(granularity): - raise AttributeError( - "Invalid level of granularity. " - "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." - ) - - for tf in timeframes: - # Make granularity plural if the delta is not equal to 1 - if trunc(abs(tf[1])) != 1: - tf[0] += "s" - return locale.describe_multi(timeframes, only_distance=only_distance) - - except KeyError as e: - raise ValueError( - "Humanization of the {} granularity is not currently translated in the '{}' locale. " - "Please consider making a contribution to this locale.".format( - e, locale_name - ) - ) - - # query functions - - def is_between(self, start, end, bounds="()"): - """Returns a boolean denoting whether the specified date and time is between - the start and end dates and times. - - :param start: an :class:`Arrow ` object. - :param end: an :class:`Arrow ` object. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the range. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '()' is used. - - Usage:: - - >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10)) - >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36)) - >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end) - True - - >>> start = arrow.get(datetime(2013, 5, 5)) - >>> end = arrow.get(datetime(2013, 5, 8)) - >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]') - True - - >>> start = arrow.get(datetime(2013, 5, 5)) - >>> end = arrow.get(datetime(2013, 5, 8)) - >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') - False - - """ - - util.validate_bounds(bounds) - - if not isinstance(start, Arrow): - raise TypeError( - "Can't parse start date argument type of '{}'".format(type(start)) - ) - - if not isinstance(end, Arrow): - raise TypeError( - "Can't parse end date argument type of '{}'".format(type(end)) - ) - - include_start = bounds[0] == "[" - include_end = bounds[1] == "]" - - target_timestamp = self.float_timestamp - start_timestamp = start.float_timestamp - end_timestamp = end.float_timestamp - - if include_start and include_end: - return ( - target_timestamp >= start_timestamp - and target_timestamp <= end_timestamp - ) - elif include_start and not include_end: - return ( - target_timestamp >= start_timestamp and target_timestamp < end_timestamp - ) - elif not include_start and include_end: - return ( - target_timestamp > start_timestamp and target_timestamp <= end_timestamp - ) - else: - return ( - target_timestamp > start_timestamp and target_timestamp < end_timestamp - ) - - # datetime methods - - def date(self): - """Returns a ``date`` object with the same year, month and day. - - Usage:: - - >>> arrow.utcnow().date() - datetime.date(2019, 1, 23) - - """ - - return self._datetime.date() - - def time(self): - """Returns a ``time`` object with the same hour, minute, second, microsecond. - - Usage:: - - >>> arrow.utcnow().time() - datetime.time(12, 15, 34, 68352) - - """ - - return self._datetime.time() - - def timetz(self): - """Returns a ``time`` object with the same hour, minute, second, microsecond and - tzinfo. - - Usage:: - - >>> arrow.utcnow().timetz() - datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) - - """ - - return self._datetime.timetz() - - def astimezone(self, tz): - """Returns a ``datetime`` object, converted to the specified timezone. - - :param tz: a ``tzinfo`` object. - - Usage:: - - >>> pacific=arrow.now('US/Pacific') - >>> nyc=arrow.now('America/New_York').tzinfo - >>> pacific.astimezone(nyc) - datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) - - """ - - return self._datetime.astimezone(tz) - - def utcoffset(self): - """Returns a ``timedelta`` object representing the whole number of minutes difference from - UTC time. - - Usage:: - - >>> arrow.now('US/Pacific').utcoffset() - datetime.timedelta(-1, 57600) - - """ - - return self._datetime.utcoffset() - - def dst(self): - """Returns the daylight savings time adjustment. - - Usage:: - - >>> arrow.utcnow().dst() - datetime.timedelta(0) - - """ - - return self._datetime.dst() - - def timetuple(self): - """Returns a ``time.struct_time``, in the current timezone. - - Usage:: - - >>> arrow.utcnow().timetuple() - time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) - - """ - - return self._datetime.timetuple() - - def utctimetuple(self): - """Returns a ``time.struct_time``, in UTC time. - - Usage:: - - >>> arrow.utcnow().utctimetuple() - time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) - - """ - - return self._datetime.utctimetuple() - - def toordinal(self): - """Returns the proleptic Gregorian ordinal of the date. - - Usage:: - - >>> arrow.utcnow().toordinal() - 737078 - - """ - - return self._datetime.toordinal() - - def weekday(self): - """Returns the day of the week as an integer (0-6). - - Usage:: - - >>> arrow.utcnow().weekday() - 5 - - """ - - return self._datetime.weekday() - - def isoweekday(self): - """Returns the ISO day of the week as an integer (1-7). - - Usage:: - - >>> arrow.utcnow().isoweekday() - 6 - - """ - - return self._datetime.isoweekday() - - def isocalendar(self): - """Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). - - Usage:: - - >>> arrow.utcnow().isocalendar() - (2019, 3, 6) - - """ - - return self._datetime.isocalendar() - - def isoformat(self, sep="T"): - """Returns an ISO 8601 formatted representation of the date and time. - - Usage:: - - >>> arrow.utcnow().isoformat() - '2019-01-19T18:30:52.442118+00:00' - - """ - - return self._datetime.isoformat(sep) - - def ctime(self): - """Returns a ctime formatted representation of the date and time. - - Usage:: - - >>> arrow.utcnow().ctime() - 'Sat Jan 19 18:26:50 2019' - - """ - - return self._datetime.ctime() - - def strftime(self, format): - """Formats in the style of ``datetime.strftime``. - - :param format: the format string. - - Usage:: - - >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') - '23-01-2019 12:28:17' - - """ - - return self._datetime.strftime(format) - - def for_json(self): - """Serializes for the ``for_json`` protocol of simplejson. - - Usage:: - - >>> arrow.utcnow().for_json() - '2019-01-19T18:25:36.760079+00:00' - - """ - - return self.isoformat() - - # math - - def __add__(self, other): - - if isinstance(other, (timedelta, relativedelta)): - return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) - - return NotImplemented - - def __radd__(self, other): - return self.__add__(other) - - def __sub__(self, other): - - if isinstance(other, (timedelta, relativedelta)): - return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) - - elif isinstance(other, datetime): - return self._datetime - other - - elif isinstance(other, Arrow): - return self._datetime - other._datetime - - return NotImplemented - - def __rsub__(self, other): - - if isinstance(other, datetime): - return other - self._datetime - - return NotImplemented - - # comparisons - - def __eq__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return False - - return self._datetime == self._get_datetime(other) - - def __ne__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return True - - return not self.__eq__(other) - - def __gt__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime > self._get_datetime(other) - - def __ge__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime >= self._get_datetime(other) - - def __lt__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime < self._get_datetime(other) - - def __le__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime <= self._get_datetime(other) - - def __cmp__(self, other): - if sys.version_info[0] < 3: # pragma: no cover - if not isinstance(other, (Arrow, datetime)): - raise TypeError( - "can't compare '{}' to '{}'".format(type(self), type(other)) - ) - - # internal methods - - @staticmethod - def _get_tzinfo(tz_expr): - - if tz_expr is None: - return dateutil_tz.tzutc() - if isinstance(tz_expr, dt_tzinfo): - return tz_expr - else: - try: - return parser.TzinfoParser.parse(tz_expr) - except parser.ParserError: - raise ValueError("'{}' not recognized as a timezone".format(tz_expr)) - - @classmethod - def _get_datetime(cls, expr): - """Get datetime object for a specified expression.""" - if isinstance(expr, Arrow): - return expr.datetime - elif isinstance(expr, datetime): - return expr - elif util.is_timestamp(expr): - timestamp = float(expr) - return cls.utcfromtimestamp(timestamp).datetime - else: - raise ValueError( - "'{}' not recognized as a datetime or timestamp.".format(expr) - ) - - @classmethod - def _get_frames(cls, name): - - if name in cls._ATTRS: - return name, "{}s".format(name), 1 - elif name[-1] == "s" and name[:-1] in cls._ATTRS: - return name[:-1], name, 1 - elif name in ["week", "weeks"]: - return "week", "weeks", 1 - elif name in ["quarter", "quarters"]: - return "quarter", "months", 3 - - supported = ", ".join( - [ - "year(s)", - "month(s)", - "day(s)", - "hour(s)", - "minute(s)", - "second(s)", - "microsecond(s)", - "week(s)", - "quarter(s)", - ] - ) - raise AttributeError( - "range/span over frame {} not supported. Supported frames: {}".format( - name, supported - ) - ) - - @classmethod - def _get_iteration_params(cls, end, limit): - - if end is None: - - if limit is None: - raise ValueError("one of 'end' or 'limit' is required") - - return cls.max, limit - - else: - if limit is None: - return end, sys.maxsize - return end, limit - - @staticmethod - def _is_last_day_of_month(date): - return date.day == calendar.monthrange(date.year, date.month)[1] - - -Arrow.min = Arrow.fromdatetime(datetime.min) -Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/client/ayon_core/vendor/python/python_2/arrow/constants.py b/client/ayon_core/vendor/python/python_2/arrow/constants.py deleted file mode 100644 index 81e37b26de..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/constants.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- - -# Output of time.mktime(datetime.max.timetuple()) on macOS -# This value must be hardcoded for compatibility with Windows -# Platform-independent max timestamps are hard to form -# https://stackoverflow.com/q/46133223 -MAX_TIMESTAMP = 253402318799.0 -MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 -MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 diff --git a/client/ayon_core/vendor/python/python_2/arrow/factory.py b/client/ayon_core/vendor/python/python_2/arrow/factory.py deleted file mode 100644 index 05933e8151..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/factory.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Implements the :class:`ArrowFactory ` class, -providing factory methods for common :class:`Arrow ` -construction scenarios. - -""" - -from __future__ import absolute_import - -import calendar -from datetime import date, datetime -from datetime import tzinfo as dt_tzinfo -from time import struct_time - -from dateutil import tz as dateutil_tz - -from arrow import parser -from arrow.arrow import Arrow -from arrow.util import is_timestamp, iso_to_gregorian, isstr - - -class ArrowFactory(object): - """A factory for generating :class:`Arrow ` objects. - - :param type: (optional) the :class:`Arrow `-based class to construct from. - Defaults to :class:`Arrow `. - - """ - - def __init__(self, type=Arrow): - self.type = type - - def get(self, *args, **kwargs): - """Returns an :class:`Arrow ` object based on flexible inputs. - - :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'. - :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. - Replaces the timezone unless using an input form that is explicitly UTC or specifies - the timezone in a positional argument. Defaults to UTC. - :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize - redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing. - Defaults to false. - - Usage:: - - >>> import arrow - - **No inputs** to get current UTC time:: - - >>> arrow.get() - - - **None** to also get current UTC time:: - - >>> arrow.get(None) - - - **One** :class:`Arrow ` object, to get a copy. - - >>> arw = arrow.utcnow() - >>> arrow.get(arw) - - - **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get - that timestamp in UTC:: - - >>> arrow.get(1367992474.293378) - - - >>> arrow.get(1367992474) - - - **One** ISO 8601-formatted ``str``, to parse it:: - - >>> arrow.get('2013-09-29T01:26:43.830580') - - - **One** ISO 8601-formatted ``str``, in basic format, to parse it:: - - >>> arrow.get('20160413T133656.456289') - - - **One** ``tzinfo``, to get the current time **converted** to that timezone:: - - >>> arrow.get(tz.tzlocal()) - - - **One** naive ``datetime``, to get that datetime in UTC:: - - >>> arrow.get(datetime(2013, 5, 5)) - - - **One** aware ``datetime``, to get that datetime:: - - >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal())) - - - **One** naive ``date``, to get that date in UTC:: - - >>> arrow.get(date(2013, 5, 5)) - - - **One** time.struct time:: - - >>> arrow.get(gmtime(0)) - - - **One** iso calendar ``tuple``, to get that week date in UTC:: - - >>> arrow.get((2013, 18, 7)) - - - **Two** arguments, a naive or aware ``datetime``, and a replacement - :ref:`timezone expression `:: - - >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - - - **Two** arguments, a naive ``date``, and a replacement - :ref:`timezone expression `:: - - >>> arrow.get(date(2013, 5, 5), 'US/Pacific') - - - **Two** arguments, both ``str``, to parse the first according to the format of the second:: - - >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ') - - - **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: - - >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) - - - **Three or more** arguments, as for the constructor of a ``datetime``:: - - >>> arrow.get(2013, 5, 5, 12, 30, 45) - - - """ - - arg_count = len(args) - locale = kwargs.pop("locale", "en_us") - tz = kwargs.get("tzinfo", None) - normalize_whitespace = kwargs.pop("normalize_whitespace", False) - - # if kwargs given, send to constructor unless only tzinfo provided - if len(kwargs) > 1: - arg_count = 3 - - # tzinfo kwarg is not provided - if len(kwargs) == 1 and tz is None: - arg_count = 3 - - # () -> now, @ utc. - if arg_count == 0: - if isstr(tz): - tz = parser.TzinfoParser.parse(tz) - return self.type.now(tz) - - if isinstance(tz, dt_tzinfo): - return self.type.now(tz) - - return self.type.utcnow() - - if arg_count == 1: - arg = args[0] - - # (None) -> now, @ utc. - if arg is None: - return self.type.utcnow() - - # try (int, float) -> from timestamp with tz - elif not isstr(arg) and is_timestamp(arg): - if tz is None: - # set to UTC by default - tz = dateutil_tz.tzutc() - return self.type.fromtimestamp(arg, tzinfo=tz) - - # (Arrow) -> from the object's datetime. - elif isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime) - - # (datetime) -> from datetime. - elif isinstance(arg, datetime): - return self.type.fromdatetime(arg) - - # (date) -> from date. - elif isinstance(arg, date): - return self.type.fromdate(arg) - - # (tzinfo) -> now, @ tzinfo. - elif isinstance(arg, dt_tzinfo): - return self.type.now(arg) - - # (str) -> parse. - elif isstr(arg): - dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) - return self.type.fromdatetime(dt, tz) - - # (struct_time) -> from struct_time - elif isinstance(arg, struct_time): - return self.type.utcfromtimestamp(calendar.timegm(arg)) - - # (iso calendar) -> convert then from date - elif isinstance(arg, tuple) and len(arg) == 3: - dt = iso_to_gregorian(*arg) - return self.type.fromdate(dt) - - else: - raise TypeError( - "Can't parse single argument of type '{}'".format(type(arg)) - ) - - elif arg_count == 2: - - arg_1, arg_2 = args[0], args[1] - - if isinstance(arg_1, datetime): - - # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): - return self.type.fromdatetime(arg_1, arg_2) - else: - raise TypeError( - "Can't parse two arguments of types 'datetime', '{}'".format( - type(arg_2) - ) - ) - - elif isinstance(arg_1, date): - - # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): - return self.type.fromdate(arg_1, tzinfo=arg_2) - else: - raise TypeError( - "Can't parse two arguments of types 'date', '{}'".format( - type(arg_2) - ) - ) - - # (str, format) -> parse. - elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): - dt = parser.DateTimeParser(locale).parse( - args[0], args[1], normalize_whitespace - ) - return self.type.fromdatetime(dt, tzinfo=tz) - - else: - raise TypeError( - "Can't parse two arguments of types '{}' and '{}'".format( - type(arg_1), type(arg_2) - ) - ) - - # 3+ args -> datetime-like via constructor. - else: - return self.type(*args, **kwargs) - - def utcnow(self): - """Returns an :class:`Arrow ` object, representing "now" in UTC time. - - Usage:: - - >>> import arrow - >>> arrow.utcnow() - - """ - - return self.type.utcnow() - - def now(self, tz=None): - """Returns an :class:`Arrow ` object, representing "now" in the given - timezone. - - :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. - - Usage:: - - >>> import arrow - >>> arrow.now() - - - >>> arrow.now('US/Pacific') - - - >>> arrow.now('+02:00') - - - >>> arrow.now('local') - - """ - - if tz is None: - tz = dateutil_tz.tzlocal() - elif not isinstance(tz, dt_tzinfo): - tz = parser.TzinfoParser.parse(tz) - - return self.type.now(tz) diff --git a/client/ayon_core/vendor/python/python_2/arrow/formatter.py b/client/ayon_core/vendor/python/python_2/arrow/formatter.py deleted file mode 100644 index 9f9d7a44da..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/formatter.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division - -import calendar -import re - -from dateutil import tz as dateutil_tz - -from arrow import locales, util - -FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" -FORMAT_RFC822 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC850 = "dddd, DD-MMM-YY HH:mm:ss ZZZ" -FORMAT_RFC1036 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC1123 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC3339 = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_RSS = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ" - - -class DateTimeFormatter(object): - - # This pattern matches characters enclosed in square brackets are matched as - # an atomic group. For more info on atomic groups and how to they are - # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 - - _FORMAT_RE = re.compile( - r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" - ) - - def __init__(self, locale="en_us"): - - self.locale = locales.get_locale(locale) - - def format(cls, dt, fmt): - - return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt) - - def _format_token(self, dt, token): - - if token and token.startswith("[") and token.endswith("]"): - return token[1:-1] - - if token == "YYYY": - return self.locale.year_full(dt.year) - if token == "YY": - return self.locale.year_abbreviation(dt.year) - - if token == "MMMM": - return self.locale.month_name(dt.month) - if token == "MMM": - return self.locale.month_abbreviation(dt.month) - if token == "MM": - return "{:02d}".format(dt.month) - if token == "M": - return str(dt.month) - - if token == "DDDD": - return "{:03d}".format(dt.timetuple().tm_yday) - if token == "DDD": - return str(dt.timetuple().tm_yday) - if token == "DD": - return "{:02d}".format(dt.day) - if token == "D": - return str(dt.day) - - if token == "Do": - return self.locale.ordinal_number(dt.day) - - if token == "dddd": - return self.locale.day_name(dt.isoweekday()) - if token == "ddd": - return self.locale.day_abbreviation(dt.isoweekday()) - if token == "d": - return str(dt.isoweekday()) - - if token == "HH": - return "{:02d}".format(dt.hour) - if token == "H": - return str(dt.hour) - if token == "hh": - return "{:02d}".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - if token == "h": - return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - - if token == "mm": - return "{:02d}".format(dt.minute) - if token == "m": - return str(dt.minute) - - if token == "ss": - return "{:02d}".format(dt.second) - if token == "s": - return str(dt.second) - - if token == "SSSSSS": - return str("{:06d}".format(int(dt.microsecond))) - if token == "SSSSS": - return str("{:05d}".format(int(dt.microsecond / 10))) - if token == "SSSS": - return str("{:04d}".format(int(dt.microsecond / 100))) - if token == "SSS": - return str("{:03d}".format(int(dt.microsecond / 1000))) - if token == "SS": - return str("{:02d}".format(int(dt.microsecond / 10000))) - if token == "S": - return str(int(dt.microsecond / 100000)) - - if token == "X": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - return str(calendar.timegm(dt.utctimetuple())) - - if token == "x": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000) - return str(int(ts * 1000000)) - - if token == "ZZZ": - return dt.tzname() - - if token in ["ZZ", "Z"]: - separator = ":" if token == "ZZ" else "" - tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo - total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) - - sign = "+" if total_minutes >= 0 else "-" - total_minutes = abs(total_minutes) - hour, minute = divmod(total_minutes, 60) - - return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) - - if token in ("a", "A"): - return self.locale.meridian(dt.hour, token) - - if token == "W": - year, week, day = dt.isocalendar() - return "{}-W{:02d}-{}".format(year, week, day) diff --git a/client/ayon_core/vendor/python/python_2/arrow/locales.py b/client/ayon_core/vendor/python/python_2/arrow/locales.py deleted file mode 100644 index 6833da5a78..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/locales.py +++ /dev/null @@ -1,4267 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import inspect -import sys -from math import trunc - - -def get_locale(name): - """Returns an appropriate :class:`Locale ` - corresponding to an inpute locale name. - - :param name: the name of the locale. - - """ - - locale_cls = _locales.get(name.lower()) - - if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) - - return locale_cls() - - -def get_locale_by_class_name(name): - """Returns an appropriate :class:`Locale ` - corresponding to an locale class name. - - :param name: the name of the locale class. - - """ - locale_cls = globals().get(name) - - if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) - - return locale_cls() - - -# base locale type. - - -class Locale(object): - """ Represents locale-specific data and functionality. """ - - names = [] - - timeframes = { - "now": "", - "second": "", - "seconds": "", - "minute": "", - "minutes": "", - "hour": "", - "hours": "", - "day": "", - "days": "", - "week": "", - "weeks": "", - "month": "", - "months": "", - "year": "", - "years": "", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - past = None - future = None - and_word = None - - month_names = [] - month_abbreviations = [] - - day_names = [] - day_abbreviations = [] - - ordinal_day_re = r"(\d+)" - - def __init__(self): - - self._month_name_to_ordinal = None - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - humanized = self._format_timeframe(timeframe, delta) - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - def describe_multi(self, timeframes, only_distance=False): - """Describes a delta within multiple timeframes in plain language. - - :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords - """ - - humanized = "" - for index, (timeframe, delta) in enumerate(timeframes): - humanized += self._format_timeframe(timeframe, delta) - if index == len(timeframes) - 2 and self.and_word: - humanized += " " + self.and_word + " " - elif index < len(timeframes) - 1: - humanized += " " - - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - def day_name(self, day): - """Returns the day name for a specified day of the week. - - :param day: the ``int`` day of the week (1-7). - - """ - - return self.day_names[day] - - def day_abbreviation(self, day): - """Returns the day abbreviation for a specified day of the week. - - :param day: the ``int`` day of the week (1-7). - - """ - - return self.day_abbreviations[day] - - def month_name(self, month): - """Returns the month name for a specified month of the year. - - :param month: the ``int`` month of the year (1-12). - - """ - - return self.month_names[month] - - def month_abbreviation(self, month): - """Returns the month abbreviation for a specified month of the year. - - :param month: the ``int`` month of the year (1-12). - - """ - - return self.month_abbreviations[month] - - def month_number(self, name): - """Returns the month number for a month specified by name or abbreviation. - - :param name: the month name or abbreviation. - - """ - - if self._month_name_to_ordinal is None: - self._month_name_to_ordinal = self._name_to_ordinal(self.month_names) - self._month_name_to_ordinal.update( - self._name_to_ordinal(self.month_abbreviations) - ) - - return self._month_name_to_ordinal.get(name) - - def year_full(self, year): - """Returns the year for specific locale if available - - :param name: the ``int`` year (4-digit) - """ - return "{:04d}".format(year) - - def year_abbreviation(self, year): - """Returns the year for specific locale if available - - :param name: the ``int`` year (4-digit) - """ - return "{:04d}".format(year)[2:] - - def meridian(self, hour, token): - """Returns the meridian indicator for a specified hour and format token. - - :param hour: the ``int`` hour of the day. - :param token: the format token. - """ - - if token == "a": - return self.meridians["am"] if hour < 12 else self.meridians["pm"] - if token == "A": - return self.meridians["AM"] if hour < 12 else self.meridians["PM"] - - def ordinal_number(self, n): - """Returns the ordinal format of a given integer - - :param n: an integer - """ - return self._ordinal_number(n) - - def _ordinal_number(self, n): - return "{}".format(n) - - def _name_to_ordinal(self, lst): - return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) - - def _format_timeframe(self, timeframe, delta): - return self.timeframes[timeframe].format(trunc(abs(delta))) - - def _format_relative(self, humanized, timeframe, delta): - - if timeframe == "now": - return humanized - - direction = self.past if delta < 0 else self.future - - return direction.format(humanized) - - -# base locale type implementations. - - -class EnglishLocale(Locale): - - names = [ - "en", - "en_us", - "en_gb", - "en_au", - "en_be", - "en_jp", - "en_za", - "en_ca", - "en_ph", - ] - - past = "{0} ago" - future = "in {0}" - and_word = "and" - - timeframes = { - "now": "just now", - "second": "a second", - "seconds": "{0} seconds", - "minute": "a minute", - "minutes": "{0} minutes", - "hour": "an hour", - "hours": "{0} hours", - "day": "a day", - "days": "{0} days", - "week": "a week", - "weeks": "{0} weeks", - "month": "a month", - "months": "{0} months", - "year": "a year", - "years": "{0} years", - } - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - month_names = [ - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ] - - day_names = [ - "", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - - ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))" - - def _ordinal_number(self, n): - if n % 100 not in (11, 12, 13): - remainder = abs(n) % 10 - if remainder == 1: - return "{}st".format(n) - elif remainder == 2: - return "{}nd".format(n) - elif remainder == 3: - return "{}rd".format(n) - return "{}th".format(n) - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) - if only_distance and timeframe == "now": - humanized = "instantly" - - return humanized - - -class ItalianLocale(Locale): - names = ["it", "it_it"] - past = "{0} fa" - future = "tra {0}" - and_word = "e" - - timeframes = { - "now": "adesso", - "second": "un secondo", - "seconds": "{0} qualche secondo", - "minute": "un minuto", - "minutes": "{0} minuti", - "hour": "un'ora", - "hours": "{0} ore", - "day": "un giorno", - "days": "{0} giorni", - "week": "una settimana,", - "weeks": "{0} settimane", - "month": "un mese", - "months": "{0} mesi", - "year": "un anno", - "years": "{0} anni", - } - - month_names = [ - "", - "gennaio", - "febbraio", - "marzo", - "aprile", - "maggio", - "giugno", - "luglio", - "agosto", - "settembre", - "ottobre", - "novembre", - "dicembre", - ] - month_abbreviations = [ - "", - "gen", - "feb", - "mar", - "apr", - "mag", - "giu", - "lug", - "ago", - "set", - "ott", - "nov", - "dic", - ] - - day_names = [ - "", - "lunedì", - "martedì", - "mercoledì", - "giovedì", - "venerdì", - "sabato", - "domenica", - ] - day_abbreviations = ["", "lun", "mar", "mer", "gio", "ven", "sab", "dom"] - - ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - - def _ordinal_number(self, n): - return "{}º".format(n) - - -class SpanishLocale(Locale): - names = ["es", "es_es"] - past = "hace {0}" - future = "en {0}" - and_word = "y" - - timeframes = { - "now": "ahora", - "second": "un segundo", - "seconds": "{0} segundos", - "minute": "un minuto", - "minutes": "{0} minutos", - "hour": "una hora", - "hours": "{0} horas", - "day": "un día", - "days": "{0} días", - "week": "una semana", - "weeks": "{0} semanas", - "month": "un mes", - "months": "{0} meses", - "year": "un año", - "years": "{0} años", - } - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - month_names = [ - "", - "enero", - "febrero", - "marzo", - "abril", - "mayo", - "junio", - "julio", - "agosto", - "septiembre", - "octubre", - "noviembre", - "diciembre", - ] - month_abbreviations = [ - "", - "ene", - "feb", - "mar", - "abr", - "may", - "jun", - "jul", - "ago", - "sep", - "oct", - "nov", - "dic", - ] - - day_names = [ - "", - "lunes", - "martes", - "miércoles", - "jueves", - "viernes", - "sábado", - "domingo", - ] - day_abbreviations = ["", "lun", "mar", "mie", "jue", "vie", "sab", "dom"] - - ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - - def _ordinal_number(self, n): - return "{}º".format(n) - - -class FrenchBaseLocale(Locale): - - past = "il y a {0}" - future = "dans {0}" - and_word = "et" - - timeframes = { - "now": "maintenant", - "second": "une seconde", - "seconds": "{0} quelques secondes", - "minute": "une minute", - "minutes": "{0} minutes", - "hour": "une heure", - "hours": "{0} heures", - "day": "un jour", - "days": "{0} jours", - "week": "une semaine", - "weeks": "{0} semaines", - "month": "un mois", - "months": "{0} mois", - "year": "un an", - "years": "{0} ans", - } - - month_names = [ - "", - "janvier", - "février", - "mars", - "avril", - "mai", - "juin", - "juillet", - "août", - "septembre", - "octobre", - "novembre", - "décembre", - ] - - day_names = [ - "", - "lundi", - "mardi", - "mercredi", - "jeudi", - "vendredi", - "samedi", - "dimanche", - ] - day_abbreviations = ["", "lun", "mar", "mer", "jeu", "ven", "sam", "dim"] - - ordinal_day_re = ( - r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)" - ) - - def _ordinal_number(self, n): - if abs(n) == 1: - return "{}er".format(n) - return "{}e".format(n) - - -class FrenchLocale(FrenchBaseLocale, Locale): - - names = ["fr", "fr_fr"] - - month_abbreviations = [ - "", - "janv", - "févr", - "mars", - "avr", - "mai", - "juin", - "juil", - "août", - "sept", - "oct", - "nov", - "déc", - ] - - -class FrenchCanadianLocale(FrenchBaseLocale, Locale): - - names = ["fr_ca"] - - month_abbreviations = [ - "", - "janv", - "févr", - "mars", - "avr", - "mai", - "juin", - "juill", - "août", - "sept", - "oct", - "nov", - "déc", - ] - - -class GreekLocale(Locale): - - names = ["el", "el_gr"] - - past = "{0} πριν" - future = "σε {0}" - and_word = "και" - - timeframes = { - "now": "τώρα", - "second": "ένα δεύτερο", - "seconds": "{0} δευτερόλεπτα", - "minute": "ένα λεπτό", - "minutes": "{0} λεπτά", - "hour": "μία ώρα", - "hours": "{0} ώρες", - "day": "μία μέρα", - "days": "{0} μέρες", - "month": "ένα μήνα", - "months": "{0} μήνες", - "year": "ένα χρόνο", - "years": "{0} χρόνια", - } - - month_names = [ - "", - "Ιανουαρίου", - "Φεβρουαρίου", - "Μαρτίου", - "Απριλίου", - "Μαΐου", - "Ιουνίου", - "Ιουλίου", - "Αυγούστου", - "Σεπτεμβρίου", - "Οκτωβρίου", - "Νοεμβρίου", - "Δεκεμβρίου", - ] - month_abbreviations = [ - "", - "Ιαν", - "Φεβ", - "Μαρ", - "Απρ", - "Μαϊ", - "Ιον", - "Ιολ", - "Αυγ", - "Σεπ", - "Οκτ", - "Νοε", - "Δεκ", - ] - - day_names = [ - "", - "Δευτέρα", - "Τρίτη", - "Τετάρτη", - "Πέμπτη", - "Παρασκευή", - "Σάββατο", - "Κυριακή", - ] - day_abbreviations = ["", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"] - - -class JapaneseLocale(Locale): - - names = ["ja", "ja_jp"] - - past = "{0}前" - future = "{0}後" - - timeframes = { - "now": "現在", - "second": "二番目の", - "seconds": "{0}数秒", - "minute": "1分", - "minutes": "{0}分", - "hour": "1時間", - "hours": "{0}時間", - "day": "1日", - "days": "{0}日", - "week": "1週間", - "weeks": "{0}週間", - "month": "1ヶ月", - "months": "{0}ヶ月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] - day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] - - -class SwedishLocale(Locale): - - names = ["sv", "sv_se"] - - past = "för {0} sen" - future = "om {0}" - and_word = "och" - - timeframes = { - "now": "just nu", - "second": "en sekund", - "seconds": "{0} några sekunder", - "minute": "en minut", - "minutes": "{0} minuter", - "hour": "en timme", - "hours": "{0} timmar", - "day": "en dag", - "days": "{0} dagar", - "week": "en vecka", - "weeks": "{0} veckor", - "month": "en månad", - "months": "{0} månader", - "year": "ett år", - "years": "{0} år", - } - - month_names = [ - "", - "januari", - "februari", - "mars", - "april", - "maj", - "juni", - "juli", - "augusti", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "måndag", - "tisdag", - "onsdag", - "torsdag", - "fredag", - "lördag", - "söndag", - ] - day_abbreviations = ["", "mån", "tis", "ons", "tor", "fre", "lör", "sön"] - - -class FinnishLocale(Locale): - - names = ["fi", "fi_fi"] - - # The finnish grammar is very complex, and its hard to convert - # 1-to-1 to something like English. - - past = "{0} sitten" - future = "{0} kuluttua" - - timeframes = { - "now": ["juuri nyt", "juuri nyt"], - "second": ["sekunti", "sekunti"], - "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], - "minute": ["minuutti", "minuutin"], - "minutes": ["{0} minuuttia", "{0} minuutin"], - "hour": ["tunti", "tunnin"], - "hours": ["{0} tuntia", "{0} tunnin"], - "day": ["päivä", "päivä"], - "days": ["{0} päivää", "{0} päivän"], - "month": ["kuukausi", "kuukauden"], - "months": ["{0} kuukautta", "{0} kuukauden"], - "year": ["vuosi", "vuoden"], - "years": ["{0} vuotta", "{0} vuoden"], - } - - # Months and days are lowercase in Finnish - month_names = [ - "", - "tammikuu", - "helmikuu", - "maaliskuu", - "huhtikuu", - "toukokuu", - "kesäkuu", - "heinäkuu", - "elokuu", - "syyskuu", - "lokakuu", - "marraskuu", - "joulukuu", - ] - - month_abbreviations = [ - "", - "tammi", - "helmi", - "maalis", - "huhti", - "touko", - "kesä", - "heinä", - "elo", - "syys", - "loka", - "marras", - "joulu", - ] - - day_names = [ - "", - "maanantai", - "tiistai", - "keskiviikko", - "torstai", - "perjantai", - "lauantai", - "sunnuntai", - ] - - day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] - - def _format_timeframe(self, timeframe, delta): - return ( - self.timeframes[timeframe][0].format(abs(delta)), - self.timeframes[timeframe][1].format(abs(delta)), - ) - - def _format_relative(self, humanized, timeframe, delta): - if timeframe == "now": - return humanized[0] - - direction = self.past if delta < 0 else self.future - which = 0 if delta < 0 else 1 - - return direction.format(humanized[which]) - - def _ordinal_number(self, n): - return "{}.".format(n) - - -class ChineseCNLocale(Locale): - - names = ["zh", "zh_cn"] - - past = "{0}前" - future = "{0}后" - - timeframes = { - "now": "刚才", - "second": "一秒", - "seconds": "{0}秒", - "minute": "1分钟", - "minutes": "{0}分钟", - "hour": "1小时", - "hours": "{0}小时", - "day": "1天", - "days": "{0}天", - "week": "一周", - "weeks": "{0}周", - "month": "1个月", - "months": "{0}个月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "一月", - "二月", - "三月", - "四月", - "五月", - "六月", - "七月", - "八月", - "九月", - "十月", - "十一月", - "十二月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class ChineseTWLocale(Locale): - - names = ["zh_tw"] - - past = "{0}前" - future = "{0}後" - and_word = "和" - - timeframes = { - "now": "剛才", - "second": "1秒", - "seconds": "{0}秒", - "minute": "1分鐘", - "minutes": "{0}分鐘", - "hour": "1小時", - "hours": "{0}小時", - "day": "1天", - "days": "{0}天", - "week": "1週", - "weeks": "{0}週", - "month": "1個月", - "months": "{0}個月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "週一", "週二", "週三", "週四", "週五", "週六", "週日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class HongKongLocale(Locale): - - names = ["zh_hk"] - - past = "{0}前" - future = "{0}後" - - timeframes = { - "now": "剛才", - "second": "1秒", - "seconds": "{0}秒", - "minute": "1分鐘", - "minutes": "{0}分鐘", - "hour": "1小時", - "hours": "{0}小時", - "day": "1天", - "days": "{0}天", - "week": "1星期", - "weeks": "{0}星期", - "month": "1個月", - "months": "{0}個月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class KoreanLocale(Locale): - - names = ["ko", "ko_kr"] - - past = "{0} 전" - future = "{0} 후" - - timeframes = { - "now": "지금", - "second": "1초", - "seconds": "{0}초", - "minute": "1분", - "minutes": "{0}분", - "hour": "한시간", - "hours": "{0}시간", - "day": "하루", - "days": "{0}일", - "week": "1주", - "weeks": "{0}주", - "month": "한달", - "months": "{0}개월", - "year": "1년", - "years": "{0}년", - } - - special_dayframes = { - -3: "그끄제", - -2: "그제", - -1: "어제", - 1: "내일", - 2: "모레", - 3: "글피", - 4: "그글피", - } - - special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"} - - month_names = [ - "", - "1월", - "2월", - "3월", - "4월", - "5월", - "6월", - "7월", - "8월", - "9월", - "10월", - "11월", - "12월", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] - day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] - - def _ordinal_number(self, n): - ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] - if n < len(ordinals): - return "{}번째".format(ordinals[n]) - return "{}번째".format(n) - - def _format_relative(self, humanized, timeframe, delta): - if timeframe in ("day", "days"): - special = self.special_dayframes.get(delta) - if special: - return special - elif timeframe in ("year", "years"): - special = self.special_yearframes.get(delta) - if special: - return special - - return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta) - - -# derived locale types & implementations. -class DutchLocale(Locale): - - names = ["nl", "nl_nl"] - - past = "{0} geleden" - future = "over {0}" - - timeframes = { - "now": "nu", - "second": "een seconde", - "seconds": "{0} seconden", - "minute": "een minuut", - "minutes": "{0} minuten", - "hour": "een uur", - "hours": "{0} uur", - "day": "een dag", - "days": "{0} dagen", - "week": "een week", - "weeks": "{0} weken", - "month": "een maand", - "months": "{0} maanden", - "year": "een jaar", - "years": "{0} jaar", - } - - # In Dutch names of months and days are not starting with a capital letter - # like in the English language. - month_names = [ - "", - "januari", - "februari", - "maart", - "april", - "mei", - "juni", - "juli", - "augustus", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mrt", - "apr", - "mei", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "maandag", - "dinsdag", - "woensdag", - "donderdag", - "vrijdag", - "zaterdag", - "zondag", - ] - day_abbreviations = ["", "ma", "di", "wo", "do", "vr", "za", "zo"] - - -class SlavicBaseLocale(Locale): - def _format_timeframe(self, timeframe, delta): - - form = self.timeframes[timeframe] - delta = abs(delta) - - if isinstance(form, list): - - if delta % 10 == 1 and delta % 100 != 11: - form = form[0] - elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[1] - else: - form = form[2] - - return form.format(delta) - - -class BelarusianLocale(SlavicBaseLocale): - - names = ["be", "be_by"] - - past = "{0} таму" - future = "праз {0}" - - timeframes = { - "now": "зараз", - "second": "секунду", - "seconds": "{0} некалькі секунд", - "minute": "хвіліну", - "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], - "hour": "гадзіну", - "hours": ["{0} гадзіну", "{0} гадзіны", "{0} гадзін"], - "day": "дзень", - "days": ["{0} дзень", "{0} дні", "{0} дзён"], - "month": "месяц", - "months": ["{0} месяц", "{0} месяцы", "{0} месяцаў"], - "year": "год", - "years": ["{0} год", "{0} гады", "{0} гадоў"], - } - - month_names = [ - "", - "студзеня", - "лютага", - "сакавіка", - "красавіка", - "траўня", - "чэрвеня", - "ліпеня", - "жніўня", - "верасня", - "кастрычніка", - "лістапада", - "снежня", - ] - month_abbreviations = [ - "", - "студ", - "лют", - "сак", - "крас", - "трав", - "чэрв", - "ліп", - "жнів", - "вер", - "каст", - "ліст", - "снеж", - ] - - day_names = [ - "", - "панядзелак", - "аўторак", - "серада", - "чацвер", - "пятніца", - "субота", - "нядзеля", - ] - day_abbreviations = ["", "пн", "ат", "ср", "чц", "пт", "сб", "нд"] - - -class PolishLocale(SlavicBaseLocale): - - names = ["pl", "pl_pl"] - - past = "{0} temu" - future = "za {0}" - - # The nouns should be in genitive case (Polish: "dopełniacz") - # in order to correctly form `past` & `future` expressions. - timeframes = { - "now": "teraz", - "second": "sekundę", - "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"], - "minute": "minutę", - "minutes": ["{0} minut", "{0} minuty", "{0} minut"], - "hour": "godzinę", - "hours": ["{0} godzin", "{0} godziny", "{0} godzin"], - "day": "dzień", - "days": "{0} dni", - "week": "tydzień", - "weeks": ["{0} tygodni", "{0} tygodnie", "{0} tygodni"], - "month": "miesiąc", - "months": ["{0} miesięcy", "{0} miesiące", "{0} miesięcy"], - "year": "rok", - "years": ["{0} lat", "{0} lata", "{0} lat"], - } - - month_names = [ - "", - "styczeń", - "luty", - "marzec", - "kwiecień", - "maj", - "czerwiec", - "lipiec", - "sierpień", - "wrzesień", - "październik", - "listopad", - "grudzień", - ] - month_abbreviations = [ - "", - "sty", - "lut", - "mar", - "kwi", - "maj", - "cze", - "lip", - "sie", - "wrz", - "paź", - "lis", - "gru", - ] - - day_names = [ - "", - "poniedziałek", - "wtorek", - "środa", - "czwartek", - "piątek", - "sobota", - "niedziela", - ] - day_abbreviations = ["", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"] - - -class RussianLocale(SlavicBaseLocale): - - names = ["ru", "ru_ru"] - - past = "{0} назад" - future = "через {0}" - - timeframes = { - "now": "сейчас", - "second": "Второй", - "seconds": "{0} несколько секунд", - "minute": "минуту", - "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], - "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часов"], - "day": "день", - "days": ["{0} день", "{0} дня", "{0} дней"], - "week": "неделю", - "weeks": ["{0} неделю", "{0} недели", "{0} недель"], - "month": "месяц", - "months": ["{0} месяц", "{0} месяца", "{0} месяцев"], - "year": "год", - "years": ["{0} год", "{0} года", "{0} лет"], - } - - month_names = [ - "", - "января", - "февраля", - "марта", - "апреля", - "мая", - "июня", - "июля", - "августа", - "сентября", - "октября", - "ноября", - "декабря", - ] - month_abbreviations = [ - "", - "янв", - "фев", - "мар", - "апр", - "май", - "июн", - "июл", - "авг", - "сен", - "окт", - "ноя", - "дек", - ] - - day_names = [ - "", - "понедельник", - "вторник", - "среда", - "четверг", - "пятница", - "суббота", - "воскресенье", - ] - day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"] - - -class AfrikaansLocale(Locale): - - names = ["af", "af_nl"] - - past = "{0} gelede" - future = "in {0}" - - timeframes = { - "now": "nou", - "second": "n sekonde", - "seconds": "{0} sekondes", - "minute": "minuut", - "minutes": "{0} minute", - "hour": "uur", - "hours": "{0} ure", - "day": "een dag", - "days": "{0} dae", - "month": "een maand", - "months": "{0} maande", - "year": "een jaar", - "years": "{0} jaar", - } - - month_names = [ - "", - "Januarie", - "Februarie", - "Maart", - "April", - "Mei", - "Junie", - "Julie", - "Augustus", - "September", - "Oktober", - "November", - "Desember", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mrt", - "Apr", - "Mei", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Des", - ] - - day_names = [ - "", - "Maandag", - "Dinsdag", - "Woensdag", - "Donderdag", - "Vrydag", - "Saterdag", - "Sondag", - ] - day_abbreviations = ["", "Ma", "Di", "Wo", "Do", "Vr", "Za", "So"] - - -class BulgarianLocale(SlavicBaseLocale): - - names = ["bg", "bg_BG"] - - past = "{0} назад" - future = "напред {0}" - - timeframes = { - "now": "сега", - "second": "секунда", - "seconds": "{0} няколко секунди", - "minute": "минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], - "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часа"], - "day": "ден", - "days": ["{0} ден", "{0} дни", "{0} дни"], - "month": "месец", - "months": ["{0} месец", "{0} месеца", "{0} месеца"], - "year": "година", - "years": ["{0} година", "{0} години", "{0} години"], - } - - month_names = [ - "", - "януари", - "февруари", - "март", - "април", - "май", - "юни", - "юли", - "август", - "септември", - "октомври", - "ноември", - "декември", - ] - month_abbreviations = [ - "", - "ян", - "февр", - "март", - "апр", - "май", - "юни", - "юли", - "авг", - "септ", - "окт", - "ноем", - "дек", - ] - - day_names = [ - "", - "понеделник", - "вторник", - "сряда", - "четвъртък", - "петък", - "събота", - "неделя", - ] - day_abbreviations = ["", "пон", "вт", "ср", "четв", "пет", "съб", "нед"] - - -class UkrainianLocale(SlavicBaseLocale): - - names = ["ua", "uk_ua"] - - past = "{0} тому" - future = "за {0}" - - timeframes = { - "now": "зараз", - "second": "секунда", - "seconds": "{0} кілька секунд", - "minute": "хвилину", - "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], - "hour": "годину", - "hours": ["{0} годину", "{0} години", "{0} годин"], - "day": "день", - "days": ["{0} день", "{0} дні", "{0} днів"], - "month": "місяць", - "months": ["{0} місяць", "{0} місяці", "{0} місяців"], - "year": "рік", - "years": ["{0} рік", "{0} роки", "{0} років"], - } - - month_names = [ - "", - "січня", - "лютого", - "березня", - "квітня", - "травня", - "червня", - "липня", - "серпня", - "вересня", - "жовтня", - "листопада", - "грудня", - ] - month_abbreviations = [ - "", - "січ", - "лют", - "бер", - "квіт", - "трав", - "черв", - "лип", - "серп", - "вер", - "жовт", - "лист", - "груд", - ] - - day_names = [ - "", - "понеділок", - "вівторок", - "середа", - "четвер", - "п’ятниця", - "субота", - "неділя", - ] - day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"] - - -class MacedonianLocale(SlavicBaseLocale): - names = ["mk", "mk_mk"] - - past = "пред {0}" - future = "за {0}" - - timeframes = { - "now": "сега", - "second": "една секунда", - "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"], - "minute": "една минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], - "hour": "еден саат", - "hours": ["{0} саат", "{0} саати", "{0} саати"], - "day": "еден ден", - "days": ["{0} ден", "{0} дена", "{0} дена"], - "week": "една недела", - "weeks": ["{0} недела", "{0} недели", "{0} недели"], - "month": "еден месец", - "months": ["{0} месец", "{0} месеци", "{0} месеци"], - "year": "една година", - "years": ["{0} година", "{0} години", "{0} години"], - } - - meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} - - month_names = [ - "", - "Јануари", - "Февруари", - "Март", - "Април", - "Мај", - "Јуни", - "Јули", - "Август", - "Септември", - "Октомври", - "Ноември", - "Декември", - ] - month_abbreviations = [ - "", - "Јан", - "Фев", - "Мар", - "Апр", - "Мај", - "Јун", - "Јул", - "Авг", - "Септ", - "Окт", - "Ноем", - "Декем", - ] - - day_names = [ - "", - "Понеделник", - "Вторник", - "Среда", - "Четврток", - "Петок", - "Сабота", - "Недела", - ] - day_abbreviations = [ - "", - "Пон", - "Вт", - "Сре", - "Чет", - "Пет", - "Саб", - "Нед", - ] - - -class GermanBaseLocale(Locale): - - past = "vor {0}" - future = "in {0}" - and_word = "und" - - timeframes = { - "now": "gerade eben", - "second": "eine Sekunde", - "seconds": "{0} Sekunden", - "minute": "einer Minute", - "minutes": "{0} Minuten", - "hour": "einer Stunde", - "hours": "{0} Stunden", - "day": "einem Tag", - "days": "{0} Tagen", - "week": "einer Woche", - "weeks": "{0} Wochen", - "month": "einem Monat", - "months": "{0} Monaten", - "year": "einem Jahr", - "years": "{0} Jahren", - } - - timeframes_only_distance = timeframes.copy() - timeframes_only_distance["minute"] = "eine Minute" - timeframes_only_distance["hour"] = "eine Stunde" - timeframes_only_distance["day"] = "ein Tag" - timeframes_only_distance["week"] = "eine Woche" - timeframes_only_distance["month"] = "ein Monat" - timeframes_only_distance["year"] = "ein Jahr" - - month_names = [ - "", - "Januar", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mär", - "Apr", - "Mai", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Dez", - ] - - day_names = [ - "", - "Montag", - "Dienstag", - "Mittwoch", - "Donnerstag", - "Freitag", - "Samstag", - "Sonntag", - ] - - day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] - - def _ordinal_number(self, n): - return "{}.".format(n) - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - if not only_distance: - return super(GermanBaseLocale, self).describe( - timeframe, delta, only_distance - ) - - # German uses a different case without 'in' or 'ago' - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) - - return humanized - - -class GermanLocale(GermanBaseLocale, Locale): - - names = ["de", "de_de"] - - -class SwissLocale(GermanBaseLocale, Locale): - - names = ["de_ch"] - - -class AustrianLocale(GermanBaseLocale, Locale): - - names = ["de_at"] - - month_names = [ - "", - "Jänner", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember", - ] - - -class NorwegianLocale(Locale): - - names = ["nb", "nb_no"] - - past = "for {0} siden" - future = "om {0}" - - timeframes = { - "now": "nå nettopp", - "second": "et sekund", - "seconds": "{0} noen sekunder", - "minute": "ett minutt", - "minutes": "{0} minutter", - "hour": "en time", - "hours": "{0} timer", - "day": "en dag", - "days": "{0} dager", - "month": "en måned", - "months": "{0} måneder", - "year": "ett år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "mars", - "april", - "mai", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "mai", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "des", - ] - - day_names = [ - "", - "mandag", - "tirsdag", - "onsdag", - "torsdag", - "fredag", - "lørdag", - "søndag", - ] - day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"] - - -class NewNorwegianLocale(Locale): - - names = ["nn", "nn_no"] - - past = "for {0} sidan" - future = "om {0}" - - timeframes = { - "now": "no nettopp", - "second": "et sekund", - "seconds": "{0} nokre sekund", - "minute": "ett minutt", - "minutes": "{0} minutt", - "hour": "ein time", - "hours": "{0} timar", - "day": "ein dag", - "days": "{0} dagar", - "month": "en månad", - "months": "{0} månader", - "year": "eit år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "mars", - "april", - "mai", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "mai", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "des", - ] - - day_names = [ - "", - "måndag", - "tysdag", - "onsdag", - "torsdag", - "fredag", - "laurdag", - "sundag", - ] - day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"] - - -class PortugueseLocale(Locale): - names = ["pt", "pt_pt"] - - past = "há {0}" - future = "em {0}" - and_word = "e" - - timeframes = { - "now": "agora", - "second": "um segundo", - "seconds": "{0} segundos", - "minute": "um minuto", - "minutes": "{0} minutos", - "hour": "uma hora", - "hours": "{0} horas", - "day": "um dia", - "days": "{0} dias", - "week": "uma semana", - "weeks": "{0} semanas", - "month": "um mês", - "months": "{0} meses", - "year": "um ano", - "years": "{0} anos", - } - - month_names = [ - "", - "Janeiro", - "Fevereiro", - "Março", - "Abril", - "Maio", - "Junho", - "Julho", - "Agosto", - "Setembro", - "Outubro", - "Novembro", - "Dezembro", - ] - month_abbreviations = [ - "", - "Jan", - "Fev", - "Mar", - "Abr", - "Mai", - "Jun", - "Jul", - "Ago", - "Set", - "Out", - "Nov", - "Dez", - ] - - day_names = [ - "", - "Segunda-feira", - "Terça-feira", - "Quarta-feira", - "Quinta-feira", - "Sexta-feira", - "Sábado", - "Domingo", - ] - day_abbreviations = ["", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab", "Dom"] - - -class BrazilianPortugueseLocale(PortugueseLocale): - names = ["pt_br"] - - past = "faz {0}" - - -class TagalogLocale(Locale): - - names = ["tl", "tl_ph"] - - past = "nakaraang {0}" - future = "{0} mula ngayon" - - timeframes = { - "now": "ngayon lang", - "second": "isang segundo", - "seconds": "{0} segundo", - "minute": "isang minuto", - "minutes": "{0} minuto", - "hour": "isang oras", - "hours": "{0} oras", - "day": "isang araw", - "days": "{0} araw", - "week": "isang linggo", - "weeks": "{0} linggo", - "month": "isang buwan", - "months": "{0} buwan", - "year": "isang taon", - "years": "{0} taon", - } - - month_names = [ - "", - "Enero", - "Pebrero", - "Marso", - "Abril", - "Mayo", - "Hunyo", - "Hulyo", - "Agosto", - "Setyembre", - "Oktubre", - "Nobyembre", - "Disyembre", - ] - month_abbreviations = [ - "", - "Ene", - "Peb", - "Mar", - "Abr", - "May", - "Hun", - "Hul", - "Ago", - "Set", - "Okt", - "Nob", - "Dis", - ] - - day_names = [ - "", - "Lunes", - "Martes", - "Miyerkules", - "Huwebes", - "Biyernes", - "Sabado", - "Linggo", - ] - day_abbreviations = ["", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab", "Lin"] - - meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} - - def _ordinal_number(self, n): - return "ika-{}".format(n) - - -class VietnameseLocale(Locale): - - names = ["vi", "vi_vn"] - - past = "{0} trước" - future = "{0} nữa" - - timeframes = { - "now": "hiện tại", - "second": "một giây", - "seconds": "{0} giây", - "minute": "một phút", - "minutes": "{0} phút", - "hour": "một giờ", - "hours": "{0} giờ", - "day": "một ngày", - "days": "{0} ngày", - "week": "một tuần", - "weeks": "{0} tuần", - "month": "một tháng", - "months": "{0} tháng", - "year": "một năm", - "years": "{0} năm", - } - - month_names = [ - "", - "Tháng Một", - "Tháng Hai", - "Tháng Ba", - "Tháng Tư", - "Tháng Năm", - "Tháng Sáu", - "Tháng Bảy", - "Tháng Tám", - "Tháng Chín", - "Tháng Mười", - "Tháng Mười Một", - "Tháng Mười Hai", - ] - month_abbreviations = [ - "", - "Tháng 1", - "Tháng 2", - "Tháng 3", - "Tháng 4", - "Tháng 5", - "Tháng 6", - "Tháng 7", - "Tháng 8", - "Tháng 9", - "Tháng 10", - "Tháng 11", - "Tháng 12", - ] - - day_names = [ - "", - "Thứ Hai", - "Thứ Ba", - "Thứ Tư", - "Thứ Năm", - "Thứ Sáu", - "Thứ Bảy", - "Chủ Nhật", - ] - day_abbreviations = ["", "Thứ 2", "Thứ 3", "Thứ 4", "Thứ 5", "Thứ 6", "Thứ 7", "CN"] - - -class TurkishLocale(Locale): - - names = ["tr", "tr_tr"] - - past = "{0} önce" - future = "{0} sonra" - - timeframes = { - "now": "şimdi", - "second": "bir saniye", - "seconds": "{0} saniye", - "minute": "bir dakika", - "minutes": "{0} dakika", - "hour": "bir saat", - "hours": "{0} saat", - "day": "bir gün", - "days": "{0} gün", - "month": "bir ay", - "months": "{0} ay", - "year": "yıl", - "years": "{0} yıl", - } - - month_names = [ - "", - "Ocak", - "Şubat", - "Mart", - "Nisan", - "Mayıs", - "Haziran", - "Temmuz", - "Ağustos", - "Eylül", - "Ekim", - "Kasım", - "Aralık", - ] - month_abbreviations = [ - "", - "Oca", - "Şub", - "Mar", - "Nis", - "May", - "Haz", - "Tem", - "Ağu", - "Eyl", - "Eki", - "Kas", - "Ara", - ] - - day_names = [ - "", - "Pazartesi", - "Salı", - "Çarşamba", - "Perşembe", - "Cuma", - "Cumartesi", - "Pazar", - ] - day_abbreviations = ["", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"] - - -class AzerbaijaniLocale(Locale): - - names = ["az", "az_az"] - - past = "{0} əvvəl" - future = "{0} sonra" - - timeframes = { - "now": "indi", - "second": "saniyə", - "seconds": "{0} saniyə", - "minute": "bir dəqiqə", - "minutes": "{0} dəqiqə", - "hour": "bir saat", - "hours": "{0} saat", - "day": "bir gün", - "days": "{0} gün", - "month": "bir ay", - "months": "{0} ay", - "year": "il", - "years": "{0} il", - } - - month_names = [ - "", - "Yanvar", - "Fevral", - "Mart", - "Aprel", - "May", - "İyun", - "İyul", - "Avqust", - "Sentyabr", - "Oktyabr", - "Noyabr", - "Dekabr", - ] - month_abbreviations = [ - "", - "Yan", - "Fev", - "Mar", - "Apr", - "May", - "İyn", - "İyl", - "Avq", - "Sen", - "Okt", - "Noy", - "Dek", - ] - - day_names = [ - "", - "Bazar ertəsi", - "Çərşənbə axşamı", - "Çərşənbə", - "Cümə axşamı", - "Cümə", - "Şənbə", - "Bazar", - ] - day_abbreviations = ["", "Ber", "Çax", "Çər", "Cax", "Cüm", "Şnb", "Bzr"] - - -class ArabicLocale(Locale): - names = [ - "ar", - "ar_ae", - "ar_bh", - "ar_dj", - "ar_eg", - "ar_eh", - "ar_er", - "ar_km", - "ar_kw", - "ar_ly", - "ar_om", - "ar_qa", - "ar_sa", - "ar_sd", - "ar_so", - "ar_ss", - "ar_td", - "ar_ye", - ] - - past = "منذ {0}" - future = "خلال {0}" - - timeframes = { - "now": "الآن", - "second": "ثانية", - "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, - "minute": "دقيقة", - "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, - "hour": "ساعة", - "hours": {"double": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, - "day": "يوم", - "days": {"double": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, - "month": "شهر", - "months": {"double": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, - "year": "سنة", - "years": {"double": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, - } - - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "مايو", - "يونيو", - "يوليو", - "أغسطس", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "مايو", - "يونيو", - "يوليو", - "أغسطس", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - - day_names = [ - "", - "الإثنين", - "الثلاثاء", - "الأربعاء", - "الخميس", - "الجمعة", - "السبت", - "الأحد", - ] - day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - delta = abs(delta) - if isinstance(form, dict): - if delta == 2: - form = form["double"] - elif delta > 2 and delta <= 10: - form = form["ten"] - else: - form = form["higher"] - - return form.format(delta) - - -class LevantArabicLocale(ArabicLocale): - names = ["ar_iq", "ar_jo", "ar_lb", "ar_ps", "ar_sy"] - month_names = [ - "", - "كانون الثاني", - "شباط", - "آذار", - "نيسان", - "أيار", - "حزيران", - "تموز", - "آب", - "أيلول", - "تشرين الأول", - "تشرين الثاني", - "كانون الأول", - ] - month_abbreviations = [ - "", - "كانون الثاني", - "شباط", - "آذار", - "نيسان", - "أيار", - "حزيران", - "تموز", - "آب", - "أيلول", - "تشرين الأول", - "تشرين الثاني", - "كانون الأول", - ] - - -class AlgeriaTunisiaArabicLocale(ArabicLocale): - names = ["ar_tn", "ar_dz"] - month_names = [ - "", - "جانفي", - "فيفري", - "مارس", - "أفريل", - "ماي", - "جوان", - "جويلية", - "أوت", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - month_abbreviations = [ - "", - "جانفي", - "فيفري", - "مارس", - "أفريل", - "ماي", - "جوان", - "جويلية", - "أوت", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - - -class MauritaniaArabicLocale(ArabicLocale): - names = ["ar_mr"] - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "إبريل", - "مايو", - "يونيو", - "يوليو", - "أغشت", - "شتمبر", - "أكتوبر", - "نوفمبر", - "دجمبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "إبريل", - "مايو", - "يونيو", - "يوليو", - "أغشت", - "شتمبر", - "أكتوبر", - "نوفمبر", - "دجمبر", - ] - - -class MoroccoArabicLocale(ArabicLocale): - names = ["ar_ma"] - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "ماي", - "يونيو", - "يوليوز", - "غشت", - "شتنبر", - "أكتوبر", - "نونبر", - "دجنبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "ماي", - "يونيو", - "يوليوز", - "غشت", - "شتنبر", - "أكتوبر", - "نونبر", - "دجنبر", - ] - - -class IcelandicLocale(Locale): - def _format_timeframe(self, timeframe, delta): - - timeframe = self.timeframes[timeframe] - if delta < 0: - timeframe = timeframe[0] - elif delta > 0: - timeframe = timeframe[1] - - return timeframe.format(abs(delta)) - - names = ["is", "is_is"] - - past = "fyrir {0} síðan" - future = "eftir {0}" - - timeframes = { - "now": "rétt í þessu", - "second": ("sekúndu", "sekúndu"), - "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), - "minute": ("einni mínútu", "eina mínútu"), - "minutes": ("{0} mínútum", "{0} mínútur"), - "hour": ("einum tíma", "einn tíma"), - "hours": ("{0} tímum", "{0} tíma"), - "day": ("einum degi", "einn dag"), - "days": ("{0} dögum", "{0} daga"), - "month": ("einum mánuði", "einn mánuð"), - "months": ("{0} mánuðum", "{0} mánuði"), - "year": ("einu ári", "eitt ár"), - "years": ("{0} árum", "{0} ár"), - } - - meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."} - - month_names = [ - "", - "janúar", - "febrúar", - "mars", - "apríl", - "maí", - "júní", - "júlí", - "ágúst", - "september", - "október", - "nóvember", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maí", - "jún", - "júl", - "ágú", - "sep", - "okt", - "nóv", - "des", - ] - - day_names = [ - "", - "mánudagur", - "þriðjudagur", - "miðvikudagur", - "fimmtudagur", - "föstudagur", - "laugardagur", - "sunnudagur", - ] - day_abbreviations = ["", "mán", "þri", "mið", "fim", "fös", "lau", "sun"] - - -class DanishLocale(Locale): - - names = ["da", "da_dk"] - - past = "for {0} siden" - future = "efter {0}" - and_word = "og" - - timeframes = { - "now": "lige nu", - "second": "et sekund", - "seconds": "{0} et par sekunder", - "minute": "et minut", - "minutes": "{0} minutter", - "hour": "en time", - "hours": "{0} timer", - "day": "en dag", - "days": "{0} dage", - "month": "en måned", - "months": "{0} måneder", - "year": "et år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "marts", - "april", - "maj", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "mandag", - "tirsdag", - "onsdag", - "torsdag", - "fredag", - "lørdag", - "søndag", - ] - day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"] - - -class MalayalamLocale(Locale): - - names = ["ml"] - - past = "{0} മുമ്പ്" - future = "{0} ശേഷം" - - timeframes = { - "now": "ഇപ്പോൾ", - "second": "ഒരു നിമിഷം", - "seconds": "{0} സെക്കന്റ്‌", - "minute": "ഒരു മിനിറ്റ്", - "minutes": "{0} മിനിറ്റ്", - "hour": "ഒരു മണിക്കൂർ", - "hours": "{0} മണിക്കൂർ", - "day": "ഒരു ദിവസം ", - "days": "{0} ദിവസം ", - "month": "ഒരു മാസം ", - "months": "{0} മാസം ", - "year": "ഒരു വർഷം ", - "years": "{0} വർഷം ", - } - - meridians = { - "am": "രാവിലെ", - "pm": "ഉച്ചക്ക് ശേഷം", - "AM": "രാവിലെ", - "PM": "ഉച്ചക്ക് ശേഷം", - } - - month_names = [ - "", - "ജനുവരി", - "ഫെബ്രുവരി", - "മാർച്ച്‌", - "ഏപ്രിൽ ", - "മെയ്‌ ", - "ജൂണ്‍", - "ജൂലൈ", - "ഓഗസ്റ്റ്‌", - "സെപ്റ്റംബർ", - "ഒക്ടോബർ", - "നവംബർ", - "ഡിസംബർ", - ] - month_abbreviations = [ - "", - "ജനു", - "ഫെബ് ", - "മാർ", - "ഏപ്രിൽ", - "മേയ്", - "ജൂണ്‍", - "ജൂലൈ", - "ഓഗസ്റ", - "സെപ്റ്റ", - "ഒക്ടോ", - "നവം", - "ഡിസം", - ] - - day_names = ["", "തിങ്കള്‍", "ചൊവ്വ", "ബുധന്‍", "വ്യാഴം", "വെള്ളി", "ശനി", "ഞായര്‍"] - day_abbreviations = [ - "", - "തിങ്കള്‍", - "ചൊവ്വ", - "ബുധന്‍", - "വ്യാഴം", - "വെള്ളി", - "ശനി", - "ഞായര്‍", - ] - - -class HindiLocale(Locale): - - names = ["hi"] - - past = "{0} पहले" - future = "{0} बाद" - - timeframes = { - "now": "अभी", - "second": "एक पल", - "seconds": "{0} सेकंड्", - "minute": "एक मिनट ", - "minutes": "{0} मिनट ", - "hour": "एक घंटा", - "hours": "{0} घंटे", - "day": "एक दिन", - "days": "{0} दिन", - "month": "एक माह ", - "months": "{0} महीने ", - "year": "एक वर्ष ", - "years": "{0} साल ", - } - - meridians = {"am": "सुबह", "pm": "शाम", "AM": "सुबह", "PM": "शाम"} - - month_names = [ - "", - "जनवरी", - "फरवरी", - "मार्च", - "अप्रैल ", - "मई", - "जून", - "जुलाई", - "अगस्त", - "सितंबर", - "अक्टूबर", - "नवंबर", - "दिसंबर", - ] - month_abbreviations = [ - "", - "जन", - "फ़र", - "मार्च", - "अप्रै", - "मई", - "जून", - "जुलाई", - "आग", - "सित", - "अकत", - "नवे", - "दिस", - ] - - day_names = [ - "", - "सोमवार", - "मंगलवार", - "बुधवार", - "गुरुवार", - "शुक्रवार", - "शनिवार", - "रविवार", - ] - day_abbreviations = ["", "सोम", "मंगल", "बुध", "गुरुवार", "शुक्र", "शनि", "रवि"] - - -class CzechLocale(Locale): - names = ["cs", "cs_cz"] - - timeframes = { - "now": "Teď", - "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, - "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, - "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]}, - "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, - "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]}, - "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"}, - "weeks": {"past": "{0} týdny", "future": ["{0} týdny", "{0} týdnů"]}, - "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, - "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]}, - "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, - "years": {"past": "{0} lety", "future": ["{0} roky", "{0} let"]}, - } - - past = "Před {0}" - future = "Za {0}" - - month_names = [ - "", - "leden", - "únor", - "březen", - "duben", - "květen", - "červen", - "červenec", - "srpen", - "září", - "říjen", - "listopad", - "prosinec", - ] - month_abbreviations = [ - "", - "led", - "úno", - "bře", - "dub", - "kvě", - "čvn", - "čvc", - "srp", - "zář", - "říj", - "lis", - "pro", - ] - - day_names = [ - "", - "pondělí", - "úterý", - "středa", - "čtvrtek", - "pátek", - "sobota", - "neděle", - ] - day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - """Czech aware time frame format function, takes into account - the differences between past and future forms.""" - form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) - - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] - - return form.format(delta) - - -class SlovakLocale(Locale): - names = ["sk", "sk_sk"] - - timeframes = { - "now": "Teraz", - "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, - "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, - "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodín"]}, - "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, - "days": {"past": "{0} dňami", "future": ["{0} dni", "{0} dní"]}, - "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"}, - "weeks": {"past": "{0} týždňami", "future": ["{0} týždne", "{0} týždňov"]}, - "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, - "months": {"past": "{0} mesiacmi", "future": ["{0} mesiace", "{0} mesiacov"]}, - "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, - "years": {"past": "{0} rokmi", "future": ["{0} roky", "{0} rokov"]}, - } - - past = "Pred {0}" - future = "O {0}" - and_word = "a" - - month_names = [ - "", - "január", - "február", - "marec", - "apríl", - "máj", - "jún", - "júl", - "august", - "september", - "október", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "máj", - "jún", - "júl", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "pondelok", - "utorok", - "streda", - "štvrtok", - "piatok", - "sobota", - "nedeľa", - ] - day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - """Slovak aware time frame format function, takes into account - the differences between past and future forms.""" - form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) - - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] - - return form.format(delta) - - -class FarsiLocale(Locale): - - names = ["fa", "fa_ir"] - - past = "{0} قبل" - future = "در {0}" - - timeframes = { - "now": "اکنون", - "second": "یک لحظه", - "seconds": "{0} ثانیه", - "minute": "یک دقیقه", - "minutes": "{0} دقیقه", - "hour": "یک ساعت", - "hours": "{0} ساعت", - "day": "یک روز", - "days": "{0} روز", - "month": "یک ماه", - "months": "{0} ماه", - "year": "یک سال", - "years": "{0} سال", - } - - meridians = { - "am": "قبل از ظهر", - "pm": "بعد از ظهر", - "AM": "قبل از ظهر", - "PM": "بعد از ظهر", - } - - month_names = [ - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ] - - day_names = [ - "", - "دو شنبه", - "سه شنبه", - "چهارشنبه", - "پنجشنبه", - "جمعه", - "شنبه", - "یکشنبه", - ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - - -class HebrewLocale(Locale): - - names = ["he", "he_IL"] - - past = "לפני {0}" - future = "בעוד {0}" - and_word = "ו" - - timeframes = { - "now": "הרגע", - "second": "שנייה", - "seconds": "{0} שניות", - "minute": "דקה", - "minutes": "{0} דקות", - "hour": "שעה", - "hours": "{0} שעות", - "2-hours": "שעתיים", - "day": "יום", - "days": "{0} ימים", - "2-days": "יומיים", - "week": "שבוע", - "weeks": "{0} שבועות", - "2-weeks": "שבועיים", - "month": "חודש", - "months": "{0} חודשים", - "2-months": "חודשיים", - "year": "שנה", - "years": "{0} שנים", - "2-years": "שנתיים", - } - - meridians = { - "am": 'לפנ"צ', - "pm": 'אחר"צ', - "AM": "לפני הצהריים", - "PM": "אחרי הצהריים", - } - - month_names = [ - "", - "ינואר", - "פברואר", - "מרץ", - "אפריל", - "מאי", - "יוני", - "יולי", - "אוגוסט", - "ספטמבר", - "אוקטובר", - "נובמבר", - "דצמבר", - ] - month_abbreviations = [ - "", - "ינו׳", - "פבר׳", - "מרץ", - "אפר׳", - "מאי", - "יוני", - "יולי", - "אוג׳", - "ספט׳", - "אוק׳", - "נוב׳", - "דצמ׳", - ] - - day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] - day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - - def _format_timeframe(self, timeframe, delta): - """Hebrew couple of aware""" - couple = "2-{}".format(timeframe) - single = timeframe.rstrip("s") - if abs(delta) == 2 and couple in self.timeframes: - key = couple - elif abs(delta) == 1 and single in self.timeframes: - key = single - else: - key = timeframe - - return self.timeframes[key].format(trunc(abs(delta))) - - def describe_multi(self, timeframes, only_distance=False): - """Describes a delta within multiple timeframes in plain language. - In Hebrew, the and word behaves a bit differently. - - :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords - """ - - humanized = "" - for index, (timeframe, delta) in enumerate(timeframes): - last_humanized = self._format_timeframe(timeframe, delta) - if index == 0: - humanized = last_humanized - elif index == len(timeframes) - 1: # Must have at least 2 items - humanized += " " + self.and_word - if last_humanized[0].isdecimal(): - humanized += "־" - humanized += last_humanized - else: # Don't add for the last one - humanized += ", " + last_humanized - - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - -class MarathiLocale(Locale): - - names = ["mr"] - - past = "{0} आधी" - future = "{0} नंतर" - - timeframes = { - "now": "सद्य", - "second": "एक सेकंद", - "seconds": "{0} सेकंद", - "minute": "एक मिनिट ", - "minutes": "{0} मिनिट ", - "hour": "एक तास", - "hours": "{0} तास", - "day": "एक दिवस", - "days": "{0} दिवस", - "month": "एक महिना ", - "months": "{0} महिने ", - "year": "एक वर्ष ", - "years": "{0} वर्ष ", - } - - meridians = {"am": "सकाळ", "pm": "संध्याकाळ", "AM": "सकाळ", "PM": "संध्याकाळ"} - - month_names = [ - "", - "जानेवारी", - "फेब्रुवारी", - "मार्च", - "एप्रिल", - "मे", - "जून", - "जुलै", - "अॉगस्ट", - "सप्टेंबर", - "अॉक्टोबर", - "नोव्हेंबर", - "डिसेंबर", - ] - month_abbreviations = [ - "", - "जान", - "फेब्रु", - "मार्च", - "एप्रि", - "मे", - "जून", - "जुलै", - "अॉग", - "सप्टें", - "अॉक्टो", - "नोव्हें", - "डिसें", - ] - - day_names = [ - "", - "सोमवार", - "मंगळवार", - "बुधवार", - "गुरुवार", - "शुक्रवार", - "शनिवार", - "रविवार", - ] - day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] - - -def _map_locales(): - - locales = {} - - for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): - if issubclass(cls, Locale): # pragma: no branch - for name in cls.names: - locales[name.lower()] = cls - - return locales - - -class CatalanLocale(Locale): - names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] - past = "Fa {0}" - future = "En {0}" - and_word = "i" - - timeframes = { - "now": "Ara mateix", - "second": "un segon", - "seconds": "{0} segons", - "minute": "1 minut", - "minutes": "{0} minuts", - "hour": "una hora", - "hours": "{0} hores", - "day": "un dia", - "days": "{0} dies", - "month": "un mes", - "months": "{0} mesos", - "year": "un any", - "years": "{0} anys", - } - - month_names = [ - "", - "gener", - "febrer", - "març", - "abril", - "maig", - "juny", - "juliol", - "agost", - "setembre", - "octubre", - "novembre", - "desembre", - ] - month_abbreviations = [ - "", - "gen.", - "febr.", - "març", - "abr.", - "maig", - "juny", - "jul.", - "ag.", - "set.", - "oct.", - "nov.", - "des.", - ] - day_names = [ - "", - "dilluns", - "dimarts", - "dimecres", - "dijous", - "divendres", - "dissabte", - "diumenge", - ] - day_abbreviations = [ - "", - "dl.", - "dt.", - "dc.", - "dj.", - "dv.", - "ds.", - "dg.", - ] - - -class BasqueLocale(Locale): - names = ["eu", "eu_eu"] - past = "duela {0}" - future = "{0}" # I don't know what's the right phrase in Basque for the future. - - timeframes = { - "now": "Orain", - "second": "segundo bat", - "seconds": "{0} segundu", - "minute": "minutu bat", - "minutes": "{0} minutu", - "hour": "ordu bat", - "hours": "{0} ordu", - "day": "egun bat", - "days": "{0} egun", - "month": "hilabete bat", - "months": "{0} hilabet", - "year": "urte bat", - "years": "{0} urte", - } - - month_names = [ - "", - "urtarrilak", - "otsailak", - "martxoak", - "apirilak", - "maiatzak", - "ekainak", - "uztailak", - "abuztuak", - "irailak", - "urriak", - "azaroak", - "abenduak", - ] - month_abbreviations = [ - "", - "urt", - "ots", - "mar", - "api", - "mai", - "eka", - "uzt", - "abu", - "ira", - "urr", - "aza", - "abe", - ] - day_names = [ - "", - "astelehena", - "asteartea", - "asteazkena", - "osteguna", - "ostirala", - "larunbata", - "igandea", - ] - day_abbreviations = ["", "al", "ar", "az", "og", "ol", "lr", "ig"] - - -class HungarianLocale(Locale): - - names = ["hu", "hu_hu"] - - past = "{0} ezelőtt" - future = "{0} múlva" - - timeframes = { - "now": "éppen most", - "second": {"past": "egy második", "future": "egy második"}, - "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, - "minute": {"past": "egy perccel", "future": "egy perc"}, - "minutes": {"past": "{0} perccel", "future": "{0} perc"}, - "hour": {"past": "egy órával", "future": "egy óra"}, - "hours": {"past": "{0} órával", "future": "{0} óra"}, - "day": {"past": "egy nappal", "future": "egy nap"}, - "days": {"past": "{0} nappal", "future": "{0} nap"}, - "month": {"past": "egy hónappal", "future": "egy hónap"}, - "months": {"past": "{0} hónappal", "future": "{0} hónap"}, - "year": {"past": "egy évvel", "future": "egy év"}, - "years": {"past": "{0} évvel", "future": "{0} év"}, - } - - month_names = [ - "", - "január", - "február", - "március", - "április", - "május", - "június", - "július", - "augusztus", - "szeptember", - "október", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "febr", - "márc", - "ápr", - "máj", - "jún", - "júl", - "aug", - "szept", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "hétfő", - "kedd", - "szerda", - "csütörtök", - "péntek", - "szombat", - "vasárnap", - ] - day_abbreviations = ["", "hét", "kedd", "szer", "csüt", "pént", "szom", "vas"] - - meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - - if isinstance(form, dict): - if delta > 0: - form = form["future"] - else: - form = form["past"] - - return form.format(abs(delta)) - - -class EsperantoLocale(Locale): - names = ["eo", "eo_xx"] - past = "antaŭ {0}" - future = "post {0}" - - timeframes = { - "now": "nun", - "second": "sekundo", - "seconds": "{0} kelkaj sekundoj", - "minute": "unu minuto", - "minutes": "{0} minutoj", - "hour": "un horo", - "hours": "{0} horoj", - "day": "unu tago", - "days": "{0} tagoj", - "month": "unu monato", - "months": "{0} monatoj", - "year": "unu jaro", - "years": "{0} jaroj", - } - - month_names = [ - "", - "januaro", - "februaro", - "marto", - "aprilo", - "majo", - "junio", - "julio", - "aŭgusto", - "septembro", - "oktobro", - "novembro", - "decembro", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aŭg", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "lundo", - "mardo", - "merkredo", - "ĵaŭdo", - "vendredo", - "sabato", - "dimanĉo", - ] - day_abbreviations = ["", "lun", "mar", "mer", "ĵaŭ", "ven", "sab", "dim"] - - meridians = {"am": "atm", "pm": "ptm", "AM": "ATM", "PM": "PTM"} - - ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" - - def _ordinal_number(self, n): - return "{}a".format(n) - - -class ThaiLocale(Locale): - - names = ["th", "th_th"] - - past = "{0}{1}ที่ผ่านมา" - future = "ในอีก{1}{0}" - - timeframes = { - "now": "ขณะนี้", - "second": "วินาที", - "seconds": "{0} ไม่กี่วินาที", - "minute": "1 นาที", - "minutes": "{0} นาที", - "hour": "1 ชั่วโมง", - "hours": "{0} ชั่วโมง", - "day": "1 วัน", - "days": "{0} วัน", - "month": "1 เดือน", - "months": "{0} เดือน", - "year": "1 ปี", - "years": "{0} ปี", - } - - month_names = [ - "", - "มกราคม", - "กุมภาพันธ์", - "มีนาคม", - "เมษายน", - "พฤษภาคม", - "มิถุนายน", - "กรกฎาคม", - "สิงหาคม", - "กันยายน", - "ตุลาคม", - "พฤศจิกายน", - "ธันวาคม", - ] - month_abbreviations = [ - "", - "ม.ค.", - "ก.พ.", - "มี.ค.", - "เม.ย.", - "พ.ค.", - "มิ.ย.", - "ก.ค.", - "ส.ค.", - "ก.ย.", - "ต.ค.", - "พ.ย.", - "ธ.ค.", - ] - - day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] - day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - BE_OFFSET = 543 - - def year_full(self, year): - """Thai always use Buddhist Era (BE) which is CE + 543""" - year += self.BE_OFFSET - return "{:04d}".format(year) - - def year_abbreviation(self, year): - """Thai always use Buddhist Era (BE) which is CE + 543""" - year += self.BE_OFFSET - return "{:04d}".format(year)[2:] - - def _format_relative(self, humanized, timeframe, delta): - """Thai normally doesn't have any space between words""" - if timeframe == "now": - return humanized - space = "" if timeframe == "seconds" else " " - direction = self.past if delta < 0 else self.future - - return direction.format(humanized, space) - - -class BengaliLocale(Locale): - - names = ["bn", "bn_bd", "bn_in"] - - past = "{0} আগে" - future = "{0} পরে" - - timeframes = { - "now": "এখন", - "second": "একটি দ্বিতীয়", - "seconds": "{0} সেকেন্ড", - "minute": "এক মিনিট", - "minutes": "{0} মিনিট", - "hour": "এক ঘণ্টা", - "hours": "{0} ঘণ্টা", - "day": "এক দিন", - "days": "{0} দিন", - "month": "এক মাস", - "months": "{0} মাস ", - "year": "এক বছর", - "years": "{0} বছর", - } - - meridians = {"am": "সকাল", "pm": "বিকাল", "AM": "সকাল", "PM": "বিকাল"} - - month_names = [ - "", - "জানুয়ারি", - "ফেব্রুয়ারি", - "মার্চ", - "এপ্রিল", - "মে", - "জুন", - "জুলাই", - "আগস্ট", - "সেপ্টেম্বর", - "অক্টোবর", - "নভেম্বর", - "ডিসেম্বর", - ] - month_abbreviations = [ - "", - "জানু", - "ফেব", - "মার্চ", - "এপ্রি", - "মে", - "জুন", - "জুল", - "অগা", - "সেপ্ট", - "অক্টো", - "নভে", - "ডিসে", - ] - - day_names = [ - "", - "সোমবার", - "মঙ্গলবার", - "বুধবার", - "বৃহস্পতিবার", - "শুক্রবার", - "শনিবার", - "রবিবার", - ] - day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"] - - def _ordinal_number(self, n): - if n > 10 or n == 0: - return "{}তম".format(n) - if n in [1, 5, 7, 8, 9, 10]: - return "{}ম".format(n) - if n in [2, 3]: - return "{}য়".format(n) - if n == 4: - return "{}র্থ".format(n) - if n == 6: - return "{}ষ্ঠ".format(n) - - -class RomanshLocale(Locale): - - names = ["rm", "rm_ch"] - - past = "avant {0}" - future = "en {0}" - - timeframes = { - "now": "en quest mument", - "second": "in secunda", - "seconds": "{0} secundas", - "minute": "ina minuta", - "minutes": "{0} minutas", - "hour": "in'ura", - "hours": "{0} ura", - "day": "in di", - "days": "{0} dis", - "month": "in mais", - "months": "{0} mais", - "year": "in onn", - "years": "{0} onns", - } - - month_names = [ - "", - "schaner", - "favrer", - "mars", - "avrigl", - "matg", - "zercladur", - "fanadur", - "avust", - "settember", - "october", - "november", - "december", - ] - - month_abbreviations = [ - "", - "schan", - "fav", - "mars", - "avr", - "matg", - "zer", - "fan", - "avu", - "set", - "oct", - "nov", - "dec", - ] - - day_names = [ - "", - "glindesdi", - "mardi", - "mesemna", - "gievgia", - "venderdi", - "sonda", - "dumengia", - ] - - day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"] - - -class RomanianLocale(Locale): - names = ["ro", "ro_ro"] - - past = "{0} în urmă" - future = "peste {0}" - and_word = "și" - - timeframes = { - "now": "acum", - "second": "o secunda", - "seconds": "{0} câteva secunde", - "minute": "un minut", - "minutes": "{0} minute", - "hour": "o oră", - "hours": "{0} ore", - "day": "o zi", - "days": "{0} zile", - "month": "o lună", - "months": "{0} luni", - "year": "un an", - "years": "{0} ani", - } - - month_names = [ - "", - "ianuarie", - "februarie", - "martie", - "aprilie", - "mai", - "iunie", - "iulie", - "august", - "septembrie", - "octombrie", - "noiembrie", - "decembrie", - ] - month_abbreviations = [ - "", - "ian", - "febr", - "mart", - "apr", - "mai", - "iun", - "iul", - "aug", - "sept", - "oct", - "nov", - "dec", - ] - - day_names = [ - "", - "luni", - "marți", - "miercuri", - "joi", - "vineri", - "sâmbătă", - "duminică", - ] - day_abbreviations = ["", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] - - -class SlovenianLocale(Locale): - names = ["sl", "sl_si"] - - past = "pred {0}" - future = "čez {0}" - and_word = "in" - - timeframes = { - "now": "zdaj", - "second": "sekundo", - "seconds": "{0} sekund", - "minute": "minuta", - "minutes": "{0} minutami", - "hour": "uro", - "hours": "{0} ur", - "day": "dan", - "days": "{0} dni", - "month": "mesec", - "months": "{0} mesecev", - "year": "leto", - "years": "{0} let", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - month_names = [ - "", - "Januar", - "Februar", - "Marec", - "April", - "Maj", - "Junij", - "Julij", - "Avgust", - "September", - "Oktober", - "November", - "December", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "Maj", - "Jun", - "Jul", - "Avg", - "Sep", - "Okt", - "Nov", - "Dec", - ] - - day_names = [ - "", - "Ponedeljek", - "Torek", - "Sreda", - "Četrtek", - "Petek", - "Sobota", - "Nedelja", - ] - - day_abbreviations = ["", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"] - - -class IndonesianLocale(Locale): - - names = ["id", "id_id"] - - past = "{0} yang lalu" - future = "dalam {0}" - and_word = "dan" - - timeframes = { - "now": "baru saja", - "second": "1 sebentar", - "seconds": "{0} detik", - "minute": "1 menit", - "minutes": "{0} menit", - "hour": "1 jam", - "hours": "{0} jam", - "day": "1 hari", - "days": "{0} hari", - "month": "1 bulan", - "months": "{0} bulan", - "year": "1 tahun", - "years": "{0} tahun", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - month_names = [ - "", - "Januari", - "Februari", - "Maret", - "April", - "Mei", - "Juni", - "Juli", - "Agustus", - "September", - "Oktober", - "November", - "Desember", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "Mei", - "Jun", - "Jul", - "Ags", - "Sept", - "Okt", - "Nov", - "Des", - ] - - day_names = ["", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] - - day_abbreviations = [ - "", - "Senin", - "Selasa", - "Rabu", - "Kamis", - "Jumat", - "Sabtu", - "Minggu", - ] - - -class NepaliLocale(Locale): - names = ["ne", "ne_np"] - - past = "{0} पहिले" - future = "{0} पछी" - - timeframes = { - "now": "अहिले", - "second": "एक सेकेन्ड", - "seconds": "{0} सेकण्ड", - "minute": "मिनेट", - "minutes": "{0} मिनेट", - "hour": "एक घण्टा", - "hours": "{0} घण्टा", - "day": "एक दिन", - "days": "{0} दिन", - "month": "एक महिना", - "months": "{0} महिना", - "year": "एक बर्ष", - "years": "बर्ष", - } - - meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"} - - month_names = [ - "", - "जनवरी", - "फेब्रुअरी", - "मार्च", - "एप्रील", - "मे", - "जुन", - "जुलाई", - "अगष्ट", - "सेप्टेम्बर", - "अक्टोबर", - "नोवेम्बर", - "डिसेम्बर", - ] - month_abbreviations = [ - "", - "जन", - "फेब", - "मार्च", - "एप्रील", - "मे", - "जुन", - "जुलाई", - "अग", - "सेप", - "अक्ट", - "नोव", - "डिस", - ] - - day_names = [ - "", - "सोमवार", - "मंगलवार", - "बुधवार", - "बिहिवार", - "शुक्रवार", - "शनिवार", - "आइतवार", - ] - - day_abbreviations = ["", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि", "आइत"] - - -class EstonianLocale(Locale): - names = ["ee", "et"] - - past = "{0} tagasi" - future = "{0} pärast" - and_word = "ja" - - timeframes = { - "now": {"past": "just nüüd", "future": "just nüüd"}, - "second": {"past": "üks sekund", "future": "ühe sekundi"}, - "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"}, - "minute": {"past": "üks minut", "future": "ühe minuti"}, - "minutes": {"past": "{0} minutit", "future": "{0} minuti"}, - "hour": {"past": "tund aega", "future": "tunni aja"}, - "hours": {"past": "{0} tundi", "future": "{0} tunni"}, - "day": {"past": "üks päev", "future": "ühe päeva"}, - "days": {"past": "{0} päeva", "future": "{0} päeva"}, - "month": {"past": "üks kuu", "future": "ühe kuu"}, - "months": {"past": "{0} kuud", "future": "{0} kuu"}, - "year": {"past": "üks aasta", "future": "ühe aasta"}, - "years": {"past": "{0} aastat", "future": "{0} aasta"}, - } - - month_names = [ - "", - "Jaanuar", - "Veebruar", - "Märts", - "Aprill", - "Mai", - "Juuni", - "Juuli", - "August", - "September", - "Oktoober", - "November", - "Detsember", - ] - month_abbreviations = [ - "", - "Jan", - "Veb", - "Mär", - "Apr", - "Mai", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Dets", - ] - - day_names = [ - "", - "Esmaspäev", - "Teisipäev", - "Kolmapäev", - "Neljapäev", - "Reede", - "Laupäev", - "Pühapäev", - ] - day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - if delta > 0: - form = form["future"] - else: - form = form["past"] - return form.format(abs(delta)) - - -class SwahiliLocale(Locale): - - names = [ - "sw", - "sw_ke", - "sw_tz", - ] - - past = "{0} iliyopita" - future = "muda wa {0}" - and_word = "na" - - timeframes = { - "now": "sasa hivi", - "second": "sekunde", - "seconds": "sekunde {0}", - "minute": "dakika moja", - "minutes": "dakika {0}", - "hour": "saa moja", - "hours": "saa {0}", - "day": "siku moja", - "days": "siku {0}", - "week": "wiki moja", - "weeks": "wiki {0}", - "month": "mwezi moja", - "months": "miezi {0}", - "year": "mwaka moja", - "years": "miaka {0}", - } - - meridians = {"am": "asu", "pm": "mch", "AM": "ASU", "PM": "MCH"} - - month_names = [ - "", - "Januari", - "Februari", - "Machi", - "Aprili", - "Mei", - "Juni", - "Julai", - "Agosti", - "Septemba", - "Oktoba", - "Novemba", - "Desemba", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mac", - "Apr", - "Mei", - "Jun", - "Jul", - "Ago", - "Sep", - "Okt", - "Nov", - "Des", - ] - - day_names = [ - "", - "Jumatatu", - "Jumanne", - "Jumatano", - "Alhamisi", - "Ijumaa", - "Jumamosi", - "Jumapili", - ] - day_abbreviations = [ - "", - "Jumatatu", - "Jumanne", - "Jumatano", - "Alhamisi", - "Ijumaa", - "Jumamosi", - "Jumapili", - ] - - -_locales = _map_locales() diff --git a/client/ayon_core/vendor/python/python_2/arrow/parser.py b/client/ayon_core/vendor/python/python_2/arrow/parser.py deleted file mode 100644 index 243fd1721c..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/parser.py +++ /dev/null @@ -1,596 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import re -from datetime import datetime, timedelta - -from dateutil import tz - -from arrow import locales -from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp - -try: - from functools import lru_cache -except ImportError: # pragma: no cover - from backports.functools_lru_cache import lru_cache # pragma: no cover - - -class ParserError(ValueError): - pass - - -# Allows for ParserErrors to be propagated from _build_datetime() -# when day_of_year errors occur. -# Before this, the ParserErrors were caught by the try/except in -# _parse_multiformat() and the appropriate error message was not -# transmitted to the user. -class ParserMatchError(ParserError): - pass - - -class DateTimeParser(object): - - _FORMAT_RE = re.compile( - r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" - ) - _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") - - _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") - _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}") - _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") - _TWO_DIGIT_RE = re.compile(r"\d{2}") - _THREE_DIGIT_RE = re.compile(r"\d{3}") - _FOUR_DIGIT_RE = re.compile(r"\d{4}") - _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") - _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") - _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will - # break cases like "15 Jul 2000" and a format list (see issue #447) - _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$") - _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$") - _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") - _WEEK_DATE_RE = re.compile(r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?") - - _BASE_INPUT_RE_MAP = { - "YYYY": _FOUR_DIGIT_RE, - "YY": _TWO_DIGIT_RE, - "MM": _TWO_DIGIT_RE, - "M": _ONE_OR_TWO_DIGIT_RE, - "DDDD": _THREE_DIGIT_RE, - "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE, - "DD": _TWO_DIGIT_RE, - "D": _ONE_OR_TWO_DIGIT_RE, - "HH": _TWO_DIGIT_RE, - "H": _ONE_OR_TWO_DIGIT_RE, - "hh": _TWO_DIGIT_RE, - "h": _ONE_OR_TWO_DIGIT_RE, - "mm": _TWO_DIGIT_RE, - "m": _ONE_OR_TWO_DIGIT_RE, - "ss": _TWO_DIGIT_RE, - "s": _ONE_OR_TWO_DIGIT_RE, - "X": _TIMESTAMP_RE, - "x": _TIMESTAMP_EXPANDED_RE, - "ZZZ": _TZ_NAME_RE, - "ZZ": _TZ_ZZ_RE, - "Z": _TZ_Z_RE, - "S": _ONE_OR_MORE_DIGIT_RE, - "W": _WEEK_DATE_RE, - } - - SEPARATORS = ["-", "/", "."] - - def __init__(self, locale="en_us", cache_size=0): - - self.locale = locales.get_locale(locale) - self._input_re_map = self._BASE_INPUT_RE_MAP.copy() - self._input_re_map.update( - { - "MMMM": self._generate_choice_re( - self.locale.month_names[1:], re.IGNORECASE - ), - "MMM": self._generate_choice_re( - self.locale.month_abbreviations[1:], re.IGNORECASE - ), - "Do": re.compile(self.locale.ordinal_day_re), - "dddd": self._generate_choice_re( - self.locale.day_names[1:], re.IGNORECASE - ), - "ddd": self._generate_choice_re( - self.locale.day_abbreviations[1:], re.IGNORECASE - ), - "d": re.compile(r"[1-7]"), - "a": self._generate_choice_re( - (self.locale.meridians["am"], self.locale.meridians["pm"]) - ), - # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to - # ensure backwards compatibility of this token - "A": self._generate_choice_re(self.locale.meridians.values()), - } - ) - if cache_size > 0: - self._generate_pattern_re = lru_cache(maxsize=cache_size)( - self._generate_pattern_re - ) - - # TODO: since we support more than ISO 8601, we should rename this function - # IDEA: break into multiple functions - def parse_iso(self, datetime_string, normalize_whitespace=False): - - if normalize_whitespace: - datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) - - has_space_divider = " " in datetime_string - has_t_divider = "T" in datetime_string - - num_spaces = datetime_string.count(" ") - if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: - raise ParserError( - "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( - datetime_string - ) - ) - - has_time = has_space_divider or has_t_divider - has_tz = False - - # date formats (ISO 8601 and others) to test against - # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) - formats = [ - "YYYY-MM-DD", - "YYYY-M-DD", - "YYYY-M-D", - "YYYY/MM/DD", - "YYYY/M/DD", - "YYYY/M/D", - "YYYY.MM.DD", - "YYYY.M.DD", - "YYYY.M.D", - "YYYYMMDD", - "YYYY-DDDD", - "YYYYDDDD", - "YYYY-MM", - "YYYY/MM", - "YYYY.MM", - "YYYY", - "W", - ] - - if has_time: - - if has_space_divider: - date_string, time_string = datetime_string.split(" ", 1) - else: - date_string, time_string = datetime_string.split("T", 1) - - time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - - time_components = self._TIME_RE.match(time_parts[0]) - - if time_components is None: - raise ParserError( - "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." - ) - - ( - hours, - minutes, - seconds, - subseconds_sep, - subseconds, - ) = time_components.groups() - - has_tz = len(time_parts) == 2 - has_minutes = minutes is not None - has_seconds = seconds is not None - has_subseconds = subseconds is not None - - is_basic_time_format = ":" not in time_parts[0] - tz_format = "Z" - - # use 'ZZ' token instead since tz offset is present in non-basic format - if has_tz and ":" in time_parts[1]: - tz_format = "ZZ" - - time_sep = "" if is_basic_time_format else ":" - - if has_subseconds: - time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format( - time_sep=time_sep, subseconds_sep=subseconds_sep - ) - elif has_seconds: - time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) - elif has_minutes: - time_string = "HH{time_sep}mm".format(time_sep=time_sep) - else: - time_string = "HH" - - if has_space_divider: - formats = ["{} {}".format(f, time_string) for f in formats] - else: - formats = ["{}T{}".format(f, time_string) for f in formats] - - if has_time and has_tz: - # Add "Z" or "ZZ" to the format strings to indicate to - # _parse_token() that a timezone needs to be parsed - formats = ["{}{}".format(f, tz_format) for f in formats] - - return self._parse_multiformat(datetime_string, formats) - - def parse(self, datetime_string, fmt, normalize_whitespace=False): - - if normalize_whitespace: - datetime_string = re.sub(r"\s+", " ", datetime_string) - - if isinstance(fmt, list): - return self._parse_multiformat(datetime_string, fmt) - - fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - - match = fmt_pattern_re.search(datetime_string) - - if match is None: - raise ParserMatchError( - "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) - ) - - parts = {} - for token in fmt_tokens: - if token == "Do": - value = match.group("value") - elif token == "W": - value = (match.group("year"), match.group("week"), match.group("day")) - else: - value = match.group(token) - self._parse_token(token, value, parts) - - return self._build_datetime(parts) - - def _generate_pattern_re(self, fmt): - - # fmt is a string of tokens like 'YYYY-MM-DD' - # we construct a new string by replacing each - # token by its pattern: - # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' - tokens = [] - offset = 0 - - # Escape all special RegEx chars - escaped_fmt = re.escape(fmt) - - # Extract the bracketed expressions to be reinserted later. - escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) - - # Any number of S is the same as one. - # TODO: allow users to specify the number of digits to parse - escaped_fmt = re.sub(r"S+", "S", escaped_fmt) - - escaped_data = re.findall(self._ESCAPE_RE, fmt) - - fmt_pattern = escaped_fmt - - for m in self._FORMAT_RE.finditer(escaped_fmt): - token = m.group(0) - try: - input_re = self._input_re_map[token] - except KeyError: - raise ParserError("Unrecognized token '{}'".format(token)) - input_pattern = "(?P<{}>{})".format(token, input_re.pattern) - tokens.append(token) - # a pattern doesn't have the same length as the token - # it replaces! We keep the difference in the offset variable. - # This works because the string is scanned left-to-right and matches - # are returned in the order found by finditer. - fmt_pattern = ( - fmt_pattern[: m.start() + offset] - + input_pattern - + fmt_pattern[m.end() + offset :] - ) - offset += len(input_pattern) - (m.end() - m.start()) - - final_fmt_pattern = "" - split_fmt = fmt_pattern.split(r"\#") - - # Due to the way Python splits, 'split_fmt' will always be longer - for i in range(len(split_fmt)): - final_fmt_pattern += split_fmt[i] - if i < len(escaped_data): - final_fmt_pattern += escaped_data[i][1:-1] - - # Wrap final_fmt_pattern in a custom word boundary to strictly - # match the formatting pattern and filter out date and time formats - # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah, - # blah1998-09-12blah. The custom word boundary matches every character - # that is not a whitespace character to allow for searching for a date - # and time string in a natural language sentence. Therefore, searching - # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will - # work properly. - # Certain punctuation before or after the target pattern such as - # "1998-09-12," is permitted. For the full list of valid punctuation, - # see the documentation. - - starting_word_boundary = ( - r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") - r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers - ) - ending_word_boundary = ( - r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time - r"(?!\S))" # Don't allow any non-whitespace character after the punctuation - ) - bounded_fmt_pattern = r"{}{}{}".format( - starting_word_boundary, final_fmt_pattern, ending_word_boundary - ) - - return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) - - def _parse_token(self, token, value, parts): - - if token == "YYYY": - parts["year"] = int(value) - - elif token == "YY": - value = int(value) - parts["year"] = 1900 + value if value > 68 else 2000 + value - - elif token in ["MMMM", "MMM"]: - parts["month"] = self.locale.month_number(value.lower()) - - elif token in ["MM", "M"]: - parts["month"] = int(value) - - elif token in ["DDDD", "DDD"]: - parts["day_of_year"] = int(value) - - elif token in ["DD", "D"]: - parts["day"] = int(value) - - elif token == "Do": - parts["day"] = int(value) - - elif token == "dddd": - # locale day names are 1-indexed - day_of_week = [x.lower() for x in self.locale.day_names].index( - value.lower() - ) - parts["day_of_week"] = day_of_week - 1 - - elif token == "ddd": - # locale day abbreviations are 1-indexed - day_of_week = [x.lower() for x in self.locale.day_abbreviations].index( - value.lower() - ) - parts["day_of_week"] = day_of_week - 1 - - elif token.upper() in ["HH", "H"]: - parts["hour"] = int(value) - - elif token in ["mm", "m"]: - parts["minute"] = int(value) - - elif token in ["ss", "s"]: - parts["second"] = int(value) - - elif token == "S": - # We have the *most significant* digits of an arbitrary-precision integer. - # We want the six most significant digits as an integer, rounded. - # IDEA: add nanosecond support somehow? Need datetime support for it first. - value = value.ljust(7, str("0")) - - # floating-point (IEEE-754) defaults to half-to-even rounding - seventh_digit = int(value[6]) - if seventh_digit == 5: - rounding = int(value[5]) % 2 - elif seventh_digit > 5: - rounding = 1 - else: - rounding = 0 - - parts["microsecond"] = int(value[:6]) + rounding - - elif token == "X": - parts["timestamp"] = float(value) - - elif token == "x": - parts["expanded_timestamp"] = int(value) - - elif token in ["ZZZ", "ZZ", "Z"]: - parts["tzinfo"] = TzinfoParser.parse(value) - - elif token in ["a", "A"]: - if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): - parts["am_pm"] = "am" - elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): - parts["am_pm"] = "pm" - - elif token == "W": - parts["weekdate"] = value - - @staticmethod - def _build_datetime(parts): - - weekdate = parts.get("weekdate") - - if weekdate is not None: - # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that - year, week = int(weekdate[0]), int(weekdate[1]) - - if weekdate[2] is not None: - day = int(weekdate[2]) - else: - # day not given, default to 1 - day = 1 - - dt = iso_to_gregorian(year, week, day) - parts["year"] = dt.year - parts["month"] = dt.month - parts["day"] = dt.day - - timestamp = parts.get("timestamp") - - if timestamp is not None: - return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) - - expanded_timestamp = parts.get("expanded_timestamp") - - if expanded_timestamp is not None: - return datetime.fromtimestamp( - normalize_timestamp(expanded_timestamp), - tz=tz.tzutc(), - ) - - day_of_year = parts.get("day_of_year") - - if day_of_year is not None: - year = parts.get("year") - month = parts.get("month") - if year is None: - raise ParserError( - "Year component is required with the DDD and DDDD tokens." - ) - - if month is not None: - raise ParserError( - "Month component is not allowed with the DDD and DDDD tokens." - ) - - date_string = "{}-{}".format(year, day_of_year) - try: - dt = datetime.strptime(date_string, "%Y-%j") - except ValueError: - raise ParserError( - "The provided day of year '{}' is invalid.".format(day_of_year) - ) - - parts["year"] = dt.year - parts["month"] = dt.month - parts["day"] = dt.day - - day_of_week = parts.get("day_of_week") - day = parts.get("day") - - # If day is passed, ignore day of week - if day_of_week is not None and day is None: - year = parts.get("year", 1970) - month = parts.get("month", 1) - day = 1 - - # dddd => first day of week after epoch - # dddd YYYY => first day of week in specified year - # dddd MM YYYY => first day of week in specified year and month - # dddd MM => first day after epoch in specified month - next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week) - parts["year"] = next_weekday_dt.year - parts["month"] = next_weekday_dt.month - parts["day"] = next_weekday_dt.day - - am_pm = parts.get("am_pm") - hour = parts.get("hour", 0) - - if am_pm == "pm" and hour < 12: - hour += 12 - elif am_pm == "am" and hour == 12: - hour = 0 - - # Support for midnight at the end of day - if hour == 24: - if parts.get("minute", 0) != 0: - raise ParserError("Midnight at the end of day must not contain minutes") - if parts.get("second", 0) != 0: - raise ParserError("Midnight at the end of day must not contain seconds") - if parts.get("microsecond", 0) != 0: - raise ParserError( - "Midnight at the end of day must not contain microseconds" - ) - hour = 0 - day_increment = 1 - else: - day_increment = 0 - - # account for rounding up to 1000000 - microsecond = parts.get("microsecond", 0) - if microsecond == 1000000: - microsecond = 0 - second_increment = 1 - else: - second_increment = 0 - - increment = timedelta(days=day_increment, seconds=second_increment) - - return ( - datetime( - year=parts.get("year", 1), - month=parts.get("month", 1), - day=parts.get("day", 1), - hour=hour, - minute=parts.get("minute", 0), - second=parts.get("second", 0), - microsecond=microsecond, - tzinfo=parts.get("tzinfo"), - ) - + increment - ) - - def _parse_multiformat(self, string, formats): - - _datetime = None - - for fmt in formats: - try: - _datetime = self.parse(string, fmt) - break - except ParserMatchError: - pass - - if _datetime is None: - raise ParserError( - "Could not match input '{}' to any of the following formats: {}".format( - string, ", ".join(formats) - ) - ) - - return _datetime - - # generates a capture group of choices separated by an OR operator - @staticmethod - def _generate_choice_re(choices, flags=0): - return re.compile(r"({})".format("|".join(choices)), flags=flags) - - -class TzinfoParser(object): - _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") - - @classmethod - def parse(cls, tzinfo_string): - - tzinfo = None - - if tzinfo_string == "local": - tzinfo = tz.tzlocal() - - elif tzinfo_string in ["utc", "UTC", "Z"]: - tzinfo = tz.tzutc() - - else: - - iso_match = cls._TZINFO_RE.match(tzinfo_string) - - if iso_match: - sign, hours, minutes = iso_match.groups() - if minutes is None: - minutes = 0 - seconds = int(hours) * 3600 + int(minutes) * 60 - - if sign == "-": - seconds *= -1 - - tzinfo = tz.tzoffset(None, seconds) - - else: - tzinfo = tz.gettz(tzinfo_string) - - if tzinfo is None: - raise ParserError( - 'Could not parse timezone expression "{}"'.format(tzinfo_string) - ) - - return tzinfo diff --git a/client/ayon_core/vendor/python/python_2/arrow/util.py b/client/ayon_core/vendor/python/python_2/arrow/util.py deleted file mode 100644 index acce8878df..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/util.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import datetime -import numbers - -from dateutil.rrule import WEEKLY, rrule - -from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US - - -def next_weekday(start_date, weekday): - """Get next weekday from the specified start date. - - :param start_date: Datetime object representing the start date. - :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday). - :return: Datetime object corresponding to the next weekday after start_date. - - Usage:: - - # Get first Monday after epoch - >>> next_weekday(datetime(1970, 1, 1), 0) - 1970-01-05 00:00:00 - - # Get first Thursday after epoch - >>> next_weekday(datetime(1970, 1, 1), 3) - 1970-01-01 00:00:00 - - # Get first Sunday after epoch - >>> next_weekday(datetime(1970, 1, 1), 6) - 1970-01-04 00:00:00 - """ - if weekday < 0 or weekday > 6: - raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") - return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] - - -def total_seconds(td): - """Get total seconds for timedelta.""" - return td.total_seconds() - - -def is_timestamp(value): - """Check if value is a valid timestamp.""" - if isinstance(value, bool): - return False - if not ( - isinstance(value, numbers.Integral) - or isinstance(value, float) - or isinstance(value, str) - ): - return False - try: - float(value) - return True - except ValueError: - return False - - -def normalize_timestamp(timestamp): - """Normalize millisecond and microsecond timestamps into normal timestamps.""" - if timestamp > MAX_TIMESTAMP: - if timestamp < MAX_TIMESTAMP_MS: - timestamp /= 1e3 - elif timestamp < MAX_TIMESTAMP_US: - timestamp /= 1e6 - else: - raise ValueError( - "The specified timestamp '{}' is too large.".format(timestamp) - ) - return timestamp - - -# Credit to https://stackoverflow.com/a/1700069 -def iso_to_gregorian(iso_year, iso_week, iso_day): - """Converts an ISO week date tuple into a datetime object.""" - - if not 1 <= iso_week <= 53: - raise ValueError("ISO Calendar week value must be between 1-53.") - - if not 1 <= iso_day <= 7: - raise ValueError("ISO Calendar day value must be between 1-7") - - # The first week of the year always contains 4 Jan. - fourth_jan = datetime.date(iso_year, 1, 4) - delta = datetime.timedelta(fourth_jan.isoweekday() - 1) - year_start = fourth_jan - delta - gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) - - return gregorian - - -def validate_bounds(bounds): - if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": - raise ValueError( - 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' - ) - - -# Python 2.7 / 3.0+ definitions for isstr function. - -try: # pragma: no cover - basestring - - def isstr(s): - return isinstance(s, basestring) # noqa: F821 - - -except NameError: # pragma: no cover - - def isstr(s): - return isinstance(s, str) - - -__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] diff --git a/client/ayon_core/vendor/python/python_2/attr/__init__.py b/client/ayon_core/vendor/python/python_2/attr/__init__.py deleted file mode 100644 index f95c96dd57..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import sys - -from functools import partial - -from . import converters, exceptions, filters, setters, validators -from ._cmp import cmp_using -from ._config import get_run_validators, set_run_validators -from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types -from ._make import ( - NOTHING, - Attribute, - Factory, - attrib, - attrs, - fields, - fields_dict, - make_class, - validate, -) -from ._version_info import VersionInfo - - -__version__ = "21.4.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - -s = attributes = attrs -ib = attr = attrib -dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) - -__all__ = [ - "Attribute", - "Factory", - "NOTHING", - "asdict", - "assoc", - "astuple", - "attr", - "attrib", - "attributes", - "attrs", - "cmp_using", - "converters", - "evolve", - "exceptions", - "fields", - "fields_dict", - "filters", - "get_run_validators", - "has", - "ib", - "make_class", - "resolve_types", - "s", - "set_run_validators", - "setters", - "validate", - "validators", -] - -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - - __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/client/ayon_core/vendor/python/python_2/attr/__init__.pyi b/client/ayon_core/vendor/python/python_2/attr/__init__.pyi deleted file mode 100644 index c0a2126503..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/__init__.pyi +++ /dev/null @@ -1,484 +0,0 @@ -import sys - -from typing import ( - Any, - Callable, - Dict, - Generic, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - overload, -) - -# `import X as X` is required to make these public -from . import converters as converters -from . import exceptions as exceptions -from . import filters as filters -from . import setters as setters -from . import validators as validators -from ._version_info import VersionInfo - -__version__: str -__version_info__: VersionInfo -__title__: str -__description__: str -__url__: str -__uri__: str -__author__: str -__email__: str -__license__: str -__copyright__: str - -_T = TypeVar("_T") -_C = TypeVar("_C", bound=type) - -_EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] -_ConverterType = Callable[[Any], Any] -_FilterType = Callable[[Attribute[_T], _T], bool] -_ReprType = Callable[[Any], str] -_ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] -_OnSetAttrArgType = Union[ - _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType -] -_FieldTransformer = Callable[ - [type, List[Attribute[Any]]], List[Attribute[Any]] -] -_CompareWithType = Callable[[Any, Any], bool] -# FIXME: in reality, if multiple validators are passed they must be in a list -# or tuple, but those are invariant and so would prevent subtypes of -# _ValidatorType from working when passed in a list or tuple. -_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] - -# _make -- - -NOTHING: object - -# NOTE: Factory lies about its return type to make this possible: -# `x: List[int] # = Factory(list)` -# Work around mypy issue #4554 in the common case by using an overload. -if sys.version_info >= (3, 8): - from typing import Literal - @overload - def Factory(factory: Callable[[], _T]) -> _T: ... - @overload - def Factory( - factory: Callable[[Any], _T], - takes_self: Literal[True], - ) -> _T: ... - @overload - def Factory( - factory: Callable[[], _T], - takes_self: Literal[False], - ) -> _T: ... - -else: - @overload - def Factory(factory: Callable[[], _T]) -> _T: ... - @overload - def Factory( - factory: Union[Callable[[Any], _T], Callable[[], _T]], - takes_self: bool = ..., - ) -> _T: ... - -# Static type inference support via __dataclass_transform__ implemented as per: -# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md -# This annotation must be applied to all overloads of "define" and "attrs" -# -# NOTE: This is a typing construct and does not exist at runtime. Extensions -# wrapping attrs decorators should declare a separate __dataclass_transform__ -# signature in the extension module using the specification linked above to -# provide pyright support. -def __dataclass_transform__( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), -) -> Callable[[_T], _T]: ... - -class Attribute(Generic[_T]): - name: str - default: Optional[_T] - validator: Optional[_ValidatorType[_T]] - repr: _ReprArgType - cmp: _EqOrderType - eq: _EqOrderType - order: _EqOrderType - hash: Optional[bool] - init: bool - converter: Optional[_ConverterType] - metadata: Dict[Any, Any] - type: Optional[Type[_T]] - kw_only: bool - on_setattr: _OnSetAttrType - def evolve(self, **changes: Any) -> "Attribute[Any]": ... - -# NOTE: We had several choices for the annotation to use for type arg: -# 1) Type[_T] -# - Pros: Handles simple cases correctly -# - Cons: Might produce less informative errors in the case of conflicting -# TypeVars e.g. `attr.ib(default='bad', type=int)` -# 2) Callable[..., _T] -# - Pros: Better error messages than #1 for conflicting TypeVars -# - Cons: Terrible error messages for validator checks. -# e.g. attr.ib(type=int, validator=validate_str) -# -> error: Cannot infer function type argument -# 3) type (and do all of the work in the mypy plugin) -# - Pros: Simple here, and we could customize the plugin with our own errors. -# - Cons: Would need to write mypy plugin code to handle all the cases. -# We chose option #1. - -# `attr` lies about its return type to make the following possible: -# attr() -> Any -# attr(8) -> int -# attr(validator=) -> Whatever the callable expects. -# This makes this type of assignments possible: -# x: int = attr(8) -# -# This form catches explicit None or no default but with no other arguments -# returns Any. -@overload -def attrib( - default: None = ..., - validator: None = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ..., - factory: None = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... - -# This form catches an explicit None or no default and infers the type from the -# other arguments. -@overload -def attrib( - default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form catches an explicit default argument. -@overload -def attrib( - default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form covers type=non-Type: e.g. forward references (str), Any -@overload -def attrib( - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: object = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... -@overload -def field( - *, - default: None = ..., - validator: None = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: None = ..., - factory: None = ..., - kw_only: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... - -# This form catches an explicit None or no default and infers the type from the -# other arguments. -@overload -def field( - *, - default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form catches an explicit default argument. -@overload -def field( - *, - default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form covers type=non-Type: e.g. forward references (str), Any -@overload -def field( - *, - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... -@overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) -def attrs( - maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - auto_detect: bool = ..., - collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) -def attrs( - maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - auto_detect: bool = ..., - collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... -@overload -@__dataclass_transform__(field_descriptors=(attrib, field)) -def define( - maybe_cls: _C, - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@__dataclass_transform__(field_descriptors=(attrib, field)) -def define( - maybe_cls: None = ..., - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... - -mutable = define -frozen = define # they differ only in their defaults - -# TODO: add support for returning NamedTuple from the mypy plugin -class _Fields(Tuple[Attribute[Any], ...]): - def __getattr__(self, name: str) -> Attribute[Any]: ... - -def fields(cls: type) -> _Fields: ... -def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... -def validate(inst: Any) -> None: ... -def resolve_types( - cls: _C, - globalns: Optional[Dict[str, Any]] = ..., - localns: Optional[Dict[str, Any]] = ..., - attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... - -# TODO: add support for returning a proper attrs class from the mypy plugin -# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', -# [attr.ib()])` is valid -def make_class( - name: str, - attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], - bases: Tuple[type, ...] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - collect_by_mro: bool = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., -) -> type: ... - -# _funcs -- - -# TODO: add support for returning TypedDict from the mypy plugin -# FIXME: asdict/astuple do not honor their factory args. Waiting on one of -# these: -# https://github.com/python/mypy/issues/4236 -# https://github.com/python/typing/issues/253 -# XXX: remember to fix attrs.asdict/astuple too! -def asdict( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., - retain_collection_types: bool = ..., - value_serializer: Optional[ - Callable[[type, Attribute[Any], Any], Any] - ] = ..., - tuple_keys: Optional[bool] = ..., -) -> Dict[str, Any]: ... - -# TODO: add support for returning NamedTuple from the mypy plugin -def astuple( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - tuple_factory: Type[Sequence[Any]] = ..., - retain_collection_types: bool = ..., -) -> Tuple[Any, ...]: ... -def has(cls: type) -> bool: ... -def assoc(inst: _T, **changes: Any) -> _T: ... -def evolve(inst: _T, **changes: Any) -> _T: ... - -# _config -- - -def set_run_validators(run: bool) -> None: ... -def get_run_validators() -> bool: ... - -# aliases -- - -s = attributes = attrs -ib = attr = attrib -dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/client/ayon_core/vendor/python/python_2/attr/_cmp.py b/client/ayon_core/vendor/python/python_2/attr/_cmp.py deleted file mode 100644 index 6cffa4dbab..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_cmp.py +++ /dev/null @@ -1,154 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import functools - -from ._compat import new_class -from ._make import _make_ne - - -_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} - - -def cmp_using( - eq=None, - lt=None, - le=None, - gt=None, - ge=None, - require_same_type=True, - class_name="Comparable", -): - """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. - - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. - - :param bool require_same_type: When `True`, equality and ordering methods - will return `NotImplemented` if objects are not of the same type. - - :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. - - See `comparison` for more details. - - .. versionadded:: 21.1.0 - """ - - body = { - "__slots__": ["value"], - "__init__": _make_init(), - "_requirements": [], - "_is_comparable_to": _is_comparable_to, - } - - # Add operations. - num_order_functions = 0 - has_eq_function = False - - if eq is not None: - has_eq_function = True - body["__eq__"] = _make_operator("eq", eq) - body["__ne__"] = _make_ne() - - if lt is not None: - num_order_functions += 1 - body["__lt__"] = _make_operator("lt", lt) - - if le is not None: - num_order_functions += 1 - body["__le__"] = _make_operator("le", le) - - if gt is not None: - num_order_functions += 1 - body["__gt__"] = _make_operator("gt", gt) - - if ge is not None: - num_order_functions += 1 - body["__ge__"] = _make_operator("ge", ge) - - type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) - - # Add same type requirement. - if require_same_type: - type_._requirements.append(_check_same_type) - - # Add total ordering if at least one operation was defined. - if 0 < num_order_functions < 4: - if not has_eq_function: - # functools.total_ordering requires __eq__ to be defined, - # so raise early error here to keep a nice stack. - raise ValueError( - "eq must be define is order to complete ordering from " - "lt, le, gt, ge." - ) - type_ = functools.total_ordering(type_) - - return type_ - - -def _make_init(): - """ - Create __init__ method. - """ - - def __init__(self, value): - """ - Initialize object with *value*. - """ - self.value = value - - return __init__ - - -def _make_operator(name, func): - """ - Create operator method. - """ - - def method(self, other): - if not self._is_comparable_to(other): - return NotImplemented - - result = func(self.value, other.value) - if result is NotImplemented: - return NotImplemented - - return result - - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], - ) - - return method - - -def _is_comparable_to(self, other): - """ - Check whether `other` is comparable to `self`. - """ - for func in self._requirements: - if not func(self, other): - return False - return True - - -def _check_same_type(self, other): - """ - Return True if *self* and *other* are of the same type, False otherwise. - """ - return other.value.__class__ is self.value.__class__ diff --git a/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi b/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi deleted file mode 100644 index e71aaff7a1..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Type - -from . import _CompareWithType - -def cmp_using( - eq: Optional[_CompareWithType], - lt: Optional[_CompareWithType], - le: Optional[_CompareWithType], - gt: Optional[_CompareWithType], - ge: Optional[_CompareWithType], - require_same_type: bool, - class_name: str, -) -> Type: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/_compat.py b/client/ayon_core/vendor/python/python_2/attr/_compat.py deleted file mode 100644 index dc0cb02b64..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_compat.py +++ /dev/null @@ -1,261 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import platform -import sys -import threading -import types -import warnings - - -PY2 = sys.version_info[0] == 2 -PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 -PY310 = sys.version_info[:2] >= (3, 10) - - -if PYPY or PY36: - ordered_dict = dict -else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - -if PY2: - from collections import Mapping, Sequence - - from UserDict import IterableUserDict - - # We 'bundle' isclass instead of using inspect as importing inspect is - # fairly expensive (order of 10-15 ms for a modern machine in 2016) - def isclass(klass): - return isinstance(klass, (type, types.ClassType)) - - def new_class(name, bases, kwds, exec_body): - """ - A minimal stub of types.new_class that we need for make_class. - """ - ns = {} - exec_body(ns) - - return type(name, bases, ns) - - # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. - TYPE = "type" - - def iteritems(d): - return d.iteritems() - - # Python 2 is bereft of a read-only dict proxy, so we make one! - class ReadOnlyDict(IterableUserDict): - """ - Best-effort read-only dict wrapper. - """ - - def __setitem__(self, key, val): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item assignment" - ) - - def update(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'update'" - ) - - def __delitem__(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item deletion" - ) - - def clear(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'clear'" - ) - - def pop(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'pop'" - ) - - def popitem(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'popitem'" - ) - - def setdefault(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'setdefault'" - ) - - def __repr__(self): - # Override to be identical to the Python 3 version. - return "mappingproxy(" + repr(self.data) + ")" - - def metadata_proxy(d): - res = ReadOnlyDict() - res.data.update(d) # We blocked update, so we have to do it like this. - return res - - def just_warn(*args, **kw): # pragma: no cover - """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. - """ - -else: # Python 3 and later. - from collections.abc import Mapping, Sequence # noqa - - def just_warn(*args, **kw): - """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. - """ - warnings.warn( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - RuntimeWarning, - stacklevel=2, - ) - - def isclass(klass): - return isinstance(klass, type) - - TYPE = "class" - - def iteritems(d): - return d.items() - - new_class = types.new_class - - def metadata_proxy(d): - return types.MappingProxyType(dict(d)) - - -def make_set_closure_cell(): - """Return a function of two arguments (cell, value) which sets - the value stored in the closure cell `cell` to `value`. - """ - # pypy makes this easy. (It also supports the logic below, but - # why not do the easy/fast thing?) - if PYPY: - - def set_closure_cell(cell, value): - cell.__setstate__((value,)) - - return set_closure_cell - - # Otherwise gotta do it the hard way. - - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - if PY2: - co = set_first_cellvar_to.func_code - else: - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. - if sys.version_info >= (3, 8): - # CPython 3.8+ has an incompatible CodeType signature - # (added a posonlyargcount argument) but also added - # CodeType.replace() to do this without counting parameters. - set_first_freevar_code = co.replace( - co_cellvars=co.co_freevars, co_freevars=co.co_cellvars - ) - else: - args = [co.co_argcount] - if not PY2: - args.append(co.co_kwonlyargcount) - args.extend( - [ - co.co_nlocals, - co.co_stacksize, - co.co_flags, - co.co_code, - co.co_consts, - co.co_names, - co.co_varnames, - co.co_filename, - co.co_name, - co.co_firstlineno, - co.co_lnotab, - # These two arguments are reversed: - co.co_cellvars, - co.co_freevars, - ] - ) - set_first_freevar_code = types.CodeType(*args) - - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) - - # Make sure it works on this interpreter: - def make_func_with_cell(): - x = None - - def func(): - return x # pragma: no cover - - return func - - if PY2: - cell = make_func_with_cell().func_closure[0] - else: - cell = make_func_with_cell().__closure__[0] - set_closure_cell(cell, 100) - if cell.cell_contents != 100: - raise AssertionError # pragma: no cover - - except Exception: - return just_warn - else: - return set_closure_cell - - -set_closure_cell = make_set_closure_cell() - -# Thread-local global to track attrs instances which are already being repr'd. -# This is needed because there is no other (thread-safe) way to pass info -# about the instances that are already being repr'd through the call stack -# in order to ensure we don't perform infinite recursion. -# -# For instance, if an instance contains a dict which contains that instance, -# we need to know that we're already repr'ing the outside instance from within -# the dict's repr() call. -# -# This lives here rather than in _make.py so that the functions in _make.py -# don't have a direct reference to the thread-local in their globals dict. -# If they have such a reference, it breaks cloudpickle. -repr_context = threading.local() diff --git a/client/ayon_core/vendor/python/python_2/attr/_config.py b/client/ayon_core/vendor/python/python_2/attr/_config.py deleted file mode 100644 index fc9be29d00..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_config.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - - -__all__ = ["set_run_validators", "get_run_validators"] - -_run_validators = True - - -def set_run_validators(run): - """ - Set whether or not validators are run. By default, they are run. - - .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` - instead. - """ - if not isinstance(run, bool): - raise TypeError("'run' must be bool.") - global _run_validators - _run_validators = run - - -def get_run_validators(): - """ - Return whether or not validators are run. - - .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` - instead. - """ - return _run_validators diff --git a/client/ayon_core/vendor/python/python_2/attr/_funcs.py b/client/ayon_core/vendor/python/python_2/attr/_funcs.py deleted file mode 100644 index 4c90085a40..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_funcs.py +++ /dev/null @@ -1,422 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import copy - -from ._compat import iteritems -from ._make import NOTHING, _obj_setattr, fields -from .exceptions import AttrsAttributeNotFoundError - - -def asdict( - inst, - recurse=True, - filter=None, - dict_factory=dict, - retain_collection_types=False, - value_serializer=None, -): - """ - Return the ``attrs`` attribute values of *inst* as a dict. - - Optionally recurse into other ``attrs``-decorated classes. - - :param inst: Instance of an ``attrs``-decorated class. - :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable dict_factory: A callable to produce dictionaries from. For - example, to produce ordered dictionaries instead of normal Python - dictionaries, pass in ``collections.OrderedDict``. - :param bool retain_collection_types: Do not convert to ``list`` when - encountering an attribute whose type is ``tuple`` or ``set``. Only - meaningful if ``recurse`` is ``True``. - :param Optional[callable] value_serializer: A hook that is called for every - attribute or dict key/value. It receives the current instance, field - and value and must return the (updated) value. The hook is run *after* - the optional *filter* has been applied. - - :rtype: return type of *dict_factory* - - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 16.0.0 *dict_factory* - .. versionadded:: 16.1.0 *retain_collection_types* - .. versionadded:: 20.3.0 *value_serializer* - .. versionadded:: 21.3.0 If a dict has a collection for a key, it is - serialized as a tuple. - """ - attrs = fields(inst.__class__) - rv = dict_factory() - for a in attrs: - v = getattr(inst, a.name) - if filter is not None and not filter(a, v): - continue - - if value_serializer is not None: - v = value_serializer(inst, a, v) - - if recurse is True: - if has(v.__class__): - rv[a.name] = asdict( - v, - recurse=True, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - elif isinstance(v, (tuple, list, set, frozenset)): - cf = v.__class__ if retain_collection_types is True else list - rv[a.name] = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in v - ] - ) - elif isinstance(v, dict): - df = dict_factory - rv[a.name] = df( - ( - _asdict_anything( - kk, - is_key=True, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - _asdict_anything( - vv, - is_key=False, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - ) - for kk, vv in iteritems(v) - ) - else: - rv[a.name] = v - else: - rv[a.name] = v - return rv - - -def _asdict_anything( - val, - is_key, - filter, - dict_factory, - retain_collection_types, - value_serializer, -): - """ - ``asdict`` only works on attrs instances, this works on anything. - """ - if getattr(val.__class__, "__attrs_attrs__", None) is not None: - # Attrs class. - rv = asdict( - val, - recurse=True, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - elif isinstance(val, (tuple, list, set, frozenset)): - if retain_collection_types is True: - cf = val.__class__ - elif is_key: - cf = tuple - else: - cf = list - - rv = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in val - ] - ) - elif isinstance(val, dict): - df = dict_factory - rv = df( - ( - _asdict_anything( - kk, - is_key=True, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - _asdict_anything( - vv, - is_key=False, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - ) - for kk, vv in iteritems(val) - ) - else: - rv = val - if value_serializer is not None: - rv = value_serializer(None, None, rv) - - return rv - - -def astuple( - inst, - recurse=True, - filter=None, - tuple_factory=tuple, - retain_collection_types=False, -): - """ - Return the ``attrs`` attribute values of *inst* as a tuple. - - Optionally recurse into other ``attrs``-decorated classes. - - :param inst: Instance of an ``attrs``-decorated class. - :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable tuple_factory: A callable to produce tuples from. For - example, to produce lists instead of tuples. - :param bool retain_collection_types: Do not convert to ``list`` - or ``dict`` when encountering an attribute which type is - ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is - ``True``. - - :rtype: return type of *tuple_factory* - - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 16.2.0 - """ - attrs = fields(inst.__class__) - rv = [] - retain = retain_collection_types # Very long. :/ - for a in attrs: - v = getattr(inst, a.name) - if filter is not None and not filter(a, v): - continue - if recurse is True: - if has(v.__class__): - rv.append( - astuple( - v, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - ) - elif isinstance(v, (tuple, list, set, frozenset)): - cf = v.__class__ if retain is True else list - rv.append( - cf( - [ - astuple( - j, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(j.__class__) - else j - for j in v - ] - ) - ) - elif isinstance(v, dict): - df = v.__class__ if retain is True else dict - rv.append( - df( - ( - astuple( - kk, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(kk.__class__) - else kk, - astuple( - vv, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(vv.__class__) - else vv, - ) - for kk, vv in iteritems(v) - ) - ) - else: - rv.append(v) - else: - rv.append(v) - - return rv if tuple_factory is list else tuple_factory(rv) - - -def has(cls): - """ - Check whether *cls* is a class with ``attrs`` attributes. - - :param type cls: Class to introspect. - :raise TypeError: If *cls* is not a class. - - :rtype: bool - """ - return getattr(cls, "__attrs_attrs__", None) is not None - - -def assoc(inst, **changes): - """ - Copy *inst* and apply *changes*. - - :param inst: Instance of a class with ``attrs`` attributes. - :param changes: Keyword changes in the new copy. - - :return: A copy of inst with *changes* incorporated. - - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. deprecated:: 17.1.0 - Use `attrs.evolve` instead if you can. - This function will not be removed du to the slightly different approach - compared to `attrs.evolve`. - """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) - new = copy.copy(inst) - attrs = fields(inst.__class__) - for k, v in iteritems(changes): - a = getattr(attrs, k, NOTHING) - if a is NOTHING: - raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) - ) - _obj_setattr(new, k, v) - return new - - -def evolve(inst, **changes): - """ - Create a new instance, based on *inst* with *changes* applied. - - :param inst: Instance of a class with ``attrs`` attributes. - :param changes: Keyword changes in the new copy. - - :return: A copy of inst with *changes* incorporated. - - :raise TypeError: If *attr_name* couldn't be found in the class - ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 17.1.0 - """ - cls = inst.__class__ - attrs = fields(cls) - for a in attrs: - if not a.init: - continue - attr_name = a.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] - if init_name not in changes: - changes[init_name] = getattr(inst, attr_name) - - return cls(**changes) - - -def resolve_types(cls, globalns=None, localns=None, attribs=None): - """ - Resolve any strings and forward annotations in type annotations. - - This is only required if you need concrete types in `Attribute`'s *type* - field. In other words, you don't need to resolve your types if you only - use them for static type checking. - - With no arguments, names will be looked up in the module in which the class - was created. If this is not what you want, e.g. if the name only exists - inside a method, you may pass *globalns* or *localns* to specify other - dictionaries in which to look up these names. See the docs of - `typing.get_type_hints` for more details. - - :param type cls: Class to resolve. - :param Optional[dict] globalns: Dictionary containing global variables. - :param Optional[dict] localns: Dictionary containing local variables. - :param Optional[list] attribs: List of attribs for the given class. - This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class and you didn't pass any attribs. - :raise NameError: If types cannot be resolved because of missing variables. - - :returns: *cls* so you can use this function also as a class decorator. - Please note that you have to apply it **after** `attrs.define`. That - means the decorator has to come in the line **before** `attrs.define`. - - .. versionadded:: 20.1.0 - .. versionadded:: 21.1.0 *attribs* - - """ - # Since calling get_type_hints is expensive we cache whether we've - # done it already. - if getattr(cls, "__attrs_types_resolved__", None) != cls: - import typing - - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) - for field in fields(cls) if attribs is None else attribs: - if field.name in hints: - # Since fields have been frozen we must work around it. - _obj_setattr(field, "type", hints[field.name]) - # We store the class we resolved so that subclasses know they haven't - # been resolved. - cls.__attrs_types_resolved__ = cls - - # Return the class so you can use it as a decorator too. - return cls diff --git a/client/ayon_core/vendor/python/python_2/attr/_make.py b/client/ayon_core/vendor/python/python_2/attr/_make.py deleted file mode 100644 index d46f8a3e7a..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_make.py +++ /dev/null @@ -1,3173 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import copy -import inspect -import linecache -import sys -import warnings - -from operator import itemgetter - -# We need to import _compat itself in addition to the _compat members to avoid -# having the thread-local in the globals here. -from . import _compat, _config, setters -from ._compat import ( - HAS_F_STRINGS, - PY2, - PY310, - PYPY, - isclass, - iteritems, - metadata_proxy, - new_class, - ordered_dict, - set_closure_cell, -) -from .exceptions import ( - DefaultAlreadySetError, - FrozenInstanceError, - NotAnAttrsClassError, - PythonTooOldError, - UnannotatedAttributeError, -) - - -if not PY2: - import typing - - -# This is used at least twice, so cache it here. -_obj_setattr = object.__setattr__ -_init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) -_classvar_prefixes = ( - "typing.ClassVar", - "t.ClassVar", - "ClassVar", - "typing_extensions.ClassVar", -) -# we don't use a double-underscore prefix because that triggers -# name mangling when trying to create a slot for the field -# (when slots=True) -_hash_cache_field = "_attrs_cached_hash" - -_empty_metadata_singleton = metadata_proxy({}) - -# Unique object for unequivocal getattr() defaults. -_sentinel = object() - -_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) - - -class _Nothing(object): - """ - Sentinel class to indicate the lack of a value when ``None`` is ambiguous. - - ``_Nothing`` is a singleton. There is only ever one of it. - - .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. - """ - - _singleton = None - - def __new__(cls): - if _Nothing._singleton is None: - _Nothing._singleton = super(_Nothing, cls).__new__(cls) - return _Nothing._singleton - - def __repr__(self): - return "NOTHING" - - def __bool__(self): - return False - - def __len__(self): - return 0 # __bool__ for Python 2 - - -NOTHING = _Nothing() -""" -Sentinel to indicate the lack of a value when ``None`` is ambiguous. -""" - - -class _CacheHashWrapper(int): - """ - An integer subclass that pickles / copies as None - - This is used for non-slots classes with ``cache_hash=True``, to avoid - serializing a potentially (even likely) invalid hash value. Since ``None`` - is the default value for uncalculated hashes, whenever this is copied, - the copy's value for the hash should automatically reset. - - See GH #613 for more details. - """ - - if PY2: - # For some reason `type(None)` isn't callable in Python 2, but we don't - # actually need a constructor for None objects, we just need any - # available function that returns None. - def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): - return _none_constructor, _args - - else: - - def __reduce__(self, _none_constructor=type(None), _args=()): - return _none_constructor, _args - - -def attrib( - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=None, - init=True, - metadata=None, - type=None, - converter=None, - factory=None, - kw_only=False, - eq=None, - order=None, - on_setattr=None, -): - """ - Create a new attribute on a class. - - .. warning:: - - Does *not* do anything unless the class is also decorated with - `attr.s`! - - :param default: A value that is used if an ``attrs``-generated ``__init__`` - is used and no value is passed while instantiating or the attribute is - excluded using ``init=False``. - - If the value is an instance of `attrs.Factory`, its callable will be - used to construct a new value (useful for mutable data types like lists - or dicts). - - If a default is not set (or set manually to `attrs.NOTHING`), a value - *must* be supplied when instantiating; otherwise a `TypeError` - will be raised. - - The default can also be set using decorator notation as shown below. - - :type default: Any value - - :param callable factory: Syntactic sugar for - ``default=attr.Factory(factory)``. - - :param validator: `callable` that is called by ``attrs``-generated - ``__init__`` methods after the instance has been initialized. They - receive the initialized instance, the :func:`~attrs.Attribute`, and the - passed value. - - The return value is *not* inspected so the validator has to throw an - exception itself. - - If a `list` is passed, its items are treated as validators and must - all pass. - - Validators can be globally disabled and re-enabled using - `get_run_validators`. - - The validator can also be set using decorator notation as shown below. - - :type validator: `callable` or a `list` of `callable`\\ s. - - :param repr: Include this attribute in the generated ``__repr__`` - method. If ``True``, include the attribute; if ``False``, omit it. By - default, the built-in ``repr()`` function is used. To override how the - attribute value is formatted, pass a ``callable`` that takes a single - value and returns a string. Note that the resulting string is used - as-is, i.e. it will be used directly *instead* of calling ``repr()`` - (the default). - :type repr: a `bool` or a `callable` to use a custom function. - - :param eq: If ``True`` (default), include this attribute in the - generated ``__eq__`` and ``__ne__`` methods that check two instances - for equality. To override how the attribute value is compared, - pass a ``callable`` that takes a single value and returns the value - to be compared. - :type eq: a `bool` or a `callable`. - - :param order: If ``True`` (default), include this attributes in the - generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. - To override how the attribute value is ordered, - pass a ``callable`` that takes a single value and returns the value - to be ordered. - :type order: a `bool` or a `callable`. - - :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the - same value. Must not be mixed with *eq* or *order*. - :type cmp: a `bool` or a `callable`. - - :param Optional[bool] hash: Include this attribute in the generated - ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This - is the correct behavior according the Python spec. Setting this value - to anything else than ``None`` is *discouraged*. - :param bool init: Include this attribute in the generated ``__init__`` - method. It is possible to set this to ``False`` and set a default - value. In that case this attributed is unconditionally initialized - with the specified default value or factory. - :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value - to the desired format. It is given the passed-in value, and the - returned value will be used as the new value of the attribute. The - value is converted before being passed to the validator, if any. - :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see `PEP 526 `_). - This argument is provided for backward compatibility. - Regardless of the approach used, the type will be stored on - ``Attribute.type``. - - Please note that ``attrs`` doesn't do anything with this metadata by - itself. You can use it as part of your own code or for - `static type checking `. - :param kw_only: Make this attribute keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param on_setattr: Allows to overwrite the *on_setattr* setting from - `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. - Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this - attribute -- regardless of the setting in `attr.s`. - :type on_setattr: `callable`, or a list of callables, or `None`, or - `attrs.setters.NO_OP` - - .. versionadded:: 15.2.0 *convert* - .. versionadded:: 16.3.0 *metadata* - .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. - .. versionchanged:: 17.1.0 - *hash* is ``None`` and therefore mirrors *eq* by default. - .. versionadded:: 17.3.0 *type* - .. deprecated:: 17.4.0 *convert* - .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated - *convert* to achieve consistency with other noun-based arguments. - .. versionadded:: 18.1.0 - ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. - .. versionadded:: 18.2.0 *kw_only* - .. versionchanged:: 19.2.0 *convert* keyword argument removed. - .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. - .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. - .. versionadded:: 19.2.0 *eq* and *order* - .. versionadded:: 20.1.0 *on_setattr* - .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 - .. versionchanged:: 21.1.0 - *eq*, *order*, and *cmp* also accept a custom callable - .. versionchanged:: 21.1.0 *cmp* undeprecated - """ - eq, eq_key, order, order_key = _determine_attrib_eq_order( - cmp, eq, order, True - ) - - if hash is not None and hash is not True and hash is not False: - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - - if factory is not None: - if default is not NOTHING: - raise ValueError( - "The `default` and `factory` arguments are mutually " - "exclusive." - ) - if not callable(factory): - raise ValueError("The `factory` argument must be a callable.") - default = Factory(factory) - - if metadata is None: - metadata = {} - - # Apply syntactic sugar by auto-wrapping. - if isinstance(on_setattr, (list, tuple)): - on_setattr = setters.pipe(*on_setattr) - - if validator and isinstance(validator, (list, tuple)): - validator = and_(*validator) - - if converter and isinstance(converter, (list, tuple)): - converter = pipe(*converter) - - return _CountingAttr( - default=default, - validator=validator, - repr=repr, - cmp=None, - hash=hash, - init=init, - converter=converter, - metadata=metadata, - type=type, - kw_only=kw_only, - eq=eq, - eq_key=eq_key, - order=order, - order_key=order_key, - on_setattr=on_setattr, - ) - - -def _compile_and_eval(script, globs, locs=None, filename=""): - """ - "Exec" the script with the given global (globs) and local (locs) variables. - """ - bytecode = compile(script, filename, "exec") - eval(bytecode, globs, locs) - - -def _make_method(name, script, filename, globs=None): - """ - Create the method with the script given and return the method object. - """ - locs = {} - if globs is None: - globs = {} - - # In order of debuggers like PDB being able to step through the code, - # we add a fake linecache entry. - count = 1 - base_filename = filename - while True: - linecache_tuple = ( - len(script), - None, - script.splitlines(True), - filename, - ) - old_val = linecache.cache.setdefault(filename, linecache_tuple) - if old_val == linecache_tuple: - break - else: - filename = "{}-{}>".format(base_filename[:-1], count) - count += 1 - - _compile_and_eval(script, globs, locs, filename) - - return locs[name] - - -def _make_attr_tuple_class(cls_name, attr_names): - """ - Create a tuple subclass to hold `Attribute`s for an `attrs` class. - - The subclass is a bare tuple with properties for names. - - class MyClassAttributes(tuple): - __slots__ = () - x = property(itemgetter(0)) - """ - attr_class_name = "{}Attributes".format(cls_name) - attr_class_template = [ - "class {}(tuple):".format(attr_class_name), - " __slots__ = ()", - ] - if attr_names: - for i, attr_name in enumerate(attr_names): - attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) - ) - else: - attr_class_template.append(" pass") - globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property} - _compile_and_eval("\n".join(attr_class_template), globs) - return globs[attr_class_name] - - -# Tuple class for extracted attributes from a class definition. -# `base_attrs` is a subset of `attrs`. -_Attributes = _make_attr_tuple_class( - "_Attributes", - [ - # all attributes to build dunder methods for - "attrs", - # attributes that have been inherited - "base_attrs", - # map inherited attributes to their originating classes - "base_attrs_map", - ], -) - - -def _is_class_var(annot): - """ - Check whether *annot* is a typing.ClassVar. - - The string comparison hack is used to avoid evaluating all string - annotations which would put attrs-based classes at a performance - disadvantage compared to plain old classes. - """ - annot = str(annot) - - # Annotation can be quoted. - if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): - annot = annot[1:-1] - - return annot.startswith(_classvar_prefixes) - - -def _has_own_attribute(cls, attrib_name): - """ - Check whether *cls* defines *attrib_name* (and doesn't just inherit it). - - Requires Python 3. - """ - attr = getattr(cls, attrib_name, _sentinel) - if attr is _sentinel: - return False - - for base_cls in cls.__mro__[1:]: - a = getattr(base_cls, attrib_name, None) - if attr is a: - return False - - return True - - -def _get_annotations(cls): - """ - Get annotations for *cls*. - """ - if _has_own_attribute(cls, "__annotations__"): - return cls.__annotations__ - - return {} - - -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - -def _collect_base_attrs(cls, taken_attr_names): - """ - Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. - """ - base_attrs = [] - base_attr_map = {} # A dictionary of base attrs to their classes. - - # Traverse the MRO and collect attributes. - for base_cls in reversed(cls.__mro__[1:-1]): - for a in getattr(base_cls, "__attrs_attrs__", []): - if a.inherited or a.name in taken_attr_names: - continue - - a = a.evolve(inherited=True) - base_attrs.append(a) - base_attr_map[a.name] = base_cls - - # For each name, only keep the freshest definition i.e. the furthest at the - # back. base_attr_map is fine because it gets overwritten with every new - # instance. - filtered = [] - seen = set() - for a in reversed(base_attrs): - if a.name in seen: - continue - filtered.insert(0, a) - seen.add(a.name) - - return filtered, base_attr_map - - -def _collect_base_attrs_broken(cls, taken_attr_names): - """ - Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. - - N.B. *taken_attr_names* will be mutated. - - Adhere to the old incorrect behavior. - - Notably it collects from the front and considers inherited attributes which - leads to the buggy behavior reported in #428. - """ - base_attrs = [] - base_attr_map = {} # A dictionary of base attrs to their classes. - - # Traverse the MRO and collect attributes. - for base_cls in cls.__mro__[1:-1]: - for a in getattr(base_cls, "__attrs_attrs__", []): - if a.name in taken_attr_names: - continue - - a = a.evolve(inherited=True) - taken_attr_names.add(a.name) - base_attrs.append(a) - base_attr_map[a.name] = base_cls - - return base_attrs, base_attr_map - - -def _transform_attrs( - cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer -): - """ - Transform all `_CountingAttr`s on a class into `Attribute`s. - - If *these* is passed, use that and don't look for them on the class. - - *collect_by_mro* is True, collect them in the correct MRO order, otherwise - use the old -- incorrect -- order. See #428. - - Return an `_Attributes`. - """ - cd = cls.__dict__ - anns = _get_annotations(cls) - - if these is not None: - ca_list = [(name, ca) for name, ca in iteritems(these)] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) - elif auto_attribs is True: - ca_names = { - name - for name, attr in cd.items() - if isinstance(attr, _CountingAttr) - } - ca_list = [] - annot_names = set() - for attr_name, type in anns.items(): - if _is_class_var(type): - continue - annot_names.add(attr_name) - a = cd.get(attr_name, NOTHING) - - if not isinstance(a, _CountingAttr): - if a is NOTHING: - a = attrib() - else: - a = attrib(default=a) - ca_list.append((attr_name, a)) - - unannotated = ca_names - annot_names - if len(unannotated) > 0: - raise UnannotatedAttributeError( - "The following `attr.ib`s lack a type annotation: " - + ", ".join( - sorted(unannotated, key=lambda n: cd.get(n).counter) - ) - + "." - ) - else: - ca_list = sorted( - ( - (name, attr) - for name, attr in cd.items() - if isinstance(attr, _CountingAttr) - ), - key=lambda e: e[1].counter, - ) - - own_attrs = [ - Attribute.from_counting_attr( - name=attr_name, ca=ca, type=anns.get(attr_name) - ) - for attr_name, ca in ca_list - ] - - if collect_by_mro: - base_attrs, base_attr_map = _collect_base_attrs( - cls, {a.name for a in own_attrs} - ) - else: - base_attrs, base_attr_map = _collect_base_attrs_broken( - cls, {a.name for a in own_attrs} - ) - - if kw_only: - own_attrs = [a.evolve(kw_only=True) for a in own_attrs] - base_attrs = [a.evolve(kw_only=True) for a in base_attrs] - - attrs = base_attrs + own_attrs - - # Mandatory vs non-mandatory attr order only matters when they are part of - # the __init__ signature and when they aren't kw_only (which are moved to - # the end and can be mandatory or non-mandatory in any order, as they will - # be specified as keyword args anyway). Check the order of those attrs: - had_default = False - for a in (a for a in attrs if a.init is not False and a.kw_only is False): - if had_default is True and a.default is NOTHING: - raise ValueError( - "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) - ) - - if had_default is False and a.default is not NOTHING: - had_default = True - - if field_transformer is not None: - attrs = field_transformer(cls, attrs) - - # Create AttrsClass *after* applying the field_transformer since it may - # add or remove attributes! - attr_names = [a.name for a in attrs] - AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) - - return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) - - -if PYPY: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return - - raise FrozenInstanceError() - -else: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() - - -def _frozen_delattrs(self, name): - """ - Attached to frozen classes as __delattr__. - """ - raise FrozenInstanceError() - - -class _ClassBuilder(object): - """ - Iteratively build *one* class. - """ - - __slots__ = ( - "_attr_names", - "_attrs", - "_base_attr_map", - "_base_names", - "_cache_hash", - "_cls", - "_cls_dict", - "_delete_attribs", - "_frozen", - "_has_pre_init", - "_has_post_init", - "_is_exc", - "_on_setattr", - "_slots", - "_weakref_slot", - "_wrote_own_setattr", - "_has_custom_setattr", - ) - - def __init__( - self, - cls, - these, - slots, - frozen, - weakref_slot, - getstate_setstate, - auto_attribs, - kw_only, - cache_hash, - is_exc, - collect_by_mro, - on_setattr, - has_custom_setattr, - field_transformer, - ): - attrs, base_attrs, base_map = _transform_attrs( - cls, - these, - auto_attribs, - kw_only, - collect_by_mro, - field_transformer, - ) - - self._cls = cls - self._cls_dict = dict(cls.__dict__) if slots else {} - self._attrs = attrs - self._base_names = set(a.name for a in base_attrs) - self._base_attr_map = base_map - self._attr_names = tuple(a.name for a in attrs) - self._slots = slots - self._frozen = frozen - self._weakref_slot = weakref_slot - self._cache_hash = cache_hash - self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) - self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) - self._delete_attribs = not bool(these) - self._is_exc = is_exc - self._on_setattr = on_setattr - - self._has_custom_setattr = has_custom_setattr - self._wrote_own_setattr = False - - self._cls_dict["__attrs_attrs__"] = self._attrs - - if frozen: - self._cls_dict["__setattr__"] = _frozen_setattrs - self._cls_dict["__delattr__"] = _frozen_delattrs - - self._wrote_own_setattr = True - elif on_setattr in ( - _ng_default_on_setattr, - setters.validate, - setters.convert, - ): - has_validator = has_converter = False - for a in attrs: - if a.validator is not None: - has_validator = True - if a.converter is not None: - has_converter = True - - if has_validator and has_converter: - break - if ( - ( - on_setattr == _ng_default_on_setattr - and not (has_validator or has_converter) - ) - or (on_setattr == setters.validate and not has_validator) - or (on_setattr == setters.convert and not has_converter) - ): - # If class-level on_setattr is set to convert + validate, but - # there's no field to convert or validate, pretend like there's - # no on_setattr. - self._on_setattr = None - - if getstate_setstate: - ( - self._cls_dict["__getstate__"], - self._cls_dict["__setstate__"], - ) = self._make_getstate_setstate() - - def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) - - def build_class(self): - """ - Finalize class based on the accumulated configuration. - - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: - return self._patch_original_class() - - def _patch_original_class(self): - """ - Apply accumulated methods and return the class. - """ - cls = self._cls - base_names = self._base_names - - # Clean class of attribute definitions (`attr.ib()`s). - if self._delete_attribs: - for name in self._attr_names: - if ( - name not in base_names - and getattr(cls, name, _sentinel) is not _sentinel - ): - try: - delattr(cls, name) - except AttributeError: - # This can happen if a base class defines a class - # variable and we want to set an attribute with the - # same name by using only a type annotation. - pass - - # Attach our dunder methods. - for name, value in self._cls_dict.items(): - setattr(cls, name, value) - - # If we've inherited an attrs __setattr__ and don't write our own, - # reset it to object's. - if not self._wrote_own_setattr and getattr( - cls, "__attrs_own_setattr__", False - ): - cls.__attrs_own_setattr__ = False - - if not self._has_custom_setattr: - cls.__setattr__ = object.__setattr__ - - return cls - - def _create_slots_class(self): - """ - Build and return a new class with a `__slots__` attribute. - """ - cd = { - k: v - for k, v in iteritems(self._cls_dict) - if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") - } - - # If our class doesn't have its own implementation of __setattr__ - # (either from the user or by us), check the bases, if one of them has - # an attrs-made __setattr__, that needs to be reset. We don't walk the - # MRO because we only care about our immediate base classes. - # XXX: This can be confused by subclassing a slotted attrs class with - # XXX: a non-attrs class and subclass the resulting class with an attrs - # XXX: class. See `test_slotted_confused` for details. For now that's - # XXX: OK with us. - if not self._wrote_own_setattr: - cd["__attrs_own_setattr__"] = False - - if not self._has_custom_setattr: - for base_cls in self._cls.__bases__: - if base_cls.__dict__.get("__attrs_own_setattr__", False): - cd["__setattr__"] = object.__setattr__ - break - - # Traverse the MRO to collect existing slots - # and check for an existing __weakref__. - existing_slots = dict() - weakref_inherited = False - for base_cls in self._cls.__mro__[1:-1]: - if base_cls.__dict__.get("__weakref__", None) is not None: - weakref_inherited = True - existing_slots.update( - { - name: getattr(base_cls, name) - for name in getattr(base_cls, "__slots__", []) - } - ) - - base_names = set(self._base_names) - - names = self._attr_names - if ( - self._weakref_slot - and "__weakref__" not in getattr(self._cls, "__slots__", ()) - and "__weakref__" not in names - and not weakref_inherited - ): - names += ("__weakref__",) - - # We only add the names of attributes that aren't inherited. - # Setting __slots__ to inherited attributes wastes memory. - slot_names = [name for name in names if name not in base_names] - # There are slots for attributes from current class - # that are defined in parent classes. - # As their descriptors may be overriden by a child class, - # we collect them here and update the class dict - reused_slots = { - slot: slot_descriptor - for slot, slot_descriptor in iteritems(existing_slots) - if slot in slot_names - } - slot_names = [name for name in slot_names if name not in reused_slots] - cd.update(reused_slots) - if self._cache_hash: - slot_names.append(_hash_cache_field) - cd["__slots__"] = tuple(slot_names) - - qualname = getattr(self._cls, "__qualname__", None) - if qualname is not None: - cd["__qualname__"] = qualname - - # Create new class based on old class and our methods. - cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) - - # The following is a fix for - # . On Python 3, - # if a method mentions `__class__` or uses the no-arg super(), the - # compiler will bake a reference to the class in the method itself - # as `method.__closure__`. Since we replace the class with a - # clone, we rewrite these references so it keeps working. - for item in cls.__dict__.values(): - if isinstance(item, (classmethod, staticmethod)): - # Class- and staticmethods hide their functions inside. - # These might need to be rewritten as well. - closure_cells = getattr(item.__func__, "__closure__", None) - elif isinstance(item, property): - # Workaround for property `super()` shortcut (PY3-only). - # There is no universal way for other descriptors. - closure_cells = getattr(item.fget, "__closure__", None) - else: - closure_cells = getattr(item, "__closure__", None) - - if not closure_cells: # Catch None or the empty list. - continue - for cell in closure_cells: - try: - match = cell.cell_contents is self._cls - except ValueError: # ValueError: Cell is empty - pass - else: - if match: - set_closure_cell(cell, cls) - - return cls - - def add_repr(self, ns): - self._cls_dict["__repr__"] = self._add_method_dunders( - _make_repr(self._attrs, ns, self._cls) - ) - return self - - def add_str(self): - repr = self._cls_dict.get("__repr__") - if repr is None: - raise ValueError( - "__str__ can only be generated if a __repr__ exists." - ) - - def __str__(self): - return self.__repr__() - - self._cls_dict["__str__"] = self._add_method_dunders(__str__) - return self - - def _make_getstate_setstate(self): - """ - Create custom __setstate__ and __getstate__ methods. - """ - # __weakref__ is not writable. - state_attr_names = tuple( - an for an in self._attr_names if an != "__weakref__" - ) - - def slots_getstate(self): - """ - Automatically created by attrs. - """ - return tuple(getattr(self, name) for name in state_attr_names) - - hash_caching_enabled = self._cache_hash - - def slots_setstate(self, state): - """ - Automatically created by attrs. - """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) - - # The hash code cache is not included when the object is - # serialized, but it still needs to be initialized to None to - # indicate that the first call to __hash__ should be a cache - # miss. - if hash_caching_enabled: - __bound_setattr(_hash_cache_field, None) - - return slots_getstate, slots_setstate - - def make_unhashable(self): - self._cls_dict["__hash__"] = None - return self - - def add_hash(self): - self._cls_dict["__hash__"] = self._add_method_dunders( - _make_hash( - self._cls, - self._attrs, - frozen=self._frozen, - cache_hash=self._cache_hash, - ) - ) - - return self - - def add_init(self): - self._cls_dict["__init__"] = self._add_method_dunders( - _make_init( - self._cls, - self._attrs, - self._has_pre_init, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, - self._on_setattr, - attrs_init=False, - ) - ) - - return self - - def add_match_args(self): - self._cls_dict["__match_args__"] = tuple( - field.name - for field in self._attrs - if field.init and not field.kw_only - ) - - def add_attrs_init(self): - self._cls_dict["__attrs_init__"] = self._add_method_dunders( - _make_init( - self._cls, - self._attrs, - self._has_pre_init, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, - self._on_setattr, - attrs_init=True, - ) - ) - - return self - - def add_eq(self): - cd = self._cls_dict - - cd["__eq__"] = self._add_method_dunders( - _make_eq(self._cls, self._attrs) - ) - cd["__ne__"] = self._add_method_dunders(_make_ne()) - - return self - - def add_order(self): - cd = self._cls_dict - - cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( - self._add_method_dunders(meth) - for meth in _make_order(self._cls, self._attrs) - ) - - return self - - def add_setattr(self): - if self._frozen: - return self - - sa_attrs = {} - for a in self._attrs: - on_setattr = a.on_setattr or self._on_setattr - if on_setattr and on_setattr is not setters.NO_OP: - sa_attrs[a.name] = a, on_setattr - - if not sa_attrs: - return self - - if self._has_custom_setattr: - # We need to write a __setattr__ but there already is one! - raise ValueError( - "Can't combine custom __setattr__ with on_setattr hooks." - ) - - # docstring comes from _add_method_dunders - def __setattr__(self, name, val): - try: - a, hook = sa_attrs[name] - except KeyError: - nval = val - else: - nval = hook(self, a, val) - - _obj_setattr(self, name, nval) - - self._cls_dict["__attrs_own_setattr__"] = True - self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) - self._wrote_own_setattr = True - - return self - - def _add_method_dunders(self, method): - """ - Add __module__ and __qualname__ to a *method* if possible. - """ - try: - method.__module__ = self._cls.__module__ - except AttributeError: - pass - - try: - method.__qualname__ = ".".join( - (self._cls.__qualname__, method.__name__) - ) - except AttributeError: - pass - - try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, - ) - except AttributeError: - pass - - return method - - -_CMP_DEPRECATION = ( - "The usage of `cmp` is deprecated and will be removed on or after " - "2021-06-01. Please use `eq` and `order` instead." -) - - -def _determine_attrs_eq_order(cmp, eq, order, default_eq): - """ - Validate the combination of *cmp*, *eq*, and *order*. Derive the effective - values of eq and order. If *eq* is None, set it to *default_eq*. - """ - if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") - - # cmp takes precedence due to bw-compatibility. - if cmp is not None: - return cmp, cmp - - # If left None, equality is set to the specified default and ordering - # mirrors equality. - if eq is None: - eq = default_eq - - if order is None: - order = eq - - if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") - - return eq, order - - -def _determine_attrib_eq_order(cmp, eq, order, default_eq): - """ - Validate the combination of *cmp*, *eq*, and *order*. Derive the effective - values of eq and order. If *eq* is None, set it to *default_eq*. - """ - if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") - - def decide_callable_or_boolean(value): - """ - Decide whether a key function is used. - """ - if callable(value): - value, key = True, value - else: - key = None - return value, key - - # cmp takes precedence due to bw-compatibility. - if cmp is not None: - cmp, cmp_key = decide_callable_or_boolean(cmp) - return cmp, cmp_key, cmp, cmp_key - - # If left None, equality is set to the specified default and ordering - # mirrors equality. - if eq is None: - eq, eq_key = default_eq, None - else: - eq, eq_key = decide_callable_or_boolean(eq) - - if order is None: - order, order_key = eq, eq_key - else: - order, order_key = decide_callable_or_boolean(order) - - if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") - - return eq, eq_key, order, order_key - - -def _determine_whether_to_implement( - cls, flag, auto_detect, dunders, default=True -): - """ - Check whether we should implement a set of methods for *cls*. - - *flag* is the argument passed into @attr.s like 'init', *auto_detect* the - same as passed into @attr.s and *dunders* is a tuple of attribute names - whose presence signal that the user has implemented it themselves. - - Return *default* if no reason for either for or against is found. - - auto_detect must be False on Python 2. - """ - if flag is True or flag is False: - return flag - - if flag is None and auto_detect is False: - return default - - # Logically, flag is None and auto_detect is True here. - for dunder in dunders: - if _has_own_attribute(cls, dunder): - return False - - return default - - -def attrs( - maybe_cls=None, - these=None, - repr_ns=None, - repr=None, - cmp=None, - hash=None, - init=None, - slots=False, - frozen=False, - weakref_slot=True, - str=False, - auto_attribs=False, - kw_only=False, - cache_hash=False, - auto_exc=False, - eq=None, - order=None, - auto_detect=False, - collect_by_mro=False, - getstate_setstate=None, - on_setattr=None, - field_transformer=None, - match_args=True, -): - r""" - A class decorator that adds `dunder - `_\ -methods according to the - specified attributes using `attr.ib` or the *these* argument. - - :param these: A dictionary of name to `attr.ib` mappings. This is - useful to avoid the definition of your attributes within the class body - because you can't (e.g. if you want to add ``__repr__`` methods to - Django models) or don't want to. - - If *these* is not ``None``, ``attrs`` will *not* search the class body - for attributes and will *not* remove any attributes from it. - - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. - - :type these: `dict` of `str` to `attr.ib` - - :param str repr_ns: When using nested classes, there's no way in Python 2 - to automatically detect that. Therefore it's possible to set the - namespace explicitly for a more meaningful ``repr`` output. - :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, - *order*, and *hash* arguments explicitly, assume they are set to - ``True`` **unless any** of the involved methods for one of the - arguments is implemented in the *current* class (i.e. it is *not* - inherited from some base class). - - So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* - ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible - ``__ne__`` by default, so it *should* be enough to only implement - ``__eq__`` in most cases). - - .. warning:: - - If you prevent ``attrs`` from creating the ordering methods for you - (``order=False``, e.g. by implementing ``__le__``), it becomes - *your* responsibility to make sure its ordering is sound. The best - way is to use the `functools.total_ordering` decorator. - - - Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, - *cmp*, or *hash* overrides whatever *auto_detect* would determine. - - *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - an `attrs.exceptions.PythonTooOldError`. - - :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. - :param bool str: Create a ``__str__`` method that is identical to - ``__repr__``. This is usually not necessary except for - `Exception`\ s. - :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` - and ``__ne__`` methods that check two instances for equality. - - They compare the instances as if they were tuples of their ``attrs`` - attributes if and only if the types of both classes are *identical*! - :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, - ``__gt__``, and ``__ge__`` methods that behave like *eq* above and - allow instances to be ordered. If ``None`` (default) mirror value of - *eq*. - :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* - and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method - is generated according how *eq* and *frozen* are set. - - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. - 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to - None, marking it unhashable (which it is). - 3. If *eq* is False, ``__hash__`` will be left untouched meaning the - ``__hash__`` method of the base class will be used (if base class is - ``object``, this means it will fall back to id-based hashing.). - - Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you - didn't freeze it programmatically) by passing ``True`` or not. Both of - these cases are rather special and should be used carefully. - - See our documentation on `hashing`, Python's documentation on - `object.__hash__`, and the `GitHub issue that led to the default \ - behavior `_ for more - details. - :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument - name. If a ``__attrs_pre_init__`` method exists on the class, it will - be called before the class is initialized. If a ``__attrs_post_init__`` - method exists on the class, it will be called after the class is fully - initialized. - - If ``init`` is ``False``, an ``__attrs_init__`` method will be - injected instead. This allows you to define a custom ``__init__`` - method that can do pre-init work such as ``super().__init__()``, - and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class ` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry `. - :param bool frozen: Make instances immutable after initialization. If - someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. - - .. note:: - - 1. This is achieved by installing a custom ``__setattr__`` method - on your class, so you can't implement your own. - - 2. True immutability is impossible in Python. - - 3. This *does* have a minor a runtime performance `impact - ` when initializing new instances. In other words: - ``__init__`` is slightly slower with ``frozen=True``. - - 4. If a class is frozen, you cannot modify ``self`` in - ``__attrs_post_init__`` or a self-written ``__init__``. You can - circumvent that limitation by using - ``object.__setattr__(self, "attribute_name", value)``. - - 5. Subclasses of a frozen class are frozen too. - - :param bool weakref_slot: Make instances weak-referenceable. This has no - effect unless ``slots`` is also enabled. - :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated - attributes (Python 3.6 and later only) from the class body. - - In this case, you **must** annotate every field. If ``attrs`` - encounters a field that is set to an `attr.ib` but lacks a type - annotation, an `attr.exceptions.UnannotatedAttributeError` is - raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't - want to set a type. - - If you assign a value to those attributes (e.g. ``x: int = 42``), that - value becomes the default value like if it were passed using - ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also - works as expected in most cases (see warning below). - - Attributes annotated as `typing.ClassVar`, and attributes that are - neither annotated nor set to an `attr.ib` are **ignored**. - - .. warning:: - For features that use the attribute name to create decorators (e.g. - `validators `), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. - - These errors can be quite confusing and probably the most common bug - report on our bug tracker. - - .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ - :param bool kw_only: Make all attributes keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param bool cache_hash: Ensure that the object's hash code is computed - only once and stored on the object. If this is set to ``True``, - hashing must be either explicitly or implicitly enabled for this - class. If the hash code is cached, avoid any reassignments of - fields involved in hash code computation or mutations of the objects - those fields point to after object creation. If such changes occur, - the behavior of the object's hash code is undefined. - :param bool auto_exc: If the class subclasses `BaseException` - (which implicitly includes any subclass of any exception), the - following happens to behave like a well-behaved Python exceptions - class: - - - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will - *not* remove existing implementations of ``__hash__`` or the equality - methods. It just won't add own ones.), - - all attributes that are either passed into ``__init__`` or have a - default value are additionally available as a tuple in the ``args`` - attribute, - - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` - collects attributes from base classes. The default behavior is - incorrect in certain cases of multiple inheritance. It should be on by - default but is kept off for backward-compatibility. - - See issue `#428 `_ for - more details. - - :param Optional[bool] getstate_setstate: - .. note:: - This is usually only interesting for slotted classes and you should - probably just set *auto_detect* to `True`. - - If `True`, ``__getstate__`` and - ``__setstate__`` are generated and attached to the class. This is - necessary for slotted classes to be pickleable. If left `None`, it's - `True` by default for slotted classes and ``False`` for dict classes. - - If *auto_detect* is `True`, and *getstate_setstate* is left `None`, - and **either** ``__getstate__`` or ``__setstate__`` is detected directly - on the class (i.e. not inherited), it is set to `False` (this is usually - what you want). - - :param on_setattr: A callable that is run whenever the user attempts to set - an attribute (either by assignment like ``i.x = 42`` or by using - `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments - as validators: the instance, the attribute that is being modified, and - the new value. - - If no exception is raised, the attribute is set to the return value of - the callable. - - If a list of callables is passed, they're automatically wrapped in an - `attrs.setters.pipe`. - - :param Optional[callable] field_transformer: - A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use - this, e.g., to automatically add converters or validators to - fields based on their types. See `transform-fields` for more details. - - :param bool match_args: - If `True` (default), set ``__match_args__`` on the class to support - `PEP 634 `_ (Structural - Pattern Matching). It is a tuple of all positional-only ``__init__`` - parameter names on Python 3.10 and later. Ignored on older Python - versions. - - .. versionadded:: 16.0.0 *slots* - .. versionadded:: 16.1.0 *frozen* - .. versionadded:: 16.3.0 *str* - .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. - .. versionchanged:: 17.1.0 - *hash* supports ``None`` as value which is also the default now. - .. versionadded:: 17.3.0 *auto_attribs* - .. versionchanged:: 18.1.0 - If *these* is passed, no attributes are deleted from the class body. - .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. - .. versionadded:: 18.2.0 *weakref_slot* - .. deprecated:: 18.2.0 - ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a - `DeprecationWarning` if the classes compared are subclasses of - each other. ``__eq`` and ``__ne__`` never tried to compared subclasses - to each other. - .. versionchanged:: 19.2.0 - ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider - subclasses comparable anymore. - .. versionadded:: 18.2.0 *kw_only* - .. versionadded:: 18.2.0 *cache_hash* - .. versionadded:: 19.1.0 *auto_exc* - .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. - .. versionadded:: 19.2.0 *eq* and *order* - .. versionadded:: 20.1.0 *auto_detect* - .. versionadded:: 20.1.0 *collect_by_mro* - .. versionadded:: 20.1.0 *getstate_setstate* - .. versionadded:: 20.1.0 *on_setattr* - .. versionadded:: 20.3.0 *field_transformer* - .. versionchanged:: 21.1.0 - ``init=False`` injects ``__attrs_init__`` - .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` - .. versionchanged:: 21.1.0 *cmp* undeprecated - .. versionadded:: 21.3.0 *match_args* - """ - if auto_detect and PY2: - raise PythonTooOldError( - "auto_detect only works on Python 3 and later." - ) - - eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) - hash_ = hash # work around the lack of nonlocal - - if isinstance(on_setattr, (list, tuple)): - on_setattr = setters.pipe(*on_setattr) - - def wrap(cls): - - if getattr(cls, "__class__", None) is None: - raise TypeError("attrs only works with new-style classes.") - - is_frozen = frozen or _has_frozen_base_class(cls) - is_exc = auto_exc is True and issubclass(cls, BaseException) - has_own_setattr = auto_detect and _has_own_attribute( - cls, "__setattr__" - ) - - if has_own_setattr and is_frozen: - raise ValueError("Can't freeze a class with a custom __setattr__.") - - builder = _ClassBuilder( - cls, - these, - slots, - is_frozen, - weakref_slot, - _determine_whether_to_implement( - cls, - getstate_setstate, - auto_detect, - ("__getstate__", "__setstate__"), - default=slots, - ), - auto_attribs, - kw_only, - cache_hash, - is_exc, - collect_by_mro, - on_setattr, - has_own_setattr, - field_transformer, - ) - if _determine_whether_to_implement( - cls, repr, auto_detect, ("__repr__",) - ): - builder.add_repr(repr_ns) - if str is True: - builder.add_str() - - eq = _determine_whether_to_implement( - cls, eq_, auto_detect, ("__eq__", "__ne__") - ) - if not is_exc and eq is True: - builder.add_eq() - if not is_exc and _determine_whether_to_implement( - cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") - ): - builder.add_order() - - builder.add_setattr() - - if ( - hash_ is None - and auto_detect is True - and _has_own_attribute(cls, "__hash__") - ): - hash = False - else: - hash = hash_ - if hash is not True and hash is not False and hash is not None: - # Can't use `hash in` because 1 == True for example. - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - elif hash is False or (hash is None and eq is False) or is_exc: - # Don't do anything. Should fall back to __object__'s __hash__ - # which is by id. - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) - elif hash is True or ( - hash is None and eq is True and is_frozen is True - ): - # Build a __hash__ if told so, or if it's safe. - builder.add_hash() - else: - # Raise TypeError on attempts to hash. - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) - builder.make_unhashable() - - if _determine_whether_to_implement( - cls, init, auto_detect, ("__init__",) - ): - builder.add_init() - else: - builder.add_attrs_init() - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) - - if ( - PY310 - and match_args - and not _has_own_attribute(cls, "__match_args__") - ): - builder.add_match_args() - - return builder.build_class() - - # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. - if maybe_cls is None: - return wrap - else: - return wrap(maybe_cls) - - -_attrs = attrs -""" -Internal alias so we can use it in functions that take an argument called -*attrs*. -""" - - -if PY2: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return ( - getattr(cls.__setattr__, "__module__", None) - == _frozen_setattrs.__module__ - and cls.__setattr__.__name__ == _frozen_setattrs.__name__ - ) - -else: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return cls.__setattr__ == _frozen_setattrs - - -def _generate_unique_filename(cls, func_name): - """ - Create a "filename" suitable for a function being generated. - """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), - ) - return unique_filename - - -def _make_hash(cls, attrs, frozen, cache_hash): - attrs = tuple( - a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) - ) - - tab = " " - - unique_filename = _generate_unique_filename(cls, "hash") - type_hash = hash(unique_filename) - - hash_def = "def __hash__(self" - hash_func = "hash((" - closing_braces = "))" - if not cache_hash: - hash_def += "):" - else: - if not PY2: - hash_def += ", *" - - hash_def += ( - ", _cache_wrapper=" - + "__import__('attr._make')._make._CacheHashWrapper):" - ) - hash_func = "_cache_wrapper(" + hash_func - closing_braces += ")" - - method_lines = [hash_def] - - def append_hash_computation_lines(prefix, indent): - """ - Generate the code for actually computing the hash code. - Below this will either be returned directly or used to compute - a value which is then cached, depending on the value of cache_hash - """ - - method_lines.extend( - [ - indent + prefix + hash_func, - indent + " %d," % (type_hash,), - ] - ) - - for a in attrs: - method_lines.append(indent + " self.%s," % a.name) - - method_lines.append(indent + " " + closing_braces) - - if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) - if frozen: - append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 - ) - method_lines.append(tab * 2 + ")") # close __setattr__ - else: - append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 - ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) - else: - append_hash_computation_lines("return ", tab) - - script = "\n".join(method_lines) - return _make_method("__hash__", script, unique_filename) - - -def _add_hash(cls, attrs): - """ - Add a hash method to *cls*. - """ - cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False) - return cls - - -def _make_ne(): - """ - Create __ne__ method. - """ - - def __ne__(self, other): - """ - Check equality and either forward a NotImplemented or - return the result negated. - """ - result = self.__eq__(other) - if result is NotImplemented: - return NotImplemented - - return not result - - return __ne__ - - -def _make_eq(cls, attrs): - """ - Create __eq__ method for *cls* with *attrs*. - """ - attrs = [a for a in attrs if a.eq] - - unique_filename = _generate_unique_filename(cls, "eq") - lines = [ - "def __eq__(self, other):", - " if other.__class__ is not self.__class__:", - " return NotImplemented", - ] - - # We can't just do a big self.x = other.x and... clause due to - # irregularities like nan == nan is false but (nan,) == (nan,) is true. - globs = {} - if attrs: - lines.append(" return (") - others = [" ) == ("] - for a in attrs: - if a.eq_key: - cmp_name = "_%s_key" % (a.name,) - # Add the key function to the global namespace - # of the evaluated function. - globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) - else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) - - lines += others + [" )"] - else: - lines.append(" return True") - - script = "\n".join(lines) - - return _make_method("__eq__", script, unique_filename, globs) - - -def _make_order(cls, attrs): - """ - Create ordering methods for *cls* with *attrs*. - """ - attrs = [a for a in attrs if a.order] - - def attrs_to_tuple(obj): - """ - Save us some typing. - """ - return tuple( - key(value) if key else value - for value, key in ( - (getattr(obj, a.name), a.order_key) for a in attrs - ) - ) - - def __lt__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) < attrs_to_tuple(other) - - return NotImplemented - - def __le__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) <= attrs_to_tuple(other) - - return NotImplemented - - def __gt__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) > attrs_to_tuple(other) - - return NotImplemented - - def __ge__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) >= attrs_to_tuple(other) - - return NotImplemented - - return __lt__, __le__, __gt__, __ge__ - - -def _add_eq(cls, attrs=None): - """ - Add equality methods to *cls* with *attrs*. - """ - if attrs is None: - attrs = cls.__attrs_attrs__ - - cls.__eq__ = _make_eq(cls, attrs) - cls.__ne__ = _make_ne() - - return cls - - -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False - ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs - ) - -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ - - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - qualname = getattr(real_cls, "__qualname__", None) - if qualname is not None: # pragma: no cover - # This case only happens on Python 3.5 and 3.6. We exclude - # it from coverage, because we don't want to slow down our - # test suite by running them under coverage too for this - # one line. - class_name = qualname.rsplit(">.", 1)[-1] - else: - class_name = real_cls.__name__ - else: - class_name = ns + "." + real_cls.__name__ - - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) - - return __repr__ - - -def _add_repr(cls, ns=None, attrs=None): - """ - Add a repr method to *cls*. - """ - if attrs is None: - attrs = cls.__attrs_attrs__ - - cls.__repr__ = _make_repr(attrs, ns, cls) - return cls - - -def fields(cls): - """ - Return the tuple of ``attrs`` attributes for a class. - - The tuple also allows accessing the fields by their names (see below for - examples). - - :param type cls: Class to introspect. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - :rtype: tuple (with name accessors) of `attrs.Attribute` - - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. - """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") - attrs = getattr(cls, "__attrs_attrs__", None) - if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return attrs - - -def fields_dict(cls): - """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose - keys are the attribute names. - - :param type cls: Class to introspect. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. - - .. versionadded:: 18.1.0 - """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") - attrs = getattr(cls, "__attrs_attrs__", None) - if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict(((a.name, a) for a in attrs)) - - -def validate(inst): - """ - Validate all attributes on *inst* that have a validator. - - Leaves all exceptions through. - - :param inst: Instance of a class with ``attrs`` attributes. - """ - if _config._run_validators is False: - return - - for a in fields(inst.__class__): - v = a.validator - if v is not None: - v(inst, a, getattr(inst, a.name)) - - -def _is_slot_cls(cls): - return "__slots__" in cls.__dict__ - - -def _is_slot_attr(a_name, base_attr_map): - """ - Check if the attribute name comes from a slot class. - """ - return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) - - -def _make_init( - cls, - attrs, - pre_init, - post_init, - frozen, - slots, - cache_hash, - base_attr_map, - is_exc, - cls_on_setattr, - attrs_init, -): - has_cls_on_setattr = ( - cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP - ) - - if frozen and has_cls_on_setattr: - raise ValueError("Frozen classes can't use on_setattr.") - - needs_cached_setattr = cache_hash or frozen - filtered_attrs = [] - attr_dict = {} - for a in attrs: - if not a.init and a.default is NOTHING: - continue - - filtered_attrs.append(a) - attr_dict[a.name] = a - - if a.on_setattr is not None: - if frozen is True: - raise ValueError("Frozen classes can't use on_setattr.") - - needs_cached_setattr = True - elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: - needs_cached_setattr = True - - unique_filename = _generate_unique_filename(cls, "init") - - script, globs, annotations = _attrs_to_init_script( - filtered_attrs, - frozen, - slots, - pre_init, - post_init, - cache_hash, - base_attr_map, - is_exc, - needs_cached_setattr, - has_cls_on_setattr, - attrs_init, - ) - if cls.__module__ in sys.modules: - # This makes typing.get_type_hints(CLS.__init__) resolve string types. - globs.update(sys.modules[cls.__module__].__dict__) - - globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) - - if needs_cached_setattr: - # Save the lookup overhead in __init__ if we need to circumvent - # setattr hooks. - globs["_cached_setattr"] = _obj_setattr - - init = _make_method( - "__attrs_init__" if attrs_init else "__init__", - script, - unique_filename, - globs, - ) - init.__annotations__ = annotations - - return init - - -def _setattr(attr_name, value_var, has_on_setattr): - """ - Use the cached object.setattr to set *attr_name* to *value_var*. - """ - return "_setattr('%s', %s)" % (attr_name, value_var) - - -def _setattr_with_converter(attr_name, value_var, has_on_setattr): - """ - Use the cached object.setattr to set *attr_name* to *value_var*, but run - its converter first. - """ - return "_setattr('%s', %s(%s))" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - -def _assign(attr_name, value, has_on_setattr): - """ - Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise - relegate to _setattr. - """ - if has_on_setattr: - return _setattr(attr_name, value, True) - - return "self.%s = %s" % (attr_name, value) - - -def _assign_with_converter(attr_name, value_var, has_on_setattr): - """ - Unless *attr_name* has an on_setattr hook, use normal assignment after - conversion. Otherwise relegate to _setattr_with_converter. - """ - if has_on_setattr: - return _setattr_with_converter(attr_name, value_var, True) - - return "self.%s = %s(%s)" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - -if PY2: - - def _unpack_kw_only_py2(attr_name, default=None): - """ - Unpack *attr_name* from _kw_only dict. - """ - if default is not None: - arg_default = ", %s" % default - else: - arg_default = "" - return "%s = _kw_only.pop('%s'%s)" % ( - attr_name, - attr_name, - arg_default, - ) - - def _unpack_kw_only_lines_py2(kw_only_args): - """ - Unpack all *kw_only_args* from _kw_only dict and handle errors. - - Given a list of strings "{attr_name}" and "{attr_name}={default}" - generates list of lines of code that pop attrs from _kw_only dict and - raise TypeError similar to builtin if required attr is missing or - extra key is passed. - - >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) - try: - a = _kw_only.pop('a') - b = _kw_only.pop('b', 42) - except KeyError as _key_error: - raise TypeError( - ... - if _kw_only: - raise TypeError( - ... - """ - lines = ["try:"] - lines.extend( - " " + _unpack_kw_only_py2(*arg.split("=")) - for arg in kw_only_args - ) - lines += """\ -except KeyError as _key_error: - raise TypeError( - '__init__() missing required keyword-only argument: %s' % _key_error - ) -if _kw_only: - raise TypeError( - '__init__() got an unexpected keyword argument %r' - % next(iter(_kw_only)) - ) -""".split( - "\n" - ) - return lines - - -def _attrs_to_init_script( - attrs, - frozen, - slots, - pre_init, - post_init, - cache_hash, - base_attr_map, - is_exc, - needs_cached_setattr, - has_cls_on_setattr, - attrs_init, -): - """ - Return a script of an initializer for *attrs* and a dict of globals. - - The globals are expected by the generated script. - - If *frozen* is True, we cannot set the attributes directly so we use - a cached ``object.__setattr__``. - """ - lines = [] - if pre_init: - lines.append("self.__attrs_pre_init__()") - - if needs_cached_setattr: - lines.append( - # Circumvent the __setattr__ descriptor to save one lookup per - # assignment. - # Note _setattr will be used again below if cache_hash is True - "_setattr = _cached_setattr.__get__(self, self.__class__)" - ) - - if frozen is True: - if slots is True: - fmt_setter = _setattr - fmt_setter_with_converter = _setattr_with_converter - else: - # Dict frozen classes assign directly to __dict__. - # But only if the attribute doesn't come from an ancestor slot - # class. - # Note _inst_dict will be used again below if cache_hash is True - lines.append("_inst_dict = self.__dict__") - - def fmt_setter(attr_name, value_var, has_on_setattr): - if _is_slot_attr(attr_name, base_attr_map): - return _setattr(attr_name, value_var, has_on_setattr) - - return "_inst_dict['%s'] = %s" % (attr_name, value_var) - - def fmt_setter_with_converter( - attr_name, value_var, has_on_setattr - ): - if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): - return _setattr_with_converter( - attr_name, value_var, has_on_setattr - ) - - return "_inst_dict['%s'] = %s(%s)" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - else: - # Not frozen. - fmt_setter = _assign - fmt_setter_with_converter = _assign_with_converter - - args = [] - kw_only_args = [] - attrs_to_validate = [] - - # This is a dictionary of names to validator and converter callables. - # Injecting this into __init__ globals lets us avoid lookups. - names_for_globals = {} - annotations = {"return": None} - - for a in attrs: - if a.validator: - attrs_to_validate.append(a) - - attr_name = a.name - has_on_setattr = a.on_setattr is not None or ( - a.on_setattr is not setters.NO_OP and has_cls_on_setattr - ) - arg_name = a.name.lstrip("_") - - has_factory = isinstance(a.default, Factory) - if has_factory and a.default.takes_self: - maybe_self = "self" - else: - maybe_self = "" - - if a.init is False: - if has_factory: - init_factory_name = _init_factory_pat.format(a.name) - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - init_factory_name + "(%s)" % (maybe_self,), - has_on_setattr, - ) - ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - init_factory_name + "(%s)" % (maybe_self,), - has_on_setattr, - ) - ) - names_for_globals[init_factory_name] = a.default.factory - else: - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) - ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) - ) - elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) - if a.kw_only: - kw_only_args.append(arg) - else: - args.append(arg) - - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) - - elif has_factory: - arg = "%s=NOTHING" % (arg_name,) - if a.kw_only: - kw_only_args.append(arg) - else: - args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) - - init_factory_name = _init_factory_pat.format(a.name) - if a.converter is not None: - lines.append( - " " - + fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - lines.append("else:") - lines.append( - " " - + fmt_setter_with_converter( - attr_name, - init_factory_name + "(" + maybe_self + ")", - has_on_setattr, - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append( - " " + fmt_setter(attr_name, arg_name, has_on_setattr) - ) - lines.append("else:") - lines.append( - " " - + fmt_setter( - attr_name, - init_factory_name + "(" + maybe_self + ")", - has_on_setattr, - ) - ) - names_for_globals[init_factory_name] = a.default.factory - else: - if a.kw_only: - kw_only_args.append(arg_name) - else: - args.append(arg_name) - - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) - - if a.init is True: - if a.type is not None and a.converter is None: - annotations[arg_name] = a.type - elif a.converter is not None and not PY2: - # Try to get the type from the converter. - sig = None - try: - sig = inspect.signature(a.converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - sig_params = list(sig.parameters.values()) - if ( - sig_params - and sig_params[0].annotation - is not inspect.Parameter.empty - ): - annotations[arg_name] = sig_params[0].annotation - - if attrs_to_validate: # we can skip this if there are no validators. - names_for_globals["_config"] = _config - lines.append("if _config._run_validators is True:") - for a in attrs_to_validate: - val_name = "__attr_validator_" + a.name - attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) - names_for_globals[val_name] = a.validator - names_for_globals[attr_name] = a - - if post_init: - lines.append("self.__attrs_post_init__()") - - # because this is set only after __attrs_post_init is called, a crash - # will result if post-init tries to access the hash code. This seemed - # preferable to setting this beforehand, in which case alteration to - # field values during post-init combined with post-init accessing the - # hash code would result in silent bugs. - if cache_hash: - if frozen: - if slots: - # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr('%s', %s)" - else: - # if frozen and not slots, then _inst_dict defined above - init_hash_cache = "_inst_dict['%s'] = %s" - else: - init_hash_cache = "self.%s = %s" - lines.append(init_hash_cache % (_hash_cache_field, "None")) - - # For exceptions we rely on BaseException.__init__ for proper - # initialization. - if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) - - lines.append("BaseException.__init__(self, %s)" % (vals,)) - - args = ", ".join(args) - if kw_only_args: - if PY2: - lines = _unpack_kw_only_lines_py2(kw_only_args) + lines - - args += "%s**_kw_only" % (", " if args else "",) # leading comma - else: - args += "%s*, %s" % ( - ", " if args else "", # leading comma - ", ".join(kw_only_args), # kw_only args - ) - return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", - ), - names_for_globals, - annotations, - ) - - -class Attribute(object): - """ - *Read-only* representation of an attribute. - - The class has *all* arguments of `attr.ib` (except for ``factory`` - which is only syntactic sugar for ``default=Factory(...)`` plus the - following: - - - ``name`` (`str`): The name of the attribute. - - ``inherited`` (`bool`): Whether or not that attribute has been inherited - from a base class. - - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables - that are used for comparing and ordering objects by this attribute, - respectively. These are set by passing a callable to `attr.ib`'s ``eq``, - ``order``, or ``cmp`` arguments. See also :ref:`comparison customization - `. - - Instances of this class are frequently used for introspection purposes - like: - - - `fields` returns a tuple of them. - - Validators get them passed as the first argument. - - The :ref:`field transformer ` hook receives a list of - them. - - .. versionadded:: 20.1.0 *inherited* - .. versionadded:: 20.1.0 *on_setattr* - .. versionchanged:: 20.2.0 *inherited* is not taken into account for - equality checks and hashing anymore. - .. versionadded:: 21.1.0 *eq_key* and *order_key* - - For the full version history of the fields, see `attr.ib`. - """ - - __slots__ = ( - "name", - "default", - "validator", - "repr", - "eq", - "eq_key", - "order", - "order_key", - "hash", - "init", - "metadata", - "type", - "converter", - "kw_only", - "inherited", - "on_setattr", - ) - - def __init__( - self, - name, - default, - validator, - repr, - cmp, # XXX: unused, remove along with other cmp code. - hash, - init, - inherited, - metadata=None, - type=None, - converter=None, - kw_only=False, - eq=None, - eq_key=None, - order=None, - order_key=None, - on_setattr=None, - ): - eq, eq_key, order, order_key = _determine_attrib_eq_order( - cmp, eq_key or eq, order_key or order, True - ) - - # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) - - # Despite the big red warning, people *do* instantiate `Attribute` - # themselves. - bound_setattr("name", name) - bound_setattr("default", default) - bound_setattr("validator", validator) - bound_setattr("repr", repr) - bound_setattr("eq", eq) - bound_setattr("eq_key", eq_key) - bound_setattr("order", order) - bound_setattr("order_key", order_key) - bound_setattr("hash", hash) - bound_setattr("init", init) - bound_setattr("converter", converter) - bound_setattr( - "metadata", - ( - metadata_proxy(metadata) - if metadata - else _empty_metadata_singleton - ), - ) - bound_setattr("type", type) - bound_setattr("kw_only", kw_only) - bound_setattr("inherited", inherited) - bound_setattr("on_setattr", on_setattr) - - def __setattr__(self, name, value): - raise FrozenInstanceError() - - @classmethod - def from_counting_attr(cls, name, ca, type=None): - # type holds the annotated value. deal with conflicts: - if type is None: - type = ca.type - elif ca.type is not None: - raise ValueError( - "Type annotation and type argument cannot both be present" - ) - inst_dict = { - k: getattr(ca, k) - for k in Attribute.__slots__ - if k - not in ( - "name", - "validator", - "default", - "type", - "inherited", - ) # exclude methods and deprecated alias - } - return cls( - name=name, - validator=ca._validator, - default=ca._default, - type=type, - cmp=None, - inherited=False, - **inst_dict - ) - - @property - def cmp(self): - """ - Simulate the presence of a cmp attribute and warn. - """ - warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) - - return self.eq and self.order - - # Don't use attr.evolve since fields(Attribute) doesn't work - def evolve(self, **changes): - """ - Copy *self* and apply *changes*. - - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. - - It is mainly meant to be used for `transform-fields`. - - .. versionadded:: 20.3.0 - """ - new = copy.copy(self) - - new._setattrs(changes.items()) - - return new - - # Don't use _add_pickle since fields(Attribute) doesn't work - def __getstate__(self): - """ - Play nice with pickle. - """ - return tuple( - getattr(self, name) if name != "metadata" else dict(self.metadata) - for name in self.__slots__ - ) - - def __setstate__(self, state): - """ - Play nice with pickle. - """ - self._setattrs(zip(self.__slots__, state)) - - def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in name_values_pairs: - if name != "metadata": - bound_setattr(name, value) - else: - bound_setattr( - name, - metadata_proxy(value) - if value - else _empty_metadata_singleton, - ) - - -_a = [ - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - eq=True, - order=False, - hash=(name != "metadata"), - init=True, - inherited=False, - ) - for name in Attribute.__slots__ -] - -Attribute = _add_hash( - _add_eq( - _add_repr(Attribute, attrs=_a), - attrs=[a for a in _a if a.name != "inherited"], - ), - attrs=[a for a in _a if a.hash and a.name != "inherited"], -) - - -class _CountingAttr(object): - """ - Intermediate representation of attributes that uses a counter to preserve - the order in which the attributes have been defined. - - *Internal* data structure of the attrs library. Running into is most - likely the result of a bug like a forgotten `@attr.s` decorator. - """ - - __slots__ = ( - "counter", - "_default", - "repr", - "eq", - "eq_key", - "order", - "order_key", - "hash", - "init", - "metadata", - "_validator", - "converter", - "type", - "kw_only", - "on_setattr", - ) - __attrs_attrs__ = tuple( - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=True, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ) - for name in ( - "counter", - "_default", - "repr", - "eq", - "order", - "hash", - "init", - "on_setattr", - ) - ) + ( - Attribute( - name="metadata", - default=None, - validator=None, - repr=True, - cmp=None, - hash=False, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ), - ) - cls_counter = 0 - - def __init__( - self, - default, - validator, - repr, - cmp, - hash, - init, - converter, - metadata, - type, - kw_only, - eq, - eq_key, - order, - order_key, - on_setattr, - ): - _CountingAttr.cls_counter += 1 - self.counter = _CountingAttr.cls_counter - self._default = default - self._validator = validator - self.converter = converter - self.repr = repr - self.eq = eq - self.eq_key = eq_key - self.order = order - self.order_key = order_key - self.hash = hash - self.init = init - self.metadata = metadata - self.type = type - self.kw_only = kw_only - self.on_setattr = on_setattr - - def validator(self, meth): - """ - Decorator that adds *meth* to the list of validators. - - Returns *meth* unchanged. - - .. versionadded:: 17.1.0 - """ - if self._validator is None: - self._validator = meth - else: - self._validator = and_(self._validator, meth) - return meth - - def default(self, meth): - """ - Decorator that allows to set the default for an attribute. - - Returns *meth* unchanged. - - :raises DefaultAlreadySetError: If default has been set before. - - .. versionadded:: 17.1.0 - """ - if self._default is not NOTHING: - raise DefaultAlreadySetError() - - self._default = Factory(meth, takes_self=True) - - return meth - - -_CountingAttr = _add_eq(_add_repr(_CountingAttr)) - - -class Factory(object): - """ - Stores a factory callable. - - If passed as the default value to `attrs.field`, the factory is used to - generate a new value. - - :param callable factory: A callable that takes either none or exactly one - mandatory positional argument depending on *takes_self*. - :param bool takes_self: Pass the partially initialized instance that is - being initialized as a positional argument. - - .. versionadded:: 17.1.0 *takes_self* - """ - - __slots__ = ("factory", "takes_self") - - def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ - self.factory = factory - self.takes_self = takes_self - - def __getstate__(self): - """ - Play nice with pickle. - """ - return tuple(getattr(self, name) for name in self.__slots__) - - def __setstate__(self, state): - """ - Play nice with pickle. - """ - for name, value in zip(self.__slots__, state): - setattr(self, name, value) - - -_f = [ - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - eq=True, - order=False, - hash=True, - init=True, - inherited=False, - ) - for name in Factory.__slots__ -] - -Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) - - -def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ - A quick way to create a new class called *name* with *attrs*. - - :param str name: The name for the new class. - - :param attrs: A list of names or a dictionary of mappings of names to - attributes. - - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. - :type attrs: `list` or `dict` - - :param tuple bases: Classes that the new class will subclass. - - :param attributes_arguments: Passed unmodified to `attr.s`. - - :return: A new class with *attrs*. - :rtype: type - - .. versionadded:: 17.1.0 *bases* - .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. - """ - if isinstance(attrs, dict): - cls_dict = attrs - elif isinstance(attrs, (list, tuple)): - cls_dict = dict((a, attrib()) for a in attrs) - else: - raise TypeError("attrs argument must be a dict or a list.") - - pre_init = cls_dict.pop("__attrs_pre_init__", None) - post_init = cls_dict.pop("__attrs_post_init__", None) - user_init = cls_dict.pop("__init__", None) - - body = {} - if pre_init is not None: - body["__attrs_pre_init__"] = pre_init - if post_init is not None: - body["__attrs_post_init__"] = post_init - if user_init is not None: - body["__init__"] = user_init - - type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) - - # For pickling to work, the __module__ variable needs to be set to the - # frame where the class is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython). - try: - type_.__module__ = sys._getframe(1).f_globals.get( - "__name__", "__main__" - ) - except (AttributeError, ValueError): - pass - - # We do it here for proper warnings with meaningful stacklevel. - cmp = attributes_arguments.pop("cmp", None) - ( - attributes_arguments["eq"], - attributes_arguments["order"], - ) = _determine_attrs_eq_order( - cmp, - attributes_arguments.get("eq"), - attributes_arguments.get("order"), - True, - ) - - return _attrs(these=cls_dict, **attributes_arguments)(type_) - - -# These are required by within this module so we define them here and merely -# import into .validators / .converters. - - -@attrs(slots=True, hash=True) -class _AndValidator(object): - """ - Compose many validators to a single one. - """ - - _validators = attrib() - - def __call__(self, inst, attr, value): - for v in self._validators: - v(inst, attr, value) - - -def and_(*validators): - """ - A validator that composes multiple validators into one. - - When called on a value, it runs all wrapped validators. - - :param callables validators: Arbitrary number of validators. - - .. versionadded:: 17.1.0 - """ - vals = [] - for validator in validators: - vals.extend( - validator._validators - if isinstance(validator, _AndValidator) - else [validator] - ) - - return _AndValidator(tuple(vals)) - - -def pipe(*converters): - """ - A converter that composes multiple converters into one. - - When called on a value, it runs all wrapped converters, returning the - *last* value. - - Type annotations will be inferred from the wrapped converters', if - they have any. - - :param callables converters: Arbitrary number of converters. - - .. versionadded:: 20.1.0 - """ - - def pipe_converter(val): - for converter in converters: - val = converter(val) - - return val - - if not PY2: - if not converters: - # If the converter list is empty, pipe_converter is the identity. - A = typing.TypeVar("A") - pipe_converter.__annotations__ = {"val": A, "return": A} - else: - # Get parameter type. - sig = None - try: - sig = inspect.signature(converters[0]) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if ( - params - and params[0].annotation is not inspect.Parameter.empty - ): - pipe_converter.__annotations__["val"] = params[ - 0 - ].annotation - # Get return type. - sig = None - try: - sig = inspect.signature(converters[-1]) - except (ValueError, TypeError): # inspect failed - pass - if sig and sig.return_annotation is not inspect.Signature().empty: - pipe_converter.__annotations__[ - "return" - ] = sig.return_annotation - - return pipe_converter diff --git a/client/ayon_core/vendor/python/python_2/attr/_next_gen.py b/client/ayon_core/vendor/python/python_2/attr/_next_gen.py deleted file mode 100644 index 068253688c..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_next_gen.py +++ /dev/null @@ -1,216 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. -""" - - -from functools import partial - -from . import setters -from ._funcs import asdict as _asdict -from ._funcs import astuple as _astuple -from ._make import ( - NOTHING, - _frozen_setattrs, - _ng_default_on_setattr, - attrib, - attrs, -) -from .exceptions import UnannotatedAttributeError - - -def define( - maybe_cls=None, - *, - these=None, - repr=None, - hash=None, - init=None, - slots=True, - frozen=False, - weakref_slot=True, - str=False, - auto_attribs=None, - kw_only=False, - cache_hash=False, - auto_exc=True, - eq=None, - order=False, - auto_detect=True, - getstate_setstate=None, - on_setattr=None, - field_transformer=None, - match_args=True, -): - r""" - Define an ``attrs`` class. - - Differences to the classic `attr.s` that it uses underneath: - - - Automatically detect whether or not *auto_attribs* should be `True` - (c.f. *auto_attribs* parameter). - - If *frozen* is `False`, run converters and validators when setting an - attribute by default. - - *slots=True* (see :term:`slotted classes` for potentially surprising - behaviors) - - *auto_exc=True* - - *auto_detect=True* - - *order=False* - - *match_args=True* - - Some options that were only relevant on Python 2 or were kept around for - backwards-compatibility have been removed. - - Please note that these are all defaults and you can change them as you - wish. - - :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves - exactly like `attr.s`. If left `None`, `attr.s` will try to guess: - - 1. If any attributes are annotated and no unannotated `attrs.fields`\ s - are found, it assumes *auto_attribs=True*. - 2. Otherwise it assumes *auto_attribs=False* and tries to collect - `attrs.fields`\ s. - - For now, please refer to `attr.s` for the rest of the parameters. - - .. versionadded:: 20.1.0 - .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. - """ - - def do_it(cls, auto_attribs): - return attrs( - maybe_cls=cls, - these=these, - repr=repr, - hash=hash, - init=init, - slots=slots, - frozen=frozen, - weakref_slot=weakref_slot, - str=str, - auto_attribs=auto_attribs, - kw_only=kw_only, - cache_hash=cache_hash, - auto_exc=auto_exc, - eq=eq, - order=order, - auto_detect=auto_detect, - collect_by_mro=True, - getstate_setstate=getstate_setstate, - on_setattr=on_setattr, - field_transformer=field_transformer, - match_args=match_args, - ) - - def wrap(cls): - """ - Making this a wrapper ensures this code runs during class creation. - - We also ensure that frozen-ness of classes is inherited. - """ - nonlocal frozen, on_setattr - - had_on_setattr = on_setattr not in (None, setters.NO_OP) - - # By default, mutable classes convert & validate on setattr. - if frozen is False and on_setattr is None: - on_setattr = _ng_default_on_setattr - - # However, if we subclass a frozen class, we inherit the immutability - # and disable on_setattr. - for base_cls in cls.__bases__: - if base_cls.__setattr__ is _frozen_setattrs: - if had_on_setattr: - raise ValueError( - "Frozen classes can't use on_setattr " - "(frozen-ness was inherited)." - ) - - on_setattr = setters.NO_OP - break - - if auto_attribs is not None: - return do_it(cls, auto_attribs) - - try: - return do_it(cls, True) - except UnannotatedAttributeError: - return do_it(cls, False) - - # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. - if maybe_cls is None: - return wrap - else: - return wrap(maybe_cls) - - -mutable = define -frozen = partial(define, frozen=True, on_setattr=None) - - -def field( - *, - default=NOTHING, - validator=None, - repr=True, - hash=None, - init=True, - metadata=None, - converter=None, - factory=None, - kw_only=False, - eq=None, - order=None, - on_setattr=None, -): - """ - Identical to `attr.ib`, except keyword-only and with some arguments - removed. - - .. versionadded:: 20.1.0 - """ - return attrib( - default=default, - validator=validator, - repr=repr, - hash=hash, - init=init, - metadata=metadata, - converter=converter, - factory=factory, - kw_only=kw_only, - eq=eq, - order=order, - on_setattr=on_setattr, - ) - - -def asdict(inst, *, recurse=True, filter=None, value_serializer=None): - """ - Same as `attr.asdict`, except that collections types are always retained - and dict is always used as *dict_factory*. - - .. versionadded:: 21.3.0 - """ - return _asdict( - inst=inst, - recurse=recurse, - filter=filter, - value_serializer=value_serializer, - retain_collection_types=True, - ) - - -def astuple(inst, *, recurse=True, filter=None): - """ - Same as `attr.astuple`, except that collections types are always retained - and `tuple` is always used as the *tuple_factory*. - - .. versionadded:: 21.3.0 - """ - return _astuple( - inst=inst, recurse=recurse, filter=filter, retain_collection_types=True - ) diff --git a/client/ayon_core/vendor/python/python_2/attr/_version_info.py b/client/ayon_core/vendor/python/python_2/attr/_version_info.py deleted file mode 100644 index cdaeec37a1..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_version_info.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -from functools import total_ordering - -from ._funcs import astuple -from ._make import attrib, attrs - - -@total_ordering -@attrs(eq=False, order=False, slots=True, frozen=True) -class VersionInfo(object): - """ - A version object that can be compared to tuple of length 1--4: - - >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) - True - >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) - True - >>> vi = attr.VersionInfo(19, 2, 0, "final") - >>> vi < (19, 1, 1) - False - >>> vi < (19,) - False - >>> vi == (19, 2,) - True - >>> vi == (19, 2, 1) - False - - .. versionadded:: 19.2 - """ - - year = attrib(type=int) - minor = attrib(type=int) - micro = attrib(type=int) - releaselevel = attrib(type=str) - - @classmethod - def _from_version_string(cls, s): - """ - Parse *s* and return a _VersionInfo. - """ - v = s.split(".") - if len(v) == 3: - v.append("final") - - return cls( - year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] - ) - - def _ensure_tuple(self, other): - """ - Ensure *other* is a tuple of a valid length. - - Returns a possibly transformed *other* and ourselves as a tuple of - the same length as *other*. - """ - - if self.__class__ is other.__class__: - other = astuple(other) - - if not isinstance(other, tuple): - raise NotImplementedError - - if not (1 <= len(other) <= 4): - raise NotImplementedError - - return astuple(self)[: len(other)], other - - def __eq__(self, other): - try: - us, them = self._ensure_tuple(other) - except NotImplementedError: - return NotImplemented - - return us == them - - def __lt__(self, other): - try: - us, them = self._ensure_tuple(other) - except NotImplementedError: - return NotImplemented - - # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't - # have to do anything special with releaselevel for now. - return us < them diff --git a/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi b/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi deleted file mode 100644 index 45ced08633..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi +++ /dev/null @@ -1,9 +0,0 @@ -class VersionInfo: - @property - def year(self) -> int: ... - @property - def minor(self) -> int: ... - @property - def micro(self) -> int: ... - @property - def releaselevel(self) -> str: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/converters.py b/client/ayon_core/vendor/python/python_2/attr/converters.py deleted file mode 100644 index 1fb6c05d7b..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/converters.py +++ /dev/null @@ -1,155 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful converters. -""" - -from __future__ import absolute_import, division, print_function - -from ._compat import PY2 -from ._make import NOTHING, Factory, pipe - - -if not PY2: - import inspect - import typing - - -__all__ = [ - "default_if_none", - "optional", - "pipe", - "to_bool", -] - - -def optional(converter): - """ - A converter that allows an attribute to be optional. An optional attribute - is one which can be set to ``None``. - - Type annotations will be inferred from the wrapped converter's, if it - has any. - - :param callable converter: the converter that is used for non-``None`` - values. - - .. versionadded:: 17.1.0 - """ - - def optional_converter(val): - if val is None: - return None - return converter(val) - - if not PY2: - sig = None - try: - sig = inspect.signature(converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if params and params[0].annotation is not inspect.Parameter.empty: - optional_converter.__annotations__["val"] = typing.Optional[ - params[0].annotation - ] - if sig.return_annotation is not inspect.Signature.empty: - optional_converter.__annotations__["return"] = typing.Optional[ - sig.return_annotation - ] - - return optional_converter - - -def default_if_none(default=NOTHING, factory=None): - """ - A converter that allows to replace ``None`` values by *default* or the - result of *factory*. - - :param default: Value to be used if ``None`` is passed. Passing an instance - of `attrs.Factory` is supported, however the ``takes_self`` option - is *not*. - :param callable factory: A callable that takes no parameters whose result - is used if ``None`` is passed. - - :raises TypeError: If **neither** *default* or *factory* is passed. - :raises TypeError: If **both** *default* and *factory* are passed. - :raises ValueError: If an instance of `attrs.Factory` is passed with - ``takes_self=True``. - - .. versionadded:: 18.2.0 - """ - if default is NOTHING and factory is None: - raise TypeError("Must pass either `default` or `factory`.") - - if default is not NOTHING and factory is not None: - raise TypeError( - "Must pass either `default` or `factory` but not both." - ) - - if factory is not None: - default = Factory(factory) - - if isinstance(default, Factory): - if default.takes_self: - raise ValueError( - "`takes_self` is not supported by default_if_none." - ) - - def default_if_none_converter(val): - if val is not None: - return val - - return default.factory() - - else: - - def default_if_none_converter(val): - if val is not None: - return val - - return default - - return default_if_none_converter - - -def to_bool(val): - """ - Convert "boolean" strings (e.g., from env. vars.) to real booleans. - - Values mapping to :code:`True`: - - - :code:`True` - - :code:`"true"` / :code:`"t"` - - :code:`"yes"` / :code:`"y"` - - :code:`"on"` - - :code:`"1"` - - :code:`1` - - Values mapping to :code:`False`: - - - :code:`False` - - :code:`"false"` / :code:`"f"` - - :code:`"no"` / :code:`"n"` - - :code:`"off"` - - :code:`"0"` - - :code:`0` - - :raises ValueError: for any other value. - - .. versionadded:: 21.3.0 - """ - if isinstance(val, str): - val = val.lower() - truthy = {True, "true", "t", "yes", "y", "on", "1", 1} - falsy = {False, "false", "f", "no", "n", "off", "0", 0} - try: - if val in truthy: - return True - if val in falsy: - return False - except TypeError: - # Raised when "val" is not hashable (e.g., lists) - pass - raise ValueError("Cannot convert value to bool: {}".format(val)) diff --git a/client/ayon_core/vendor/python/python_2/attr/converters.pyi b/client/ayon_core/vendor/python/python_2/attr/converters.pyi deleted file mode 100644 index 0f58088a37..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/converters.pyi +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Callable, Optional, TypeVar, overload - -from . import _ConverterType - -_T = TypeVar("_T") - -def pipe(*validators: _ConverterType) -> _ConverterType: ... -def optional(converter: _ConverterType) -> _ConverterType: ... -@overload -def default_if_none(default: _T) -> _ConverterType: ... -@overload -def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... -def to_bool(val: str) -> bool: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/exceptions.py b/client/ayon_core/vendor/python/python_2/attr/exceptions.py deleted file mode 100644 index b2f1edc32a..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/exceptions.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - - -class FrozenError(AttributeError): - """ - A frozen/immutable instance or attribute have been attempted to be - modified. - - It mirrors the behavior of ``namedtuples`` by using the same error message - and subclassing `AttributeError`. - - .. versionadded:: 20.1.0 - """ - - msg = "can't set attribute" - args = [msg] - - -class FrozenInstanceError(FrozenError): - """ - A frozen instance has been attempted to be modified. - - .. versionadded:: 16.1.0 - """ - - -class FrozenAttributeError(FrozenError): - """ - A frozen attribute has been attempted to be modified. - - .. versionadded:: 20.1.0 - """ - - -class AttrsAttributeNotFoundError(ValueError): - """ - An ``attrs`` function couldn't find an attribute that the user asked for. - - .. versionadded:: 16.2.0 - """ - - -class NotAnAttrsClassError(ValueError): - """ - A non-``attrs`` class has been passed into an ``attrs`` function. - - .. versionadded:: 16.2.0 - """ - - -class DefaultAlreadySetError(RuntimeError): - """ - A default has been set using ``attr.ib()`` and is attempted to be reset - using the decorator. - - .. versionadded:: 17.1.0 - """ - - -class UnannotatedAttributeError(RuntimeError): - """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. - - .. versionadded:: 17.3.0 - """ - - -class PythonTooOldError(RuntimeError): - """ - It was attempted to use an ``attrs`` feature that requires a newer Python - version. - - .. versionadded:: 18.2.0 - """ - - -class NotCallableError(TypeError): - """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. - - .. versionadded:: 19.2.0 - """ - - def __init__(self, msg, value): - super(TypeError, self).__init__(msg, value) - self.msg = msg - self.value = value - - def __str__(self): - return str(self.msg) diff --git a/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi b/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi deleted file mode 100644 index f2680118b4..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -class FrozenError(AttributeError): - msg: str = ... - -class FrozenInstanceError(FrozenError): ... -class FrozenAttributeError(FrozenError): ... -class AttrsAttributeNotFoundError(ValueError): ... -class NotAnAttrsClassError(ValueError): ... -class DefaultAlreadySetError(RuntimeError): ... -class UnannotatedAttributeError(RuntimeError): ... -class PythonTooOldError(RuntimeError): ... - -class NotCallableError(TypeError): - msg: str = ... - value: Any = ... - def __init__(self, msg: str, value: Any) -> None: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/filters.py b/client/ayon_core/vendor/python/python_2/attr/filters.py deleted file mode 100644 index a1978a8775..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/filters.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful filters for `attr.asdict`. -""" - -from __future__ import absolute_import, division, print_function - -from ._compat import isclass -from ._make import Attribute - - -def _split_what(what): - """ - Returns a tuple of `frozenset`s of classes and attributes. - """ - return ( - frozenset(cls for cls in what if isclass(cls)), - frozenset(cls for cls in what if isinstance(cls, Attribute)), - ) - - -def include(*what): - """ - Include *what*. - - :param what: What to include. - :type what: `list` of `type` or `attrs.Attribute`\\ s - - :rtype: `callable` - """ - cls, attrs = _split_what(what) - - def include_(attribute, value): - return value.__class__ in cls or attribute in attrs - - return include_ - - -def exclude(*what): - """ - Exclude *what*. - - :param what: What to exclude. - :type what: `list` of classes or `attrs.Attribute`\\ s. - - :rtype: `callable` - """ - cls, attrs = _split_what(what) - - def exclude_(attribute, value): - return value.__class__ not in cls and attribute not in attrs - - return exclude_ diff --git a/client/ayon_core/vendor/python/python_2/attr/filters.pyi b/client/ayon_core/vendor/python/python_2/attr/filters.pyi deleted file mode 100644 index 993866865e..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/filters.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any, Union - -from . import Attribute, _FilterType - -def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/py.typed b/client/ayon_core/vendor/python/python_2/attr/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/vendor/python/python_2/attr/setters.py b/client/ayon_core/vendor/python/python_2/attr/setters.py deleted file mode 100644 index b1cbb5d83e..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/setters.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly used hooks for on_setattr. -""" - -from __future__ import absolute_import, division, print_function - -from . import _config -from .exceptions import FrozenAttributeError - - -def pipe(*setters): - """ - Run all *setters* and return the return value of the last one. - - .. versionadded:: 20.1.0 - """ - - def wrapped_pipe(instance, attrib, new_value): - rv = new_value - - for setter in setters: - rv = setter(instance, attrib, rv) - - return rv - - return wrapped_pipe - - -def frozen(_, __, ___): - """ - Prevent an attribute to be modified. - - .. versionadded:: 20.1.0 - """ - raise FrozenAttributeError() - - -def validate(instance, attrib, new_value): - """ - Run *attrib*'s validator on *new_value* if it has one. - - .. versionadded:: 20.1.0 - """ - if _config._run_validators is False: - return new_value - - v = attrib.validator - if not v: - return new_value - - v(instance, attrib, new_value) - - return new_value - - -def convert(instance, attrib, new_value): - """ - Run *attrib*'s converter -- if it has one -- on *new_value* and return the - result. - - .. versionadded:: 20.1.0 - """ - c = attrib.converter - if c: - return c(new_value) - - return new_value - - -NO_OP = object() -""" -Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. - -Does not work in `pipe` or within lists. - -.. versionadded:: 20.1.0 -""" diff --git a/client/ayon_core/vendor/python/python_2/attr/setters.pyi b/client/ayon_core/vendor/python/python_2/attr/setters.pyi deleted file mode 100644 index 3f5603c2b0..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/setters.pyi +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any, NewType, NoReturn, TypeVar, cast - -from . import Attribute, _OnSetAttrType - -_T = TypeVar("_T") - -def frozen( - instance: Any, attribute: Attribute[Any], new_value: Any -) -> NoReturn: ... -def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... -def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... - -# convert is allowed to return Any, because they can be chained using pipe. -def convert( - instance: Any, attribute: Attribute[Any], new_value: Any -) -> Any: ... - -_NoOpType = NewType("_NoOpType", object) -NO_OP: _NoOpType diff --git a/client/ayon_core/vendor/python/python_2/attr/validators.py b/client/ayon_core/vendor/python/python_2/attr/validators.py deleted file mode 100644 index 0b0c8342f2..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/validators.py +++ /dev/null @@ -1,561 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful validators. -""" - -from __future__ import absolute_import, division, print_function - -import operator -import re - -from contextlib import contextmanager - -from ._config import get_run_validators, set_run_validators -from ._make import _AndValidator, and_, attrib, attrs -from .exceptions import NotCallableError - - -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - -__all__ = [ - "and_", - "deep_iterable", - "deep_mapping", - "disabled", - "ge", - "get_disabled", - "gt", - "in_", - "instance_of", - "is_callable", - "le", - "lt", - "matches_re", - "max_len", - "optional", - "provides", - "set_disabled", -] - - -def set_disabled(disabled): - """ - Globally disable or enable running validators. - - By default, they are run. - - :param disabled: If ``True``, disable running all validators. - :type disabled: bool - - .. warning:: - - This function is not thread-safe! - - .. versionadded:: 21.3.0 - """ - set_run_validators(not disabled) - - -def get_disabled(): - """ - Return a bool indicating whether validators are currently disabled or not. - - :return: ``True`` if validators are currently disabled. - :rtype: bool - - .. versionadded:: 21.3.0 - """ - return not get_run_validators() - - -@contextmanager -def disabled(): - """ - Context manager that disables running validators within its context. - - .. warning:: - - This context manager is not thread-safe! - - .. versionadded:: 21.3.0 - """ - set_run_validators(False) - try: - yield - finally: - set_run_validators(True) - - -@attrs(repr=False, slots=True, hash=True) -class _InstanceOfValidator(object): - type = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not isinstance(value, self.type): - raise TypeError( - "'{name}' must be {type!r} (got {value!r} that is a " - "{actual!r}).".format( - name=attr.name, - type=self.type, - actual=value.__class__, - value=value, - ), - attr, - self.type, - value, - ) - - def __repr__(self): - return "".format( - type=self.type - ) - - -def instance_of(type): - """ - A validator that raises a `TypeError` if the initializer is called - with a wrong type for this particular attribute (checks are performed using - `isinstance` therefore it's also valid to pass a tuple of types). - - :param type: The type to check for. - :type type: type or tuple of types - - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected type, and the value it - got. - """ - return _InstanceOfValidator(type) - - -@attrs(repr=False, frozen=True, slots=True) -class _MatchesReValidator(object): - pattern = attrib() - match_func = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.match_func(value): - raise ValueError( - "'{name}' must match regex {pattern!r}" - " ({value!r} doesn't)".format( - name=attr.name, pattern=self.pattern.pattern, value=value - ), - attr, - self.pattern, - value, - ) - - def __repr__(self): - return "".format( - pattern=self.pattern - ) - - -def matches_re(regex, flags=0, func=None): - r""" - A validator that raises `ValueError` if the initializer is called - with a string that doesn't match *regex*. - - :param regex: a regex string or precompiled pattern to match against - :param int flags: flags that will be passed to the underlying re function - (default 0) - :param callable func: which underlying `re` function to call (options - are `re.fullmatch`, `re.search`, `re.match`, default - is ``None`` which means either `re.fullmatch` or an emulation of - it on Python 2). For performance reasons, they won't be used directly - but on a pre-`re.compile`\ ed pattern. - - .. versionadded:: 19.2.0 - .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. - """ - fullmatch = getattr(re, "fullmatch", None) - valid_funcs = (fullmatch, None, re.search, re.match) - if func not in valid_funcs: - raise ValueError( - "'func' must be one of {}.".format( - ", ".join( - sorted( - e and e.__name__ or "None" for e in set(valid_funcs) - ) - ) - ) - ) - - if isinstance(regex, Pattern): - if flags: - raise TypeError( - "'flags' can only be used with a string pattern; " - "pass flags to re.compile() instead" - ) - pattern = regex - else: - pattern = re.compile(regex, flags) - - if func is re.match: - match_func = pattern.match - elif func is re.search: - match_func = pattern.search - elif fullmatch: - match_func = pattern.fullmatch - else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) - pattern = re.compile( - r"(?:{})\Z".format(pattern.pattern), pattern.flags - ) - match_func = pattern.match - - return _MatchesReValidator(pattern, match_func) - - -@attrs(repr=False, slots=True, hash=True) -class _ProvidesValidator(object): - interface = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.interface.providedBy(value): - raise TypeError( - "'{name}' must provide {interface!r} which {value!r} " - "doesn't.".format( - name=attr.name, interface=self.interface, value=value - ), - attr, - self.interface, - value, - ) - - def __repr__(self): - return "".format( - interface=self.interface - ) - - -def provides(interface): - """ - A validator that raises a `TypeError` if the initializer is called - with an object that does not provide the requested *interface* (checks are - performed using ``interface.providedBy(value)`` (see `zope.interface - `_). - - :param interface: The interface to check for. - :type interface: ``zope.interface.Interface`` - - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected interface, and the - value it got. - """ - return _ProvidesValidator(interface) - - -@attrs(repr=False, slots=True, hash=True) -class _OptionalValidator(object): - validator = attrib() - - def __call__(self, inst, attr, value): - if value is None: - return - - self.validator(inst, attr, value) - - def __repr__(self): - return "".format( - what=repr(self.validator) - ) - - -def optional(validator): - """ - A validator that makes an attribute optional. An optional attribute is one - which can be set to ``None`` in addition to satisfying the requirements of - the sub-validator. - - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. - - .. versionadded:: 15.1.0 - .. versionchanged:: 17.1.0 *validator* can be a list of validators. - """ - if isinstance(validator, list): - return _OptionalValidator(_AndValidator(validator)) - return _OptionalValidator(validator) - - -@attrs(repr=False, slots=True, hash=True) -class _InValidator(object): - options = attrib() - - def __call__(self, inst, attr, value): - try: - in_options = value in self.options - except TypeError: # e.g. `1 in "abc"` - in_options = False - - if not in_options: - raise ValueError( - "'{name}' must be in {options!r} (got {value!r})".format( - name=attr.name, options=self.options, value=value - ) - ) - - def __repr__(self): - return "".format( - options=self.options - ) - - -def in_(options): - """ - A validator that raises a `ValueError` if the initializer is called - with a value that does not belong in the options provided. The check is - performed using ``value in options``. - - :param options: Allowed options. - :type options: list, tuple, `enum.Enum`, ... - - :raises ValueError: With a human readable error message, the attribute (of - type `attrs.Attribute`), the expected options, and the value it - got. - - .. versionadded:: 17.1.0 - """ - return _InValidator(options) - - -@attrs(repr=False, slots=False, hash=True) -class _IsCallableValidator(object): - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not callable(value): - message = ( - "'{name}' must be callable " - "(got {value!r} that is a {actual!r})." - ) - raise NotCallableError( - msg=message.format( - name=attr.name, value=value, actual=value.__class__ - ), - value=value, - ) - - def __repr__(self): - return "" - - -def is_callable(): - """ - A validator that raises a `attr.exceptions.NotCallableError` if the - initializer is called with a value for this particular attribute - that is not callable. - - .. versionadded:: 19.1.0 - - :raises `attr.exceptions.NotCallableError`: With a human readable error - message containing the attribute (`attrs.Attribute`) name, - and the value it got. - """ - return _IsCallableValidator() - - -@attrs(repr=False, slots=True, hash=True) -class _DeepIterable(object): - member_validator = attrib(validator=is_callable()) - iterable_validator = attrib( - default=None, validator=optional(is_callable()) - ) - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if self.iterable_validator is not None: - self.iterable_validator(inst, attr, value) - - for member in value: - self.member_validator(inst, attr, member) - - def __repr__(self): - iterable_identifier = ( - "" - if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) - ) - return ( - "" - ).format( - iterable_identifier=iterable_identifier, - member=self.member_validator, - ) - - -def deep_iterable(member_validator, iterable_validator=None): - """ - A validator that performs deep validation of an iterable. - - :param member_validator: Validator to apply to iterable members - :param iterable_validator: Validator to apply to iterable itself - (optional) - - .. versionadded:: 19.1.0 - - :raises TypeError: if any sub-validators fail - """ - return _DeepIterable(member_validator, iterable_validator) - - -@attrs(repr=False, slots=True, hash=True) -class _DeepMapping(object): - key_validator = attrib(validator=is_callable()) - value_validator = attrib(validator=is_callable()) - mapping_validator = attrib(default=None, validator=optional(is_callable())) - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if self.mapping_validator is not None: - self.mapping_validator(inst, attr, value) - - for key in value: - self.key_validator(inst, attr, key) - self.value_validator(inst, attr, value[key]) - - def __repr__(self): - return ( - "" - ).format(key=self.key_validator, value=self.value_validator) - - -def deep_mapping(key_validator, value_validator, mapping_validator=None): - """ - A validator that performs deep validation of a dictionary. - - :param key_validator: Validator to apply to dictionary keys - :param value_validator: Validator to apply to dictionary values - :param mapping_validator: Validator to apply to top-level mapping - attribute (optional) - - .. versionadded:: 19.1.0 - - :raises TypeError: if any sub-validators fail - """ - return _DeepMapping(key_validator, value_validator, mapping_validator) - - -@attrs(repr=False, frozen=True, slots=True) -class _NumberValidator(object): - bound = attrib() - compare_op = attrib() - compare_func = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.compare_func(value, self.bound): - raise ValueError( - "'{name}' must be {op} {bound}: {value}".format( - name=attr.name, - op=self.compare_op, - bound=self.bound, - value=value, - ) - ) - - def __repr__(self): - return "".format( - op=self.compare_op, bound=self.bound - ) - - -def lt(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number larger or equal to *val*. - - :param val: Exclusive upper bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, "<", operator.lt) - - -def le(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number greater than *val*. - - :param val: Inclusive upper bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, "<=", operator.le) - - -def ge(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number smaller than *val*. - - :param val: Inclusive lower bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, ">=", operator.ge) - - -def gt(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number smaller or equal to *val*. - - :param val: Exclusive lower bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, ">", operator.gt) - - -@attrs(repr=False, frozen=True, slots=True) -class _MaxLengthValidator(object): - max_length = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if len(value) > self.max_length: - raise ValueError( - "Length of '{name}' must be <= {max}: {len}".format( - name=attr.name, max=self.max_length, len=len(value) - ) - ) - - def __repr__(self): - return "".format(max=self.max_length) - - -def max_len(length): - """ - A validator that raises `ValueError` if the initializer is called - with a string or iterable that is longer than *length*. - - :param int length: Maximum length of the string or iterable - - .. versionadded:: 21.3.0 - """ - return _MaxLengthValidator(length) diff --git a/client/ayon_core/vendor/python/python_2/attr/validators.pyi b/client/ayon_core/vendor/python/python_2/attr/validators.pyi deleted file mode 100644 index 5e00b85433..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/validators.pyi +++ /dev/null @@ -1,78 +0,0 @@ -from typing import ( - Any, - AnyStr, - Callable, - Container, - ContextManager, - Iterable, - List, - Mapping, - Match, - Optional, - Pattern, - Tuple, - Type, - TypeVar, - Union, - overload, -) - -from . import _ValidatorType - -_T = TypeVar("_T") -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") -_T3 = TypeVar("_T3") -_I = TypeVar("_I", bound=Iterable) -_K = TypeVar("_K") -_V = TypeVar("_V") -_M = TypeVar("_M", bound=Mapping) - -def set_disabled(run: bool) -> None: ... -def get_disabled() -> bool: ... -def disabled() -> ContextManager[None]: ... - -# To be more precise on instance_of use some overloads. -# If there are more than 3 items in the tuple then we fall back to Any -@overload -def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... -@overload -def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... -@overload -def instance_of( - type: Tuple[Type[_T1], Type[_T2]] -) -> _ValidatorType[Union[_T1, _T2]]: ... -@overload -def instance_of( - type: Tuple[Type[_T1], Type[_T2], Type[_T3]] -) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... -@overload -def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... -def provides(interface: Any) -> _ValidatorType[Any]: ... -def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] -) -> _ValidatorType[Optional[_T]]: ... -def in_(options: Container[_T]) -> _ValidatorType[_T]: ... -def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... -def matches_re( - regex: Union[Pattern[AnyStr], AnyStr], - flags: int = ..., - func: Optional[ - Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] - ] = ..., -) -> _ValidatorType[AnyStr]: ... -def deep_iterable( - member_validator: _ValidatorType[_T], - iterable_validator: Optional[_ValidatorType[_I]] = ..., -) -> _ValidatorType[_I]: ... -def deep_mapping( - key_validator: _ValidatorType[_K], - value_validator: _ValidatorType[_V], - mapping_validator: Optional[_ValidatorType[_M]] = ..., -) -> _ValidatorType[_M]: ... -def is_callable() -> _ValidatorType[_T]: ... -def lt(val: _T) -> _ValidatorType[_T]: ... -def le(val: _T) -> _ValidatorType[_T]: ... -def ge(val: _T) -> _ValidatorType[_T]: ... -def gt(val: _T) -> _ValidatorType[_T]: ... -def max_len(length: int) -> _ValidatorType[_T]: ... diff --git a/client/ayon_core/vendor/python/python_2/attrs/__init__.py b/client/ayon_core/vendor/python/python_2/attrs/__init__.py deleted file mode 100644 index a704b8b56b..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr import ( - NOTHING, - Attribute, - Factory, - __author__, - __copyright__, - __description__, - __doc__, - __email__, - __license__, - __title__, - __url__, - __version__, - __version_info__, - assoc, - cmp_using, - define, - evolve, - field, - fields, - fields_dict, - frozen, - has, - make_class, - mutable, - resolve_types, - validate, -) -from attr._next_gen import asdict, astuple - -from . import converters, exceptions, filters, setters, validators - - -__all__ = [ - "__author__", - "__copyright__", - "__description__", - "__doc__", - "__email__", - "__license__", - "__title__", - "__url__", - "__version__", - "__version_info__", - "asdict", - "assoc", - "astuple", - "Attribute", - "cmp_using", - "converters", - "define", - "evolve", - "exceptions", - "Factory", - "field", - "fields_dict", - "fields", - "filters", - "frozen", - "has", - "make_class", - "mutable", - "NOTHING", - "resolve_types", - "setters", - "validate", - "validators", -] diff --git a/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi b/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi deleted file mode 100644 index 7426fa5ddb..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi +++ /dev/null @@ -1,63 +0,0 @@ -from typing import ( - Any, - Callable, - Dict, - Mapping, - Optional, - Sequence, - Tuple, - Type, -) - -# Because we need to type our own stuff, we have to make everything from -# attr explicitly public too. -from attr import __author__ as __author__ -from attr import __copyright__ as __copyright__ -from attr import __description__ as __description__ -from attr import __email__ as __email__ -from attr import __license__ as __license__ -from attr import __title__ as __title__ -from attr import __url__ as __url__ -from attr import __version__ as __version__ -from attr import __version_info__ as __version_info__ -from attr import _FilterType -from attr import assoc as assoc -from attr import Attribute as Attribute -from attr import define as define -from attr import evolve as evolve -from attr import Factory as Factory -from attr import exceptions as exceptions -from attr import field as field -from attr import fields as fields -from attr import fields_dict as fields_dict -from attr import frozen as frozen -from attr import has as has -from attr import make_class as make_class -from attr import mutable as mutable -from attr import NOTHING as NOTHING -from attr import resolve_types as resolve_types -from attr import setters as setters -from attr import validate as validate -from attr import validators as validators - -# TODO: see definition of attr.asdict/astuple -def asdict( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., - retain_collection_types: bool = ..., - value_serializer: Optional[ - Callable[[type, Attribute[Any], Any], Any] - ] = ..., - tuple_keys: bool = ..., -) -> Dict[str, Any]: ... - -# TODO: add support for returning NamedTuple from the mypy plugin -def astuple( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - tuple_factory: Type[Sequence[Any]] = ..., - retain_collection_types: bool = ..., -) -> Tuple[Any, ...]: ... diff --git a/client/ayon_core/vendor/python/python_2/attrs/converters.py b/client/ayon_core/vendor/python/python_2/attrs/converters.py deleted file mode 100644 index edfa8d3c16..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/converters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.converters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/exceptions.py b/client/ayon_core/vendor/python/python_2/attrs/exceptions.py deleted file mode 100644 index bd9efed202..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.exceptions import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/filters.py b/client/ayon_core/vendor/python/python_2/attrs/filters.py deleted file mode 100644 index 52959005b0..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/filters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.filters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/py.typed b/client/ayon_core/vendor/python/python_2/attrs/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/vendor/python/python_2/attrs/setters.py b/client/ayon_core/vendor/python/python_2/attrs/setters.py deleted file mode 100644 index 9b50770804..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/setters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.setters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/validators.py b/client/ayon_core/vendor/python/python_2/attrs/validators.py deleted file mode 100644 index ab2c9b3024..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/validators.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.validators import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/backports/__init__.py b/client/ayon_core/vendor/python/python_2/backports/__init__.py deleted file mode 100644 index 69e3be50da..0000000000 --- a/client/ayon_core/vendor/python/python_2/backports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py b/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py deleted file mode 100644 index 06d7a0855f..0000000000 --- a/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py +++ /dev/null @@ -1,1390 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Configuration file parser. - -A configuration file consists of sections, lead by a "[section]" header, -and followed by "name: value" entries, with continuations and such in -the style of RFC 822. - -Intrinsic defaults can be specified by passing them into the -ConfigParser constructor as a dictionary. - -class: - -ConfigParser -- responsible for parsing a list of - configuration files, and managing the parsed database. - - methods: - - __init__(defaults=None, dict_type=_default_dict, allow_no_value=False, - delimiters=('=', ':'), comment_prefixes=('#', ';'), - inline_comment_prefixes=None, strict=True, - empty_lines_in_values=True, default_section='DEFAULT', - interpolation=, converters=): - Create the parser. When `defaults' is given, it is initialized into the - dictionary or intrinsic defaults. The keys must be strings, the values - must be appropriate for %()s string interpolation. - - When `dict_type' is given, it will be used to create the dictionary - objects for the list of sections, for the options within a section, and - for the default values. - - When `delimiters' is given, it will be used as the set of substrings - that divide keys from values. - - When `comment_prefixes' is given, it will be used as the set of - substrings that prefix comments in empty lines. Comments can be - indented. - - When `inline_comment_prefixes' is given, it will be used as the set of - substrings that prefix comments in non-empty lines. - - When `strict` is True, the parser won't allow for any section or option - duplicates while reading from a single source (file, string or - dictionary). Default is True. - - When `empty_lines_in_values' is False (default: True), each empty line - marks the end of an option. Otherwise, internal empty lines of - a multiline option are kept as part of the value. - - When `allow_no_value' is True (default: False), options without - values are accepted; the value presented for these is None. - - sections() - Return all the configuration section names, sans DEFAULT. - - has_section(section) - Return whether the given section exists. - - has_option(section, option) - Return whether the given option exists in the given section. - - options(section) - Return list of configuration options for the named section. - - read(filenames, encoding=None) - Read and parse the list of named configuration files, given by - name. A single filename is also allowed. Non-existing files - are ignored. Return list of successfully read files. - - read_file(f, filename=None) - Read and parse one configuration file, given as a file object. - The filename defaults to f.name; it is only used in error - messages (if f has no `name' attribute, the string `' is used). - - read_string(string) - Read configuration from a given string. - - read_dict(dictionary) - Read configuration from a dictionary. Keys are section names, - values are dictionaries with keys and values that should be present - in the section. If the used dictionary type preserves order, sections - and their keys will be added in order. Values are automatically - converted to strings. - - get(section, option, raw=False, vars=None, fallback=_UNSET) - Return a string value for the named option. All % interpolations are - expanded in the return values, based on the defaults passed into the - constructor and the DEFAULT section. Additional substitutions may be - provided using the `vars' argument, which must be a dictionary whose - contents override any pre-existing defaults. If `option' is a key in - `vars', the value from `vars' is used. - - getint(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to an integer. - - getfloat(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to a float. - - getboolean(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to a boolean (currently case - insensitively defined as 0, false, no, off for False, and 1, true, - yes, on for True). Returns False or True. - - items(section=_UNSET, raw=False, vars=None) - If section is given, return a list of tuples with (name, value) for - each option in the section. Otherwise, return a list of tuples with - (section_name, section_proxy) for each section, including DEFAULTSECT. - - remove_section(section) - Remove the given file section and all its options. - - remove_option(section, option) - Remove the given option from the given section. - - set(section, option, value) - Set the given option. - - write(fp, space_around_delimiters=True) - Write the configuration state in .ini format. If - `space_around_delimiters' is True (the default), delimiters - between keys and values are surrounded by spaces. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from collections import MutableMapping -import functools -import io -import itertools -import re -import sys -import warnings - -from backports.configparser.helpers import OrderedDict as _default_dict -from backports.configparser.helpers import ChainMap as _ChainMap -from backports.configparser.helpers import from_none, open, str, PY2 - -__all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError", - "NoOptionError", "InterpolationError", "InterpolationDepthError", - "InterpolationMissingOptionError", "InterpolationSyntaxError", - "ParsingError", "MissingSectionHeaderError", - "ConfigParser", "SafeConfigParser", "RawConfigParser", - "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "LegacyInterpolation", "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"] - -DEFAULTSECT = "DEFAULT" - -MAX_INTERPOLATION_DEPTH = 10 - - -# exception classes -class Error(Exception): - """Base class for ConfigParser exceptions.""" - - def __init__(self, msg=''): - self.message = msg - Exception.__init__(self, msg) - - def __repr__(self): - return self.message - - __str__ = __repr__ - - -class NoSectionError(Error): - """Raised when no section matches a requested option.""" - - def __init__(self, section): - Error.__init__(self, 'No section: %r' % (section,)) - self.section = section - self.args = (section, ) - - -class DuplicateSectionError(Error): - """Raised when a section is repeated in an input source. - - Possible repetitions that raise this exception are: multiple creation - using the API or in strict parsers when a section is found more than once - in a single input file, string or dictionary. - """ - - def __init__(self, section, source=None, lineno=None): - msg = [repr(section), " already exists"] - if source is not None: - message = ["While reading from ", repr(source)] - if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) - message.append(": section ") - message.extend(msg) - msg = message - else: - msg.insert(0, "Section ") - Error.__init__(self, "".join(msg)) - self.section = section - self.source = source - self.lineno = lineno - self.args = (section, source, lineno) - - -class DuplicateOptionError(Error): - """Raised by strict parsers when an option is repeated in an input source. - - Current implementation raises this exception only when an option is found - more than once in a single file, string or dictionary. - """ - - def __init__(self, section, option, source=None, lineno=None): - msg = [repr(option), " in section ", repr(section), - " already exists"] - if source is not None: - message = ["While reading from ", repr(source)] - if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) - message.append(": option ") - message.extend(msg) - msg = message - else: - msg.insert(0, "Option ") - Error.__init__(self, "".join(msg)) - self.section = section - self.option = option - self.source = source - self.lineno = lineno - self.args = (section, option, source, lineno) - - -class NoOptionError(Error): - """A requested option was not found.""" - - def __init__(self, option, section): - Error.__init__(self, "No option %r in section: %r" % - (option, section)) - self.option = option - self.section = section - self.args = (option, section) - - -class InterpolationError(Error): - """Base class for interpolation-related exceptions.""" - - def __init__(self, option, section, msg): - Error.__init__(self, msg) - self.option = option - self.section = section - self.args = (option, section, msg) - - -class InterpolationMissingOptionError(InterpolationError): - """A string substitution required a setting which was not available.""" - - def __init__(self, option, section, rawval, reference): - msg = ("Bad value substitution: option {0!r} in section {1!r} contains " - "an interpolation key {2!r} which is not a valid option name. " - "Raw value: {3!r}".format(option, section, reference, rawval)) - InterpolationError.__init__(self, option, section, msg) - self.reference = reference - self.args = (option, section, rawval, reference) - - -class InterpolationSyntaxError(InterpolationError): - """Raised when the source text contains invalid syntax. - - Current implementation raises this exception when the source text into - which substitutions are made does not conform to the required syntax. - """ - - -class InterpolationDepthError(InterpolationError): - """Raised when substitutions are nested too deeply.""" - - def __init__(self, option, section, rawval): - msg = ("Recursion limit exceeded in value substitution: option {0!r} " - "in section {1!r} contains an interpolation key which " - "cannot be substituted in {2} steps. Raw value: {3!r}" - "".format(option, section, MAX_INTERPOLATION_DEPTH, - rawval)) - InterpolationError.__init__(self, option, section, msg) - self.args = (option, section, rawval) - - -class ParsingError(Error): - """Raised when a configuration file does not follow legal syntax.""" - - def __init__(self, source=None, filename=None): - # Exactly one of `source'/`filename' arguments has to be given. - # `filename' kept for compatibility. - if filename and source: - raise ValueError("Cannot specify both `filename' and `source'. " - "Use `source'.") - elif not filename and not source: - raise ValueError("Required argument `source' not given.") - elif filename: - source = filename - Error.__init__(self, 'Source contains parsing errors: %r' % source) - self.source = source - self.errors = [] - self.args = (source, ) - - @property - def filename(self): - """Deprecated, use `source'.""" - warnings.warn( - "The 'filename' attribute will be removed in future versions. " - "Use 'source' instead.", - DeprecationWarning, stacklevel=2 - ) - return self.source - - @filename.setter - def filename(self, value): - """Deprecated, user `source'.""" - warnings.warn( - "The 'filename' attribute will be removed in future versions. " - "Use 'source' instead.", - DeprecationWarning, stacklevel=2 - ) - self.source = value - - def append(self, lineno, line): - self.errors.append((lineno, line)) - self.message += '\n\t[line %2d]: %s' % (lineno, line) - - -class MissingSectionHeaderError(ParsingError): - """Raised when a key-value pair is found before any section header.""" - - def __init__(self, filename, lineno, line): - Error.__init__( - self, - 'File contains no section headers.\nfile: %r, line: %d\n%r' % - (filename, lineno, line)) - self.source = filename - self.lineno = lineno - self.line = line - self.args = (filename, lineno, line) - - -# Used in parser getters to indicate the default behaviour when a specific -# option is not found it to raise an exception. Created to enable `None' as -# a valid fallback value. -_UNSET = object() - - -class Interpolation(object): - """Dummy interpolation that passes the value through with no changes.""" - - def before_get(self, parser, section, option, value, defaults): - return value - - def before_set(self, parser, section, option, value): - return value - - def before_read(self, parser, section, option, value): - return value - - def before_write(self, parser, section, option, value): - return value - - -class BasicInterpolation(Interpolation): - """Interpolation as implemented in the classic ConfigParser. - - The option values can contain format strings which refer to other values in - the same section, or values in the special default section. - - For example: - - something: %(dir)s/whatever - - would resolve the "%(dir)s" to the value of dir. All reference - expansions are done late, on demand. If a user needs to use a bare % in - a configuration file, she can escape it by writing %%. Other % usage - is considered a user error and raises `InterpolationSyntaxError'.""" - - _KEYCRE = re.compile(r"%\(([^)]+)\)s") - - def before_get(self, parser, section, option, value, defaults): - L = [] - self._interpolate_some(parser, option, L, value, section, defaults, 1) - return ''.join(L) - - def before_set(self, parser, section, option, value): - tmp_value = value.replace('%%', '') # escaped percent signs - tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax - if '%' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('%'))) - return value - - def _interpolate_some(self, parser, option, accum, rest, section, map, - depth): - rawval = parser.get(section, option, raw=True, fallback=rest) - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rawval) - while rest: - p = rest.find("%") - if p < 0: - accum.append(rest) - return - if p > 0: - accum.append(rest[:p]) - rest = rest[p:] - # p is no longer used - c = rest[1:2] - if c == "%": - accum.append("%") - rest = rest[2:] - elif c == "(": - m = self._KEYCRE.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - var = parser.optionxform(m.group(1)) - rest = rest[m.end():] - try: - v = map[var] - except KeyError: - raise from_none(InterpolationMissingOptionError( - option, section, rawval, var)) - if "%" in v: - self._interpolate_some(parser, option, accum, v, - section, map, depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'%%' must be followed by '%%' or '(', " - "found: %r" % (rest,)) - - -class ExtendedInterpolation(Interpolation): - """Advanced variant of interpolation, supports the syntax used by - `zc.buildout'. Enables interpolation between sections.""" - - _KEYCRE = re.compile(r"\$\{([^}]+)\}") - - def before_get(self, parser, section, option, value, defaults): - L = [] - self._interpolate_some(parser, option, L, value, section, defaults, 1) - return ''.join(L) - - def before_set(self, parser, section, option, value): - tmp_value = value.replace('$$', '') # escaped dollar signs - tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax - if '$' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('$'))) - return value - - def _interpolate_some(self, parser, option, accum, rest, section, map, - depth): - rawval = parser.get(section, option, raw=True, fallback=rest) - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rawval) - while rest: - p = rest.find("$") - if p < 0: - accum.append(rest) - return - if p > 0: - accum.append(rest[:p]) - rest = rest[p:] - # p is no longer used - c = rest[1:2] - if c == "$": - accum.append("$") - rest = rest[2:] - elif c == "{": - m = self._KEYCRE.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - path = m.group(1).split(':') - rest = rest[m.end():] - sect = section - opt = option - try: - if len(path) == 1: - opt = parser.optionxform(path[0]) - v = map[opt] - elif len(path) == 2: - sect = path[0] - opt = parser.optionxform(path[1]) - v = parser.get(sect, opt, raw=True) - else: - raise InterpolationSyntaxError( - option, section, - "More than one ':' found: %r" % (rest,)) - except (KeyError, NoSectionError, NoOptionError): - raise from_none(InterpolationMissingOptionError( - option, section, rawval, ":".join(path))) - if "$" in v: - self._interpolate_some(parser, opt, accum, v, sect, - dict(parser.items(sect, raw=True)), - depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'$' must be followed by '$' or '{', " - "found: %r" % (rest,)) - - -class LegacyInterpolation(Interpolation): - """Deprecated interpolation used in old versions of ConfigParser. - Use BasicInterpolation or ExtendedInterpolation instead.""" - - _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") - - def before_get(self, parser, section, option, value, vars): - rawval = value - depth = MAX_INTERPOLATION_DEPTH - while depth: # Loop through this until it's done - depth -= 1 - if value and "%(" in value: - replace = functools.partial(self._interpolation_replace, - parser=parser) - value = self._KEYCRE.sub(replace, value) - try: - value = value % vars - except KeyError as e: - raise from_none(InterpolationMissingOptionError( - option, section, rawval, e.args[0])) - else: - break - if value and "%(" in value: - raise InterpolationDepthError(option, section, rawval) - return value - - def before_set(self, parser, section, option, value): - return value - - @staticmethod - def _interpolation_replace(match, parser): - s = match.group(1) - if s is None: - return match.group() - else: - return "%%(%s)s" % parser.optionxform(s) - - -class RawConfigParser(MutableMapping): - """ConfigParser that does not do interpolation.""" - - # Regular expressions for parsing section headers and options - _SECT_TMPL = r""" - \[ # [ - (?P
[^]]+) # very permissive! - \] # ] - """ - _OPT_TMPL = r""" - (?P