diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 6b4a1f824f..d7cd3ba7f5 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -8,7 +8,6 @@ from pathlib import Path import warnings import click -import acre from ayon_core import AYON_CORE_ROOT from ayon_core.addon import AddonsManager @@ -18,6 +17,11 @@ from ayon_core.lib import ( is_running_from_build, Logger, ) +from ayon_core.lib.env_tools import ( + parse_env_variables_structure, + compute_env_variables_structure, + merge_env_variables, +) @@ -235,19 +239,15 @@ def version(build): def _set_global_environments() -> None: """Set global AYON environments.""" - general_env = get_general_environments() + # First resolve general environment + general_env = parse_env_variables_structure(get_general_environments()) - # first resolve general environment because merge doesn't expect - # values to be list. - # TODO: switch to AYON environment functions - merged_env = acre.merge( - acre.compute(acre.parse(general_env), cleanup=False), + # Merge environments with current environments and update values + merged_env = merge_env_variables( + compute_env_variables_structure(general_env), dict(os.environ) ) - env = acre.compute( - merged_env, - cleanup=False - ) + env = compute_env_variables_structure(merged_env) os.environ.clear() os.environ.update(env) @@ -263,8 +263,8 @@ def _set_addons_environments(addons_manager): # Merge environments with current environments and update values if module_envs := addons_manager.collect_global_environments(): - parsed_envs = acre.parse(module_envs) - env = acre.merge(parsed_envs, dict(os.environ)) + parsed_envs = parse_env_variables_structure(module_envs) + env = merge_env_variables(parsed_envs, dict(os.environ)) os.environ.clear() os.environ.update(env) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index 25bcbf7c1b..bc788a082d 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -1,7 +1,34 @@ +from __future__ import annotations import os +import re +import platform +import typing +import collections +from string import Formatter +from typing import Optional + +if typing.TYPE_CHECKING: + from typing import Union, Literal + + PlatformName = Literal["windows", "linux", "darwin"] + EnvValue = Union[str, list[str], dict[str, str], dict[str, list[str]]] -def env_value_to_bool(env_key=None, value=None, default=False): +class CycleError(ValueError): + """Raised when a cycle is detected in dynamic env variables compute.""" + pass + + +class DynamicKeyClashError(Exception): + """Raised when dynamic key clashes with an existing key.""" + pass + + +def env_value_to_bool( + env_key: Optional[str] = None, + value: Optional[str] = None, + default: bool = False, +) -> bool: """Convert environment variable value to boolean. Function is based on value of the environemt variable. Value is lowered @@ -11,6 +38,7 @@ def env_value_to_bool(env_key=None, value=None, default=False): bool: If value match to one of ["true", "yes", "1"] result if True but if value match to ["false", "no", "0"] result is False else default value is returned. + """ if value is None and env_key is None: return default @@ -27,18 +55,23 @@ def env_value_to_bool(env_key=None, value=None, default=False): return default -def get_paths_from_environ(env_key=None, env_value=None, return_first=False): +def get_paths_from_environ( + env_key: Optional[str] = None, + env_value: Optional[str] = None, + return_first: bool = False, +) -> Optional[Union[str, list[str]]]: """Return existing paths from specific environment variable. Args: - env_key (str): Environment key where should look for paths. - env_value (str): Value of environment variable. Argument `env_key` is - skipped if this argument is entered. + env_key (Optional[str]): Environment key where should look for paths. + env_value (Optional[str]): Value of environment variable. + Argument `env_key` is skipped if this argument is entered. return_first (bool): Return first found value or return list of found paths. `None` or empty list returned if nothing found. Returns: - str, list, None: Result of found path/s. + Optional[Union[str, list[str]]]: Result of found path/s. + """ existing_paths = [] if not env_key and not env_value: @@ -69,3 +102,225 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False): return None # Return all existing paths from environment variable return existing_paths + + +def parse_env_variables_structure( + env: dict[str, EnvValue], + platform_name: Optional[PlatformName] = None +) -> dict[str, str]: + """Parse environment for platform-specific values and paths as lists. + + Args: + env (dict): The source environment to read. + platform_name (Optional[PlatformName]): Name of platform to parse for. + Defaults to current platform. + + Returns: + dict: The flattened environment for a platform. + + """ + if platform_name is None: + platform_name = platform.system().lower() + + # Separator based on OS 'os.pathsep' is ';' on Windows and ':' on Unix + sep = ";" if platform_name == "windows" else ":" + + result = {} + for variable, value in env.items(): + # Platform specific values + if isinstance(value, dict): + value = value.get(platform_name) + + # Allow to have lists as values in the tool data + if isinstance(value, (list, tuple)): + value = sep.join(value) + + if not value: + continue + + if not isinstance(value, str): + raise TypeError(f"Expected 'str' got '{type(value)}'") + + result[variable] = value + + return result + + +def _topological_sort( + dependencies: dict[str, set[str]] +) -> tuple[list[str], list[str]]: + """Sort values subject to dependency constraints. + + Args: + dependencies (dict[str, set[str]): Mapping of environment variable + keys to a set of keys they depend on. + + Returns: + tuple[list[str], list[str]]: A tuple of two lists. The first list + contains the ordered keys in which order should be environment + keys filled, the second list contains the keys that would cause + cyclic fill of values. + + """ + num_heads = collections.defaultdict(int) # num arrows pointing in + tails = collections.defaultdict(list) # list of arrows going out + heads = [] # unique list of heads in order first seen + for head, tail_values in dependencies.items(): + for tail_value in tail_values: + num_heads[tail_value] += 1 + if head not in tails: + heads.append(head) + tails[head].append(tail_value) + + ordered = [head for head in heads if head not in num_heads] + for head in ordered: + for tail in tails[head]: + num_heads[tail] -= 1 + if not num_heads[tail]: + ordered.append(tail) + cyclic = [tail for tail, heads in num_heads.items() if heads] + return ordered, cyclic + + +class _PartialFormatDict(dict): + """This supports partial formatting. + + Missing keys are replaced with the return value of __missing__. + + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._missing_template: str = "{{{key}}}" + + def set_missing_template(self, template: str): + self._missing_template = template + + def __missing__(self, key: str) -> str: + return self._missing_template.format(key=key) + + +def _partial_format( + value: str, + data: dict[str, str], + missing_template: Optional[str] = None, +) -> str: + """Return string `s` formatted by `data` allowing a partial format + + Arguments: + value (str): The string that will be formatted + data (dict): The dictionary used to format with. + missing_template (Optional[str]): The template to use when a key is + missing from the data. If `None`, the key will remain unformatted. + + Example: + >>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"}) + 'left {a} and {c} left' + + """ + + mapping = _PartialFormatDict(**data) + if missing_template is not None: + mapping.set_missing_template(missing_template) + + formatter = Formatter() + try: + output = formatter.vformat(value, (), mapping) + except Exception: + r_token = re.compile(r"({.*?})") + output = value + for match in re.findall(r_token, value): + try: + output = re.sub(match, match.format(**data), output) + except (KeyError, ValueError, IndexError): + continue + return output + + +def compute_env_variables_structure( + env: dict[str, str], + fill_dynamic_keys: bool = True, +) -> dict[str, str]: + """Compute the result from recursive dynamic environment. + + Note: Keys that are not present in the data will remain unformatted as the + original keys. So they can be formatted against the current user + environment when merging. So {"A": "{key}"} will remain {key} if not + present in the dynamic environment. + + """ + env = env.copy() + + # Collect dependencies + dependencies = collections.defaultdict(set) + for key, value in env.items(): + dependent_keys = re.findall("{(.+?)}", value) + for dependent_key in dependent_keys: + # Ignore reference to itself or key is not in env + if dependent_key != key and dependent_key in env: + dependencies[key].add(dependent_key) + + ordered, cyclic = _topological_sort(dependencies) + + # Check cycle + if cyclic: + raise CycleError(f"A cycle is detected on: {cyclic}") + + # Format dynamic values + for key in reversed(ordered): + if key in env: + if not isinstance(env[key], str): + continue + data = env.copy() + data.pop(key) # format without itself + env[key] = _partial_format(env[key], data=data) + + # Format dynamic keys + if fill_dynamic_keys: + formatted = {} + for key, value in env.items(): + if not isinstance(value, str): + formatted[key] = value + continue + + new_key = _partial_format(key, data=env) + if new_key in formatted: + raise DynamicKeyClashError( + f"Key clashes on: {new_key} (source: {key})" + ) + + formatted[new_key] = value + env = formatted + + return env + + +def merge_env_variables( + src_env: dict[str, str], + dst_env: dict[str, str], + missing_template: Optional[str] = None, +) -> dict[str, str]: + """Merge the tools environment with the 'current_env'. + + This finalizes the join with a current environment by formatting the + remainder of dynamic variables with that from the current environment. + + Remaining missing variables result in an empty value. + + Args: + src_env (dict): The dynamic environment + dst_env (dict): The target environment variables mapping to merge + the dynamic environment into. + missing_template (str): Argument passed to '_partial_format' during + merging. `None` should keep missing keys unchanged. + + Returns: + dict[str, str]: The resulting environment after the merge. + + """ + result = dst_env.copy() + for key, value in src_env.items(): + result[key] = _partial_format( + str(value), dst_env, missing_template + ) + + return result diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 08030ae87e..eff0068f00 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -9,7 +9,7 @@ from datetime import datetime from abc import ABC, abstractmethod from functools import lru_cache -import appdirs +import platformdirs import ayon_api _PLACEHOLDER = object() @@ -17,7 +17,7 @@ _PLACEHOLDER = object() def _get_ayon_appdirs(*args): return os.path.join( - appdirs.user_data_dir("AYON", "Ynput"), + platformdirs.user_data_dir("AYON", "Ynput"), *args ) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index c169df67df..24557234f4 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -755,11 +755,19 @@ class CreateContext: ).format(creator_class.host_name, self.host_name)) continue - creator = creator_class( - project_settings, - self, - self.headless - ) + # TODO report initialization error + try: + creator = creator_class( + project_settings, + self, + self.headless + ) + except Exception: + self.log.error( + f"Failed to initialize plugin: {creator_class}", + exc_info=True + ) + continue if not creator.enabled: disabled_creators[creator_identifier] = creator diff --git a/client/ayon_core/pipeline/traits/README.md b/client/ayon_core/pipeline/traits/README.md index aa38d30a8e..1566b4f353 100644 --- a/client/ayon_core/pipeline/traits/README.md +++ b/client/ayon_core/pipeline/traits/README.md @@ -184,10 +184,13 @@ to different packages based on their use: | | Overscan | holds overscan/underscan information (added pixels to bottom/sides) | | UDIM | Representation is UDIM tile set -Traits are [Pydantic models](https://docs.pydantic.dev/latest/) with optional +Traits are Python data classes with optional validation and helper methods. If they implement `TraitBase.validate(Representation)` method, they can validate against all other traits -in the representation if needed. They can also implement pydantic form of -data validators. +in the representation if needed. + +> [!NOTE] +> They could be easily converted to [Pydantic models](https://docs.pydantic.dev/latest/) but since this must run in diverse Python environments inside DCC, we cannot +> easily resolve pydantic-core dependency (as it is binary written in Rust). > [!NOTE] > Every trait has id, name and some human readable description. Every trait diff --git a/client/ayon_core/pipeline/traits/color.py b/client/ayon_core/pipeline/traits/color.py index b816593624..491131c8bc 100644 --- a/client/ayon_core/pipeline/traits/color.py +++ b/client/ayon_core/pipeline/traits/color.py @@ -1,13 +1,13 @@ """Color management related traits.""" from __future__ import annotations +from dataclasses import dataclass from typing import ClassVar, Optional -from pydantic import Field - from .trait import TraitBase +@dataclass class ColorManaged(TraitBase): """Color managed trait. @@ -24,9 +24,7 @@ class ColorManaged(TraitBase): id: ClassVar[str] = "ayon.color.ColorManaged.v1" name: ClassVar[str] = "ColorManaged" + color_space: str description: ClassVar[str] = "Color Managed trait." - color_space: str = Field( - ..., - description="Color space." - ) - config: Optional[str] = Field(default=None, description="Color config.") + persistent: ClassVar[bool] = True + config: Optional[str] = None diff --git a/client/ayon_core/pipeline/traits/content.py b/client/ayon_core/pipeline/traits/content.py index 4ee63b2b08..9bb43fcdb3 100644 --- a/client/ayon_core/pipeline/traits/content.py +++ b/client/ayon_core/pipeline/traits/content.py @@ -3,13 +3,12 @@ from __future__ import annotations import contextlib import re +from dataclasses import dataclass # TC003 is there because Path in TYPECHECKING will fail in tests from pathlib import Path # noqa: TC003 from typing import ClassVar, Generator, Optional -from pydantic import Field - from .representation import Representation from .temporal import FrameRanged, Handles, Sequence from .trait import ( @@ -21,6 +20,7 @@ from .two_dimensional import UDIM from .utils import get_sequence_from_files +@dataclass class MimeType(TraitBase): """MimeType trait model. @@ -40,9 +40,11 @@ class MimeType(TraitBase): name: ClassVar[str] = "MimeType" description: ClassVar[str] = "MimeType Trait Model" id: ClassVar[str] = "ayon.content.MimeType.v1" - mime_type: str = Field(..., title="Mime Type") + persistent: ClassVar[bool] = True + mime_type: str +@dataclass class LocatableContent(TraitBase): """LocatableContent trait model. @@ -57,15 +59,18 @@ class LocatableContent(TraitBase): description (str): Trait description. id (str): id should be namespaced trait name with version location (str): Location. + is_templated (Optional[bool]): Is the location templated? Default is None. """ name: ClassVar[str] = "LocatableContent" description: ClassVar[str] = "LocatableContent Trait Model" id: ClassVar[str] = "ayon.content.LocatableContent.v1" - location: str = Field(..., title="Location") - is_templated: Optional[bool] = Field(default=None, title="Is Templated") + persistent: ClassVar[bool] = True + location: str + is_templated: Optional[bool] = None +@dataclass class FileLocation(TraitBase): """FileLocation trait model. @@ -78,18 +83,20 @@ class FileLocation(TraitBase): description (str): Trait description. id (str): id should be namespaced trait name with version file_path (str): File path. - file_size (int): File size in bytes. - file_hash (str): File hash. + file_size (Optional[int]): File size in bytes. + file_hash (Optional[str]): File hash. """ name: ClassVar[str] = "FileLocation" description: ClassVar[str] = "FileLocation Trait Model" id: ClassVar[str] = "ayon.content.FileLocation.v1" - file_path: Path = Field(..., title="File Path") - file_size: Optional[int] = Field(default=None, title="File Size") - file_hash: Optional[str] = Field(default=None, title="File Hash") + persistent: ClassVar[bool] = True + file_path: Path + file_size: Optional[int] = None + file_hash: Optional[str] = None +@dataclass class FileLocations(TraitBase): """FileLocation trait model. @@ -108,7 +115,8 @@ class FileLocations(TraitBase): name: ClassVar[str] = "FileLocations" description: ClassVar[str] = "FileLocations Trait Model" id: ClassVar[str] = "ayon.content.FileLocations.v1" - file_paths: list[FileLocation] = Field(..., title="File Path") + persistent: ClassVar[bool] = True + file_paths: list[FileLocation] def get_files(self) -> Generator[Path, None, None]: """Get all file paths from the trait. @@ -340,6 +348,7 @@ class FileLocations(TraitBase): return frame_start_with_handles, frame_end_with_handles +@dataclass class RootlessLocation(TraitBase): """RootlessLocation trait model. @@ -363,9 +372,11 @@ class RootlessLocation(TraitBase): name: ClassVar[str] = "RootlessLocation" description: ClassVar[str] = "RootlessLocation Trait Model" id: ClassVar[str] = "ayon.content.RootlessLocation.v1" - rootless_path: str = Field(..., title="File Path") + persistent: ClassVar[bool] = True + rootless_path: str +@dataclass class Compressed(TraitBase): """Compressed trait model. @@ -386,9 +397,11 @@ class Compressed(TraitBase): name: ClassVar[str] = "Compressed" description: ClassVar[str] = "Compressed Trait" id: ClassVar[str] = "ayon.content.Compressed.v1" - compression_type: str = Field(..., title="Compression Type") + persistent: ClassVar[bool] = True + compression_type: str +@dataclass class Bundle(TraitBase): """Bundle trait model. @@ -424,8 +437,8 @@ class Bundle(TraitBase): name: ClassVar[str] = "Bundle" description: ClassVar[str] = "Bundle Trait" id: ClassVar[str] = "ayon.content.Bundle.v1" - items: list[list[TraitBase]] = Field( - ..., title="Bundles of traits") + persistent: ClassVar[bool] = True + items: list[list[TraitBase]] def to_representations(self) -> Generator[Representation]: """Convert bundle to representations. @@ -438,6 +451,7 @@ class Bundle(TraitBase): yield Representation(name=f"{self.name} {idx}", traits=item) +@dataclass class Fragment(TraitBase): """Fragment trait model. @@ -466,4 +480,5 @@ class Fragment(TraitBase): name: ClassVar[str] = "Fragment" description: ClassVar[str] = "Fragment Trait" id: ClassVar[str] = "ayon.content.Fragment.v1" - parent: str = Field(..., title="Parent Representation Id") + persistent: ClassVar[bool] = True + parent: str diff --git a/client/ayon_core/pipeline/traits/cryptography.py b/client/ayon_core/pipeline/traits/cryptography.py index 3719f3dbbf..d9445bd543 100644 --- a/client/ayon_core/pipeline/traits/cryptography.py +++ b/client/ayon_core/pipeline/traits/cryptography.py @@ -1,13 +1,13 @@ """Cryptography traits.""" from __future__ import annotations +from dataclasses import dataclass from typing import ClassVar, Optional -from pydantic import Field - from .trait import TraitBase +@dataclass class DigitallySigned(TraitBase): """Digitally signed trait. @@ -20,25 +20,22 @@ class DigitallySigned(TraitBase): id: ClassVar[str] = "ayon.cryptography.DigitallySigned.v1" name: ClassVar[str] = "DigitallySigned" description: ClassVar[str] = "Digitally signed trait." + persistent: ClassVar[bool] = True - +@dataclass class PGPSigned(DigitallySigned): """PGP signed trait. This trait holds PGP (RFC-4880) signed data. Attributes: - signature (str): PGP signature. + signed_data (str): Signed data. + clear_text (str): Clear text. """ id: ClassVar[str] = "ayon.cryptography.PGPSigned.v1" name: ClassVar[str] = "PGPSigned" description: ClassVar[str] = "PGP signed trait." - signed_data: str = Field( - ..., - description="Signed data." - ) - clear_text: Optional[str] = Field( - None, - description="Clear text." - ) + persistent: ClassVar[bool] = True + signed_data: str + clear_text: Optional[str] = None diff --git a/client/ayon_core/pipeline/traits/lifecycle.py b/client/ayon_core/pipeline/traits/lifecycle.py index 2de90824e9..b1b72f8fcb 100644 --- a/client/ayon_core/pipeline/traits/lifecycle.py +++ b/client/ayon_core/pipeline/traits/lifecycle.py @@ -1,9 +1,11 @@ """Lifecycle traits.""" +from dataclasses import dataclass from typing import ClassVar from .trait import TraitBase, TraitValidationError +@dataclass class Transient(TraitBase): """Transient trait model. @@ -19,6 +21,7 @@ class Transient(TraitBase): name: ClassVar[str] = "Transient" description: ClassVar[str] = "Transient Trait Model" id: ClassVar[str] = "ayon.lifecycle.Transient.v1" + persistent: ClassVar[bool] = True # see note in Persistent def validate_trait(self, representation) -> None: # noqa: ANN001 """Validate representation is not Persistent. @@ -36,6 +39,7 @@ class Transient(TraitBase): raise TraitValidationError(self.name, msg) +@dataclass class Persistent(TraitBase): """Persistent trait model. @@ -52,6 +56,10 @@ class Persistent(TraitBase): name: ClassVar[str] = "Persistent" description: ClassVar[str] = "Persistent Trait Model" id: ClassVar[str] = "ayon.lifecycle.Persistent.v1" + # note that this affects persistence of the trait itself, not + # the representation. This is a class variable, so it is shared + # among all instances of the class. + persistent: bool = True def validate_trait(self, representation) -> None: # noqa: ANN001 """Validate representation is not Transient. diff --git a/client/ayon_core/pipeline/traits/meta.py b/client/ayon_core/pipeline/traits/meta.py index 0f8c175af5..3bf4a87a0b 100644 --- a/client/ayon_core/pipeline/traits/meta.py +++ b/client/ayon_core/pipeline/traits/meta.py @@ -1,13 +1,13 @@ """Metadata traits.""" from __future__ import annotations +from dataclasses import dataclass from typing import ClassVar, List, Optional -from pydantic import Field - from .trait import TraitBase +@dataclass class Tagged(TraitBase): """Tagged trait model. @@ -27,9 +27,11 @@ class Tagged(TraitBase): name: ClassVar[str] = "Tagged" description: ClassVar[str] = "Tagged Trait Model" id: ClassVar[str] = "ayon.meta.Tagged.v1" - tags: List[str] = Field(..., title="Tags") + persistent: ClassVar[bool] = True + tags: List[str] +@dataclass class TemplatePath(TraitBase): """TemplatePath trait model. @@ -51,10 +53,12 @@ class TemplatePath(TraitBase): name: ClassVar[str] = "TemplatePath" description: ClassVar[str] = "Template Path Trait Model" id: ClassVar[str] = "ayon.meta.TemplatePath.v1" - template: str = Field(..., title="Template Path") - data: dict = Field(..., title="Formatting Data") + persistent: ClassVar[bool] = True + template: str + data: dict +@dataclass class Variant(TraitBase): """Variant trait model. @@ -75,9 +79,11 @@ class Variant(TraitBase): name: ClassVar[str] = "Variant" description: ClassVar[str] = "Variant Trait Model" id: ClassVar[str] = "ayon.meta.Variant.v1" - variant: str = Field(..., title="Variant") + persistent: ClassVar[bool] = True + variant: str +@dataclass class KeepOriginalLocation(TraitBase): """Keep files in its original location. @@ -88,9 +94,10 @@ class KeepOriginalLocation(TraitBase): name: ClassVar[str] = "KeepOriginalLocation" description: ClassVar[str] = "Keep Original Location Trait Model" id: ClassVar[str] = "ayon.meta.KeepOriginalLocation.v1" - persistent: bool = Field(default=False, title="Persistent") + persistent: ClassVar[bool] = False +@dataclass class KeepOriginalName(TraitBase): """Keep files in its original name. @@ -101,9 +108,10 @@ class KeepOriginalName(TraitBase): name: ClassVar[str] = "KeepOriginalName" description: ClassVar[str] = "Keep Original Name Trait Model" id: ClassVar[str] = "ayon.meta.KeepOriginalName.v1" - persistent: bool = Field(default=False, title="Persistent") + persistent: ClassVar[bool] = False +@dataclass class SourceApplication(TraitBase): """Metadata about the source (producing) application. @@ -115,22 +123,26 @@ class SourceApplication(TraitBase): Note that this is not really connected to any logic in ayon-applications addon. + Attributes: + application (str): Application name. + variant (str): Application variant. + version (str): Application version. + platform (str): Platform name (Windows, darwin, etc.). + host_name (str): AYON host name if applicable. """ name: ClassVar[str] = "SourceApplication" description: ClassVar[str] = "Source Application Trait Model" id: ClassVar[str] = "ayon.meta.SourceApplication.v1" - application: str = Field(..., title="Application Name") - variant: Optional[str] = Field( - None, title="Application Variant (e.g. Pro)") - version: Optional[str] = Field( - None, title="Application Version") - platform: Optional[str] = Field( - None, title="Platform Name (e.g. Windows)") - host_name: Optional[str] = Field( - None, title="AYON host Name if applicable") + persistent: ClassVar[bool] = True + application: str + variant: Optional[str] = None + version: Optional[str] = None + platform: Optional[str] = None + host_name: Optional[str] = None +@dataclass class IntendedUse(TraitBase): """Intended use of the representation. @@ -138,9 +150,13 @@ class IntendedUse(TraitBase): can be used in cases, where the other traits are not enough to describe the intended use. For example txt file with tracking points can be used as corner pin in After Effect but not in Nuke. - """ + Attributes: + use (str): Intended use description. + + """ name: ClassVar[str] = "IntendedUse" description: ClassVar[str] = "Intended Use Trait Model" id: ClassVar[str] = "ayon.meta.IntendedUse.v1" - use: str = Field(..., title="Intended Use") + persistent: ClassVar[bool] = True + use: str diff --git a/client/ayon_core/pipeline/traits/temporal.py b/client/ayon_core/pipeline/traits/temporal.py index ad4b65d8ee..dd11daa975 100644 --- a/client/ayon_core/pipeline/traits/temporal.py +++ b/client/ayon_core/pipeline/traits/temporal.py @@ -3,12 +3,12 @@ from __future__ import annotations import contextlib import re +from dataclasses import dataclass from enum import Enum, auto from re import Pattern from typing import TYPE_CHECKING, ClassVar, Optional import clique -from pydantic import Field, field_validator from .trait import MissingTraitError, TraitBase, TraitValidationError @@ -36,6 +36,7 @@ class GapPolicy(Enum): black = auto() +@dataclass class FrameRanged(TraitBase): """Frame ranged trait model. @@ -70,16 +71,16 @@ class FrameRanged(TraitBase): name: ClassVar[str] = "FrameRanged" description: ClassVar[str] = "Frame Ranged Trait" id: ClassVar[str] = "ayon.time.FrameRanged.v1" - frame_start: int = Field( - ..., title="Start Frame") - frame_end: int = Field( - ..., title="Frame Start") - frame_in: Optional[int] = Field(default=None, title="In Frame") - frame_out: Optional[int] = Field(default=None, title="Out Frame") - frames_per_second: str = Field(..., title="Frames Per Second") - step: Optional[int] = Field(default=1, title="Step") + persistent: ClassVar[bool] = True + frame_start: int + frame_end: int + frame_in: Optional[int] = None + frame_out: Optional[int] = None + frames_per_second: str = None + step: Optional[int] = None +@dataclass class Handles(TraitBase): """Handles trait model. @@ -98,14 +99,13 @@ class Handles(TraitBase): name: ClassVar[str] = "Handles" description: ClassVar[str] = "Handles Trait" id: ClassVar[str] = "ayon.time.Handles.v1" - inclusive: Optional[bool] = Field( - False, title="Handles are inclusive") # noqa: FBT003 - frame_start_handle: Optional[int] = Field( - 0, title="Frame Start Handle") - frame_end_handle: Optional[int] = Field( - 0, title="Frame End Handle") + persistent: ClassVar[bool] = True + inclusive: Optional[bool] = False + frame_start_handle: Optional[int] = None + frame_end_handle: Optional[int] = None +@dataclass class Sequence(TraitBase): """Sequence trait model. @@ -130,15 +130,12 @@ class Sequence(TraitBase): name: ClassVar[str] = "Sequence" description: ClassVar[str] = "Sequence Trait Model" id: ClassVar[str] = "ayon.time.Sequence.v1" - gaps_policy: Optional[GapPolicy] = Field( - default=GapPolicy.forbidden, title="Gaps Policy") - frame_padding: int = Field(..., title="Frame Padding") - frame_regex: Optional[Pattern] = Field( - default=None, title="Frame Regex") - frame_spec: Optional[str] = Field(default=None, - title="Frame Specification") + persistent: ClassVar[bool] = True + frame_padding: int + gaps_policy: Optional[GapPolicy] = GapPolicy.forbidden + frame_regex: Optional[Pattern] = None + frame_spec: Optional[str] = None - @field_validator("frame_regex") @classmethod def validate_frame_regex( cls, v: Optional[Pattern] @@ -432,15 +429,22 @@ class Sequence(TraitBase): # Do we need one for drop and non-drop frame? +@dataclass class SMPTETimecode(TraitBase): - """SMPTE Timecode trait model.""" + """SMPTE Timecode trait model. + + Attributes: + timecode (str): SMPTE Timecode HH:MM:SS:FF + """ name: ClassVar[str] = "Timecode" description: ClassVar[str] = "SMPTE Timecode Trait" id: ClassVar[str] = "ayon.time.SMPTETimecode.v1" - timecode: str = Field(..., title="SMPTE Timecode HH:MM:SS:FF") + persistent: ClassVar[bool] = True + timecode: str +@dataclass class Static(TraitBase): """Static time trait. @@ -450,3 +454,4 @@ class Static(TraitBase): name: ClassVar[str] = "Static" description: ClassVar[str] = "Static Time Trait" id: ClassVar[str] = "ayon.time.Static.v1" + persistent: ClassVar[bool] = True diff --git a/client/ayon_core/pipeline/traits/three_dimensional.py b/client/ayon_core/pipeline/traits/three_dimensional.py index 67f4415f73..d68fb99e61 100644 --- a/client/ayon_core/pipeline/traits/three_dimensional.py +++ b/client/ayon_core/pipeline/traits/three_dimensional.py @@ -1,11 +1,11 @@ """3D traits.""" +from dataclasses import dataclass from typing import ClassVar -from pydantic import Field - from .trait import TraitBase +@dataclass class Spatial(TraitBase): """Spatial trait model. @@ -29,11 +29,13 @@ class Spatial(TraitBase): id: ClassVar[str] = "ayon.3d.Spatial.v1" name: ClassVar[str] = "Spatial" description: ClassVar[str] = "Spatial trait model." - up_axis: str = Field(..., title="Up axis") - handedness: str = Field(..., title="Handedness") - meters_per_unit: float = Field(..., title="Meters per unit") + persistent: ClassVar[bool] = True + up_axis: str + handedness: str + meters_per_unit: float +@dataclass class Geometry(TraitBase): """Geometry type trait model. @@ -45,8 +47,10 @@ class Geometry(TraitBase): id: ClassVar[str] = "ayon.3d.Geometry.v1" name: ClassVar[str] = "Geometry" description: ClassVar[str] = "Geometry trait model." + persistent: ClassVar[bool] = True +@dataclass class Shader(TraitBase): """Shader trait model. @@ -58,8 +62,10 @@ class Shader(TraitBase): id: ClassVar[str] = "ayon.3d.Shader.v1" name: ClassVar[str] = "Shader" description: ClassVar[str] = "Shader trait model." + persistent: ClassVar[bool] = True +@dataclass class Lighting(TraitBase): """Lighting trait model. @@ -71,8 +77,10 @@ class Lighting(TraitBase): id: ClassVar[str] = "ayon.3d.Lighting.v1" name: ClassVar[str] = "Lighting" description: ClassVar[str] = "Lighting trait model." + persistent: ClassVar[bool] = True +@dataclass class IESProfile(TraitBase): """IES profile (IES-LM-64) type trait model. @@ -82,3 +90,4 @@ class IESProfile(TraitBase): id: ClassVar[str] = "ayon.3d.IESProfile.v1" name: ClassVar[str] = "IESProfile" description: ClassVar[str] = "IES profile trait model." + persistent: ClassVar[bool] = True diff --git a/client/ayon_core/pipeline/traits/trait.py b/client/ayon_core/pipeline/traits/trait.py index f15c525495..01a7641c59 100644 --- a/client/ayon_core/pipeline/traits/trait.py +++ b/client/ayon_core/pipeline/traits/trait.py @@ -3,16 +3,9 @@ from __future__ import annotations import re from abc import ABC, abstractmethod +from dataclasses import dataclass from typing import TYPE_CHECKING, Generic, Optional, TypeVar -import pydantic.alias_generators -from pydantic import ( - AliasGenerator, - BaseModel, - ConfigDict, - Field, -) - if TYPE_CHECKING: from .representation import Representation @@ -20,25 +13,15 @@ if TYPE_CHECKING: T = TypeVar("T", bound="TraitBase") -class TraitBase(ABC, BaseModel): +@dataclass +class TraitBase(ABC): """Base trait model. This model must be used as a base for all trait models. - It is using Pydantic BaseModel for serialization and validation. ``id``, ``name``, and ``description`` are abstract attributes that must be implemented in the derived classes. """ - model_config = ConfigDict( - alias_generator=AliasGenerator( - serialization_alias=pydantic.alias_generators.to_camel, - ) - ) - - persistent: bool = Field( - default=True, title="Persistent", - description="Whether the trait is persistent (integrated) or not.") - @property @abstractmethod def id(self) -> str: diff --git a/client/ayon_core/pipeline/traits/two_dimensional.py b/client/ayon_core/pipeline/traits/two_dimensional.py index 77aa5767d6..93d7d8c86a 100644 --- a/client/ayon_core/pipeline/traits/two_dimensional.py +++ b/client/ayon_core/pipeline/traits/two_dimensional.py @@ -2,16 +2,16 @@ from __future__ import annotations import re +from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar, Optional -from pydantic import Field, field_validator - from .trait import TraitBase if TYPE_CHECKING: from .content import FileLocation, FileLocations +@dataclass class Image(TraitBase): """Image trait model. @@ -26,8 +26,10 @@ class Image(TraitBase): name: ClassVar[str] = "Image" description: ClassVar[str] = "Image Trait" id: ClassVar[str] = "ayon.2d.Image.v1" + persistent: ClassVar[bool] = True +@dataclass class PixelBased(TraitBase): """PixelBased trait model. @@ -45,11 +47,13 @@ class PixelBased(TraitBase): name: ClassVar[str] = "PixelBased" description: ClassVar[str] = "PixelBased Trait Model" id: ClassVar[str] = "ayon.2d.PixelBased.v1" - display_window_width: int = Field(..., title="Display Window Width") - display_window_height: int = Field(..., title="Display Window Height") - pixel_aspect_ratio: float = Field(..., title="Pixel Aspect Ratio") + persistent: ClassVar[bool] = True + display_window_width: int + display_window_height: int + pixel_aspect_ratio: float +@dataclass class Planar(TraitBase): """Planar trait model. @@ -57,7 +61,7 @@ class Planar(TraitBase): Todo: * (antirotor): Is this really a planar configuration? As with - bitplanes and everything? If it serves as differentiator for + bit planes and everything? If it serves as differentiator for Deep images, should it be named differently? Like Raster? Attributes: @@ -70,9 +74,11 @@ class Planar(TraitBase): name: ClassVar[str] = "Planar" description: ClassVar[str] = "Planar Trait Model" id: ClassVar[str] = "ayon.2d.Planar.v1" - planar_configuration: str = Field(..., title="Planar-based Image") + persistent: ClassVar[bool] = True + planar_configuration: str +@dataclass class Deep(TraitBase): """Deep trait model. @@ -87,8 +93,10 @@ class Deep(TraitBase): name: ClassVar[str] = "Deep" description: ClassVar[str] = "Deep Trait Model" id: ClassVar[str] = "ayon.2d.Deep.v1" + persistent: ClassVar[bool] = True +@dataclass class Overscan(TraitBase): """Overscan trait model. @@ -108,12 +116,14 @@ class Overscan(TraitBase): name: ClassVar[str] = "Overscan" description: ClassVar[str] = "Overscan Trait" id: ClassVar[str] = "ayon.2d.Overscan.v1" - left: int = Field(..., title="Left Overscan") - right: int = Field(..., title="Right Overscan") - top: int = Field(..., title="Top Overscan") - bottom: int = Field(..., title="Bottom Overscan") + persistent: ClassVar[bool] = True + left: int + right: int + top: int + bottom: int +@dataclass class UDIM(TraitBase): """UDIM trait model. @@ -124,16 +134,18 @@ class UDIM(TraitBase): description (str): Trait description. id (str): id should be namespaced trait name with version udim (int): UDIM value. + udim_regex (str): UDIM regex. """ name: ClassVar[str] = "UDIM" description: ClassVar[str] = "UDIM Trait" id: ClassVar[str] = "ayon.2d.UDIM.v1" - udim: list[int] = Field(..., title="UDIM") - udim_regex: Optional[str] = Field( - default=r"(?:\.|_)(?P\d+)\.\D+\d?$", title="UDIM Regex") + persistent: ClassVar[bool] = True + udim: list[int] + udim_regex: Optional[str] = r"(?:\.|_)(?P\d+)\.\D+\d?$" - @field_validator("udim_regex") + # field validator for udim_regex - this works in pydantic model v2 but not + # with the pure data classes @classmethod def validate_frame_regex(cls, v: Optional[str]) -> Optional[str]: """Validate udim regex. diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 4412e4489b..27da278c5e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -54,6 +54,7 @@ from ayon_core.pipeline.plugin_discover import ( from ayon_core.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, + HiddenCreator, ) _NOT_SET = object() @@ -309,7 +310,13 @@ class AbstractTemplateBuilder(ABC): self._creators_by_name = creators_by_name def _collect_creators(self): - self._creators_by_name = dict(self.create_context.creators) + self._creators_by_name = { + identifier: creator + for identifier, creator + in self.create_context.manual_creators.items() + # Do not list HiddenCreator even though it is a 'manual creator' + if not isinstance(creator, HiddenCreator) + } def get_creators_by_name(self): if self._creators_by_name is None: 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 354d877b62..677ebb04a2 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -116,11 +116,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if not_found_folder_paths: joined_folder_paths = ", ".join( - ["\"{}\"".format(path) for path in not_found_folder_paths] + [f"\"{path}\"" for path in not_found_folder_paths] + ) + self.log.warning( + f"Not found folder entities with paths {joined_folder_paths}." ) - self.log.warning(( - "Not found folder entities with paths \"{}\"." - ).format(joined_folder_paths)) def fill_missing_task_entities(self, context, project_name): self.log.debug("Querying task entities for instances.") diff --git a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py index 62b4cefec6..0a4efc2172 100644 --- a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py +++ b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py @@ -1,78 +1,120 @@ +"""Plugin for collecting OTIO frame ranges and related timing information. + +This module contains a unified plugin that handles: +- Basic timeline frame ranges +- Source media frame ranges +- Retimed clip frame ranges """ -Requires: - otioTimeline -> context data attribute - review -> instance data attribute - masterLayer -> instance data attribute - otioClipRange -> instance data attribute -""" + from pprint import pformat +import opentimelineio as otio import pyblish.api +from ayon_core.pipeline.editorial import ( + get_media_range_with_retimes, + otio_range_to_frame_range, + otio_range_with_handles, +) -class CollectOtioFrameRanges(pyblish.api.InstancePlugin): - """Getting otio ranges from otio_clip +def validate_otio_clip(instance, logger): + """Validate if instance has required OTIO clip data. - Adding timeline and source ranges to instance data""" + Args: + instance: The instance to validate + logger: Logger object to use for debug messages - label = "Collect OTIO Frame Ranges" + Returns: + bool: True if valid, False otherwise + """ + if not instance.data.get("otioClip"): + logger.debug("Skipping collect OTIO range - no clip found.") + return False + return True + + +class CollectOtioRanges(pyblish.api.InstancePlugin): + """Collect all OTIO-related frame ranges and timing information. + + This plugin handles collection of: + - Basic timeline frame ranges with handles + - Source media frame ranges with handles + - Retimed clip frame ranges + + Requires: + otioClip (otio.schema.Clip): OTIO clip object + workfileFrameStart (int): Starting frame of work file + + Optional: + shotDurationFromSource (int): Duration from source if retimed + + Provides: + frameStart (int): Start frame in timeline + frameEnd (int): End frame in timeline + clipIn (int): Clip in point + clipOut (int): Clip out point + clipInH (int): Clip in point with handles + clipOutH (int): Clip out point with handles + sourceStart (int): Source media start frame + sourceEnd (int): Source media end frame + sourceStartH (int): Source media start frame with handles + sourceEndH (int): Source media end frame with handles + """ + + label = "Collect OTIO Ranges" order = pyblish.api.CollectorOrder - 0.08 families = ["shot", "clip"] - hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, instance): - # Not all hosts can import these modules. - import opentimelineio as otio - from ayon_core.pipeline.editorial import ( - get_media_range_with_retimes, - otio_range_to_frame_range, - otio_range_with_handles - ) + """Process the instance to collect all frame ranges. - if not instance.data.get("otioClip"): - self.log.debug("Skipping collect OTIO frame range.") + Args: + instance: The instance to process + """ + if not validate_otio_clip(instance, self.log): return - # get basic variables otio_clip = instance.data["otioClip"] + + # Collect timeline ranges if workfile start frame is available + if "workfileFrameStart" in instance.data: + self._collect_timeline_ranges(instance, otio_clip) + + # Traypublisher Simple or Advanced editorial publishing is + # working with otio clips which are having no available range + # because they are not having any media references. + try: + otio_clip.available_range() + has_available_range = True + except otio._otio.CannotComputeAvailableRangeError: + self.log.info("Clip has no available range") + has_available_range = False + + # Collect source ranges if clip has available range + if has_available_range: + self._collect_source_ranges(instance, otio_clip) + + # Handle retimed ranges if source duration is available + if "shotDurationFromSource" in instance.data: + self._collect_retimed_ranges(instance, otio_clip) + + def _collect_timeline_ranges(self, instance, otio_clip): + """Collect basic timeline frame ranges.""" workfile_start = instance.data["workfileFrameStart"] - workfile_source_duration = instance.data.get("shotDurationFromSource") - # get ranges + # Get timeline ranges otio_tl_range = otio_clip.range_in_parent() - otio_src_range = otio_clip.source_range - otio_avalable_range = otio_clip.available_range() otio_tl_range_handles = otio_range_with_handles( - otio_tl_range, instance) - otio_src_range_handles = otio_range_with_handles( - otio_src_range, instance) + otio_tl_range, + instance + ) - # get source avalable start frame - src_starting_from = otio.opentime.to_frames( - otio_avalable_range.start_time, - otio_avalable_range.start_time.rate) + # Convert to frames + tl_start, tl_end = otio_range_to_frame_range(otio_tl_range) + tl_start_h, tl_end_h = otio_range_to_frame_range(otio_tl_range_handles) - # convert to frames - range_convert = otio_range_to_frame_range - tl_start, tl_end = range_convert(otio_tl_range) - tl_start_h, tl_end_h = range_convert(otio_tl_range_handles) - src_start, src_end = range_convert(otio_src_range) - src_start_h, src_end_h = range_convert(otio_src_range_handles) frame_start = workfile_start - frame_end = frame_start + otio.opentime.to_frames( - otio_tl_range.duration, otio_tl_range.duration.rate) - 1 - - # in case of retimed clip and frame range should not be retimed - if workfile_source_duration: - # get available range trimmed with processed retimes - retimed_attributes = get_media_range_with_retimes( - otio_clip, 0, 0) - self.log.debug( - ">> retimed_attributes: {}".format(retimed_attributes)) - media_in = int(retimed_attributes["mediaIn"]) - media_out = int(retimed_attributes["mediaOut"]) - frame_end = frame_start + (media_out - media_in) + 1 - self.log.debug(frame_end) + frame_end = frame_start + otio_tl_range.duration.to_frames() - 1 data = { "frameStart": frame_start, @@ -81,13 +123,77 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): "clipOut": tl_end - 1, "clipInH": tl_start_h, "clipOutH": tl_end_h - 1, - "sourceStart": src_starting_from + src_start, - "sourceEnd": src_starting_from + src_end - 1, - "sourceStartH": src_starting_from + src_start_h, - "sourceEndH": src_starting_from + src_end_h - 1, } instance.data.update(data) - self.log.debug( - "_ data: {}".format(pformat(data))) - self.log.debug( - "_ instance.data: {}".format(pformat(instance.data))) + self.log.debug(f"Added frame ranges: {pformat(data)}") + + def _collect_source_ranges(self, instance, otio_clip): + """Collect source media frame ranges.""" + # Get source ranges + otio_src_range = otio_clip.source_range + otio_available_range = otio_clip.available_range() + + # Backward-compatibility for Hiero OTIO exporter. + # NTSC compatibility might introduce floating rates, when these are + # not exactly the same (23.976 vs 23.976024627685547) + # this will cause precision issue in computation. + # Currently round to 2 decimals for comparison, + # but this should always rescale after that. + rounded_av_rate = round(otio_available_range.start_time.rate, 2) + rounded_src_rate = round(otio_src_range.start_time.rate, 2) + if rounded_av_rate != rounded_src_rate: + conformed_src_in = otio_src_range.start_time.rescaled_to( + otio_available_range.start_time.rate + ) + conformed_src_duration = otio_src_range.duration.rescaled_to( + otio_available_range.duration.rate + ) + conformed_source_range = otio.opentime.TimeRange( + start_time=conformed_src_in, + duration=conformed_src_duration + ) + else: + conformed_source_range = otio_src_range + + source_start = conformed_source_range.start_time + source_end = source_start + conformed_source_range.duration + handle_start = otio.opentime.RationalTime( + instance.data.get("handleStart", 0), + source_start.rate + ) + handle_end = otio.opentime.RationalTime( + instance.data.get("handleEnd", 0), + source_start.rate + ) + source_start_h = source_start - handle_start + source_end_h = source_end + handle_end + data = { + "sourceStart": source_start.to_frames(), + "sourceEnd": source_end.to_frames() - 1, + "sourceStartH": source_start_h.to_frames(), + "sourceEndH": source_end_h.to_frames() - 1, + } + instance.data.update(data) + self.log.debug(f"Added source ranges: {pformat(data)}") + + def _collect_retimed_ranges(self, instance, otio_clip): + """Handle retimed clip frame ranges.""" + retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0) + self.log.debug(f"Retimed attributes: {retimed_attributes}") + + frame_start = instance.data["frameStart"] + media_in = int(retimed_attributes["mediaIn"]) + media_out = int(retimed_attributes["mediaOut"]) + frame_end = frame_start + (media_out - media_in) + + data = { + "frameStart": frame_start, + "frameEnd": frame_end, + "sourceStart": media_in, + "sourceEnd": media_out, + "sourceStartH": media_in - int(retimed_attributes["handleStart"]), + "sourceEndH": media_out + int(retimed_attributes["handleEnd"]), + } + + instance.data.update(data) + self.log.debug(f"Updated retimed values: {data}") diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 2461195b27..7a9a020ff0 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -286,7 +286,7 @@ class ExtractOTIOReview( ) instance.data["representations"].append(representation) - self.log.info("Adding representation: {}".format(representation)) + self.log.debug("Adding representation: {}".format(representation)) def _create_representation(self, start, duration): """ diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 180cb8bbf1..a2698b03de 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -1,7 +1,7 @@ from operator import attrgetter import dataclasses import os -from typing import Dict +from typing import Any, Dict, List import pyblish.api try: @@ -14,7 +14,8 @@ from ayon_core.lib import ( BoolDef, UISeparatorDef, UILabelDef, - EnumDef + EnumDef, + filter_profiles ) try: from ayon_core.pipeline.usdlib import ( @@ -281,6 +282,9 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "fx": 500, "lighting": 600, } + # Default profiles to set certain instance attribute defaults based on + # profiles in settings + profiles: List[Dict[str, Any]] = [] @classmethod def apply_settings(cls, project_settings): @@ -298,6 +302,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, if contribution_layers: cls.contribution_layers = contribution_layers + cls.profiles = plugin_settings.get("profiles", []) + def process(self, instance): attr_values = self.get_attr_values_from_data(instance.data) @@ -463,6 +469,28 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, if not cls.instance_matches_plugin_families(instance): return [] + # Set default target layer based on product type + current_context_task_type = create_context.get_current_task_type() + profile = filter_profiles(cls.profiles, { + "product_types": instance.data["productType"], + "task_types": current_context_task_type + }) + if not profile: + profile = {} + + # Define defaults + default_contribution_layer = profile.get( + "contribution_layer", None) + default_apply_as_variant = profile.get( + "contribution_apply_as_variant", False) + default_target_product = profile.get( + "contribution_target_product", "usdAsset") + default_init_as = ( + "asset" + if profile.get("contribution_target_product") == "usdAsset" + else "shot") + init_as_visible = False + # Attributes logic publish_attributes = instance["publish_attributes"].get( cls.__name__, {}) @@ -495,7 +523,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the contribution itself will be added to the " "department layer." ), - default="usdAsset", + default=default_target_product, visible=visible), EnumDef("contribution_target_product_init", label="Initialize as", @@ -507,8 +535,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "setting will do nothing." ), items=["asset", "shot"], - default="asset", - visible=visible), + default=default_init_as, + visible=visible and init_as_visible), # Asset layer, e.g. model.usd, look.usd, rig.usd EnumDef("contribution_layer", @@ -520,7 +548,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the list) will contribute as a stronger opinion." ), items=list(cls.contribution_layers.keys()), - default="model", + default=default_contribution_layer, visible=visible), BoolDef("contribution_apply_as_variant", label="Add as variant", @@ -532,7 +560,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "appended to as a sublayer to the department layer " "instead." ), - default=True, + default=default_apply_as_variant, visible=visible), TextDef("contribution_variant_set_name", label="Variant Set Name", @@ -588,31 +616,6 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs) -class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): - """ - This is solely here to expose the attribute definitions for the - Houdini "look" family. - """ - # TODO: Improve how this is built for the look family - hosts = ["houdini"] - families = ["look"] - label = CollectUSDLayerContributions.label + " (Look)" - - @classmethod - def get_attr_defs_for_instance(cls, create_context, instance): - # Filtering of instance, if needed, can be customized - if not cls.instance_matches_plugin_families(instance): - return [] - - defs = super().get_attr_defs_for_instance(create_context, instance) - - # Update default for department layer to look - layer_def = next(d for d in defs if d.key == "contribution_layer") - layer_def.default = "look" - - return defs - - class ValidateUSDDependencies(pyblish.api.InstancePlugin): families = ["usdLayer"] diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 0b790dfbbd..26b476de1f 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -108,6 +108,7 @@ class VersionItem: version (int): Version. Can be negative when is hero version. is_hero (bool): Is hero version. product_id (str): Product id. + task_id (Union[str, None]): Task id. thumbnail_id (Union[str, None]): Thumbnail id. published_time (Union[str, None]): Published time in format '%Y%m%dT%H%M%SZ'. @@ -127,6 +128,7 @@ class VersionItem: version, is_hero, product_id, + task_id, thumbnail_id, published_time, author, @@ -140,6 +142,7 @@ class VersionItem: ): self.version_id = version_id self.product_id = product_id + self.task_id = task_id self.thumbnail_id = thumbnail_id self.version = version self.is_hero = is_hero @@ -161,6 +164,7 @@ class VersionItem: and self.version == other.version and self.version_id == other.version_id and self.product_id == other.product_id + and self.task_id == other.task_id ) def __ne__(self, other): @@ -198,6 +202,7 @@ class VersionItem: return { "version_id": self.version_id, "product_id": self.product_id, + "task_id": self.task_id, "thumbnail_id": self.thumbnail_id, "version": self.version, "is_hero": self.is_hero, @@ -536,6 +541,55 @@ class FrontendLoaderController(_BaseLoaderController): """ pass + @abstractmethod + def get_task_items(self, project_name, folder_ids, sender=None): + """Task items for folder ids. + + Args: + project_name (str): Project name. + folder_ids (Iterable[str]): Folder ids. + sender (Optional[str]): Sender who requested the items. + + Returns: + list[TaskItem]: List of task items. + + """ + pass + + @abstractmethod + def get_task_type_items(self, project_name, sender=None): + """Task type items for a project. + + This function may trigger events with topics + 'projects.task_types.refresh.started' and + 'projects.task_types.refresh.finished' which will contain 'sender' + value in data. + That may help to avoid re-refresh of items in UI elements. + + Args: + project_name (str): Project name. + sender (str): Who requested task type items. + + Returns: + list[TaskTypeItem]: Task type information. + + """ + pass + + @abstractmethod + def get_folder_labels(self, project_name, folder_ids): + """Get folder labels for folder ids. + + Args: + project_name (str): Project name. + folder_ids (Iterable[str]): Folder ids. + + Returns: + dict[str, Optional[str]]: Folder labels by folder id. + + """ + pass + @abstractmethod def get_project_status_items(self, project_name, sender=None): """Items for all projects available on server. @@ -717,8 +771,30 @@ class FrontendLoaderController(_BaseLoaderController): Returns: list[str]: Selected folder ids. - """ + """ + pass + + @abstractmethod + def get_selected_task_ids(self): + """Get selected task ids. + + The information is based on last selection from UI. + + Returns: + list[str]: Selected folder ids. + + """ + pass + + @abstractmethod + def set_selected_tasks(self, task_ids): + """Set selected tasks. + + Args: + task_ids (Iterable[str]): Selected task ids. + + """ pass @abstractmethod @@ -729,8 +805,8 @@ class FrontendLoaderController(_BaseLoaderController): Returns: list[str]: Selected version ids. - """ + """ pass @abstractmethod diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 16cf7c31c7..7959a63edb 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -198,6 +198,31 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) + def get_task_items(self, project_name, folder_ids, sender=None): + output = [] + for folder_id in folder_ids: + output.extend(self._hierarchy_model.get_task_items( + project_name, folder_id, sender + )) + return output + + def get_task_type_items(self, project_name, sender=None): + return self._projects_model.get_task_type_items( + project_name, sender + ) + + def get_folder_labels(self, project_name, folder_ids): + folder_items_by_id = self._hierarchy_model.get_folder_items_by_id( + project_name, folder_ids + ) + output = {} + for folder_id, folder_item in folder_items_by_id.items(): + label = None + if folder_item is not None: + label = folder_item.label + output[folder_id] = label + return output + def get_product_items(self, project_name, folder_ids, sender=None): return self._products_model.get_product_items( project_name, folder_ids, sender) @@ -299,6 +324,12 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): def set_selected_folders(self, folder_ids): self._selection_model.set_selected_folders(folder_ids) + def get_selected_task_ids(self): + return self._selection_model.get_selected_task_ids() + + def set_selected_tasks(self, task_ids): + self._selection_model.set_selected_tasks(task_ids) + def get_selected_version_ids(self): return self._selection_model.get_selected_version_ids() diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 58eab0cabe..34acc0550c 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -55,6 +55,7 @@ def version_item_from_entity(version): version=version_num, is_hero=is_hero, product_id=version["productId"], + task_id=version["taskId"], thumbnail_id=version["thumbnailId"], published_time=published_time, author=author, diff --git a/client/ayon_core/tools/loader/models/selection.py b/client/ayon_core/tools/loader/models/selection.py index 326ff835f6..04add26f86 100644 --- a/client/ayon_core/tools/loader/models/selection.py +++ b/client/ayon_core/tools/loader/models/selection.py @@ -14,6 +14,7 @@ class SelectionModel(object): self._project_name = None self._folder_ids = set() + self._task_ids = set() self._version_ids = set() self._representation_ids = set() @@ -48,6 +49,23 @@ class SelectionModel(object): self.event_source ) + def get_selected_task_ids(self): + return self._task_ids + + def set_selected_tasks(self, task_ids): + if task_ids == self._task_ids: + return + + self._task_ids = task_ids + self._controller.emit_event( + "selection.tasks.changed", + { + "project_name": self._project_name, + "task_ids": task_ids, + }, + self.event_source + ) + def get_selected_version_ids(self): return self._version_ids diff --git a/client/ayon_core/tools/loader/ui/_multicombobox.py b/client/ayon_core/tools/loader/ui/_multicombobox.py index 9efe57ef0f..393272fdf9 100644 --- a/client/ayon_core/tools/loader/ui/_multicombobox.py +++ b/client/ayon_core/tools/loader/ui/_multicombobox.py @@ -1,7 +1,10 @@ +from __future__ import annotations +import typing from typing import List, Tuple, Optional, Iterable, Any from qtpy import QtWidgets, QtCore, QtGui +from ayon_core.tools.utils import get_qt_icon from ayon_core.tools.utils.lib import ( checkstate_int_to_enum, checkstate_enum_to_int, @@ -11,14 +14,269 @@ from ayon_core.tools.utils.constants import ( UNCHECKED_INT, ITEM_IS_USER_TRISTATE, ) +if typing.TYPE_CHECKING: + from ayon_core.tools.loader.abstract import FrontendLoaderController VALUE_ITEM_TYPE = 0 STANDARD_ITEM_TYPE = 1 SEPARATOR_ITEM_TYPE = 2 +VALUE_ITEM_SUBTYPE = 0 +SELECT_ALL_SUBTYPE = 1 +DESELECT_ALL_SUBTYPE = 2 +SWAP_STATE_SUBTYPE = 3 + + +class BaseQtModel(QtGui.QStandardItemModel): + _empty_icon = None + + def __init__( + self, + item_type_role: int, + item_subtype_role: int, + empty_values_label: str, + controller: FrontendLoaderController, + ): + self._item_type_role = item_type_role + self._item_subtype_role = item_subtype_role + self._empty_values_label = empty_values_label + self._controller = controller + + self._last_project = None + + self._select_project_item = None + self._empty_values_item = None + + self._select_all_item = None + self._deselect_all_item = None + self._swap_states_item = None + + super().__init__() + + self.refresh(None) + + def _get_standard_items(self) -> list[QtGui.QStandardItem]: + raise NotImplementedError( + "'_get_standard_items' is not implemented" + f" for {self.__class__}" + ) + + def _clear_standard_items(self): + raise NotImplementedError( + "'_clear_standard_items' is not implemented" + f" for {self.__class__}" + ) + + def _prepare_new_value_items( + self, project_name: str, project_changed: bool + ) -> tuple[ + list[QtGui.QStandardItem], list[QtGui.QStandardItem] + ]: + raise NotImplementedError( + "'_prepare_new_value_items' is not implemented" + f" for {self.__class__}" + ) + + def refresh(self, project_name: Optional[str]): + # New project was selected + project_changed = False + if project_name != self._last_project: + self._last_project = project_name + project_changed = True + + if project_name is None: + self._add_select_project_item() + return + + value_items, items_to_remove = self._prepare_new_value_items( + project_name, project_changed + ) + if not value_items: + self._add_empty_values_item() + return + + self._remove_empty_items() + + root_item = self.invisibleRootItem() + for row_idx, value_item in enumerate(value_items): + if value_item.row() == row_idx: + continue + if value_item.row() >= 0: + root_item.takeRow(value_item.row()) + root_item.insertRow(row_idx, value_item) + + for item in items_to_remove: + root_item.removeRow(item.row()) + + self._add_selection_items() + + def setData(self, index, value, role): + if role == QtCore.Qt.CheckStateRole and index.isValid(): + item_subtype = index.data(self._item_subtype_role) + if item_subtype == SELECT_ALL_SUBTYPE: + for item in self._get_standard_items(): + item.setCheckState(QtCore.Qt.Checked) + return True + if item_subtype == DESELECT_ALL_SUBTYPE: + for item in self._get_standard_items(): + item.setCheckState(QtCore.Qt.Unchecked) + return True + if item_subtype == SWAP_STATE_SUBTYPE: + for item in self._get_standard_items(): + current_state = item.checkState() + item.setCheckState( + QtCore.Qt.Checked + if current_state == QtCore.Qt.Unchecked + else QtCore.Qt.Unchecked + ) + return True + return super().setData(index, value, role) + + @classmethod + def _get_empty_icon(cls): + if cls._empty_icon is None: + pix = QtGui.QPixmap(1, 1) + pix.fill(QtCore.Qt.transparent) + cls._empty_icon = QtGui.QIcon(pix) + return cls._empty_icon + + def _init_default_items(self): + if self._empty_values_item is not None: + return + + empty_values_item = QtGui.QStandardItem(self._empty_values_label) + select_project_item = QtGui.QStandardItem("Select project...") + + select_all_item = QtGui.QStandardItem("Select all") + deselect_all_item = QtGui.QStandardItem("Deselect all") + swap_states_item = QtGui.QStandardItem("Swap") + + for item in ( + empty_values_item, + select_project_item, + select_all_item, + deselect_all_item, + swap_states_item, + ): + item.setData(STANDARD_ITEM_TYPE, self._item_type_role) + + select_all_item.setIcon(get_qt_icon({ + "type": "material-symbols", + "name": "done_all", + "color": "white" + })) + deselect_all_item.setIcon(get_qt_icon({ + "type": "material-symbols", + "name": "remove_done", + "color": "white" + })) + swap_states_item.setIcon(get_qt_icon({ + "type": "material-symbols", + "name": "swap_horiz", + "color": "white" + })) + + for item in ( + empty_values_item, + select_project_item, + ): + item.setFlags(QtCore.Qt.NoItemFlags) + + for item, item_type in ( + (select_all_item, SELECT_ALL_SUBTYPE), + (deselect_all_item, DESELECT_ALL_SUBTYPE), + (swap_states_item, SWAP_STATE_SUBTYPE), + ): + item.setData(item_type, self._item_subtype_role) + + for item in ( + select_all_item, + deselect_all_item, + swap_states_item, + ): + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) + + self._empty_values_item = empty_values_item + self._select_project_item = select_project_item + + self._select_all_item = select_all_item + self._deselect_all_item = deselect_all_item + self._swap_states_item = swap_states_item + + def _get_empty_values_item(self): + self._init_default_items() + return self._empty_values_item + + def _get_select_project_item(self): + self._init_default_items() + return self._select_project_item + + def _get_empty_items(self): + self._init_default_items() + return [ + self._empty_values_item, + self._select_project_item, + ] + + def _get_selection_items(self): + self._init_default_items() + return [ + self._select_all_item, + self._deselect_all_item, + self._swap_states_item, + ] + + def _get_default_items(self): + return self._get_empty_items() + self._get_selection_items() + + def _add_select_project_item(self): + item = self._get_select_project_item() + if item.row() < 0: + self._remove_items() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _add_empty_values_item(self): + item = self._get_empty_values_item() + if item.row() < 0: + self._remove_items() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _add_selection_items(self): + root_item = self.invisibleRootItem() + items = self._get_selection_items() + for item in self._get_selection_items(): + row = item.row() + if row >= 0: + root_item.takeRow(row) + root_item.appendRows(items) + + def _remove_items(self): + root_item = self.invisibleRootItem() + for item in self._get_default_items(): + if item.row() < 0: + continue + root_item.takeRow(item.row()) + + root_item.removeRows(0, root_item.rowCount()) + self._clear_standard_items() + + def _remove_empty_items(self): + root_item = self.invisibleRootItem() + for item in self._get_empty_items(): + if item.row() < 0: + continue + root_item.takeRow(item.row()) + class CustomPaintDelegate(QtWidgets.QStyledItemDelegate): """Delegate showing status name and short name.""" + _empty_icon = None _checked_value = checkstate_enum_to_int(QtCore.Qt.Checked) _checked_bg_color = QtGui.QColor("#2C3B4C") @@ -38,6 +296,14 @@ class CustomPaintDelegate(QtWidgets.QStyledItemDelegate): self._icon_role = icon_role self._item_type_role = item_type_role + @classmethod + def _get_empty_icon(cls): + if cls._empty_icon is None: + pix = QtGui.QPixmap(1, 1) + pix.fill(QtCore.Qt.transparent) + cls._empty_icon = QtGui.QIcon(pix) + return cls._empty_icon + def paint(self, painter, option, index): item_type = None if self._item_type_role is not None: @@ -70,6 +336,9 @@ class CustomPaintDelegate(QtWidgets.QStyledItemDelegate): if option.state & QtWidgets.QStyle.State_Open: state = QtGui.QIcon.On icon = self._get_index_icon(index) + if icon is None or icon.isNull(): + icon = self._get_empty_icon() + option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration # Disable visible check indicator @@ -241,6 +510,10 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): QtCore.Qt.Key_Home, QtCore.Qt.Key_End, } + _top_bottom_margins = 1 + _top_bottom_padding = 2 + _left_right_padding = 3 + _item_bg_color = QtGui.QColor("#31424e") def __init__( self, @@ -433,14 +706,14 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): idxs = self._get_checked_idx() # draw the icon and text - draw_text = True + draw_items = False combotext = None if self._custom_text is not None: combotext = self._custom_text elif not idxs: combotext = self._placeholder_text else: - draw_text = False + draw_items = True content_field_rect = self.style().subControlRect( QtWidgets.QStyle.CC_ComboBox, @@ -448,7 +721,9 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): QtWidgets.QStyle.SC_ComboBoxEditField ).adjusted(1, 0, -1, 0) - if draw_text: + if draw_items: + self._paint_items(painter, idxs, content_field_rect) + else: color = option.palette.color(QtGui.QPalette.Text) color.setAlpha(67) pen = painter.pen() @@ -459,15 +734,12 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, combotext ) - else: - self._paint_items(painter, idxs, content_field_rect) painter.end() def _paint_items(self, painter, indexes, content_rect): origin_rect = QtCore.QRect(content_rect) - metrics = self.fontMetrics() model = self.model() available_width = content_rect.width() total_used_width = 0 @@ -482,31 +754,80 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): continue icon = index.data(self._icon_role) - # TODO handle this case - if icon is None or icon.isNull(): - continue + text = index.data(self._text_role) + valid_icon = icon is not None and not icon.isNull() + if valid_icon: + sizes = icon.availableSizes() + if sizes: + valid_icon = any(size.width() > 1 for size in sizes) - icon_rect = QtCore.QRect(content_rect) - diff = icon_rect.height() - metrics.height() - if diff < 0: - diff = 0 - top_offset = diff // 2 - bottom_offset = diff - top_offset - icon_rect.adjust(0, top_offset, 0, -bottom_offset) - icon_rect.setWidth(metrics.height()) - icon.paint( - painter, - icon_rect, - QtCore.Qt.AlignCenter, - QtGui.QIcon.Normal, - QtGui.QIcon.On - ) - content_rect.setLeft(icon_rect.right() + spacing) - if total_used_width > 0: - total_used_width += spacing - total_used_width += icon_rect.width() - if total_used_width > available_width: - break + if valid_icon: + metrics = self.fontMetrics() + icon_rect = QtCore.QRect(content_rect) + diff = icon_rect.height() - metrics.height() + if diff < 0: + diff = 0 + top_offset = diff // 2 + bottom_offset = diff - top_offset + icon_rect.adjust(0, top_offset, 0, -bottom_offset) + used_width = metrics.height() + if total_used_width > 0: + total_used_width += spacing + total_used_width += used_width + if total_used_width > available_width: + break + + icon_rect.setWidth(used_width) + icon.paint( + painter, + icon_rect, + QtCore.Qt.AlignCenter, + QtGui.QIcon.Normal, + QtGui.QIcon.On + ) + content_rect.setLeft(icon_rect.right() + spacing) + + elif text: + bg_height = ( + content_rect.height() + - (2 * self._top_bottom_margins) + ) + font_height = bg_height - (2 * self._top_bottom_padding) + + bg_top_y = content_rect.y() + self._top_bottom_margins + + font = self.font() + font.setPixelSize(font_height) + metrics = QtGui.QFontMetrics(font) + painter.setFont(font) + + label_rect = metrics.boundingRect(text) + + bg_width = label_rect.width() + (2 * self._left_right_padding) + if total_used_width > 0: + total_used_width += spacing + total_used_width += bg_width + if total_used_width > available_width: + break + + bg_rect = QtCore.QRectF(label_rect) + bg_rect.moveTop(bg_top_y) + bg_rect.moveLeft(content_rect.left()) + bg_rect.setWidth(bg_width) + bg_rect.setHeight(bg_height) + + label_rect.moveTop(bg_top_y) + label_rect.moveLeft( + content_rect.left() + self._left_right_padding + ) + + path = QtGui.QPainterPath() + path.addRoundedRect(bg_rect, 5, 5) + + painter.fillPath(path, self._item_bg_color) + painter.drawText(label_rect, QtCore.Qt.AlignCenter, text) + + content_rect.setLeft(bg_rect.right() + spacing) painter.restore() diff --git a/client/ayon_core/tools/loader/ui/product_types_combo.py b/client/ayon_core/tools/loader/ui/product_types_combo.py new file mode 100644 index 0000000000..91fa52b0e9 --- /dev/null +++ b/client/ayon_core/tools/loader/ui/product_types_combo.py @@ -0,0 +1,169 @@ +from qtpy import QtGui, QtCore + +from ._multicombobox import ( + CustomPaintMultiselectComboBox, + BaseQtModel, +) + +STATUS_ITEM_TYPE = 0 +SELECT_ALL_TYPE = 1 +DESELECT_ALL_TYPE = 2 +SWAP_STATE_TYPE = 3 + +PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1 +ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 2 +ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 3 + + +class ProductTypesQtModel(BaseQtModel): + refreshed = QtCore.Signal() + + def __init__(self, controller): + self._reset_filters_on_refresh = True + self._refreshing = False + self._bulk_change = False + self._items_by_name = {} + + super().__init__( + item_type_role=ITEM_TYPE_ROLE, + item_subtype_role=ITEM_SUBTYPE_ROLE, + empty_values_label="No product types...", + controller=controller, + ) + + def is_refreshing(self): + return self._refreshing + + def refresh(self, project_name): + self._refreshing = True + super().refresh(project_name) + + self._reset_filters_on_refresh = False + self._refreshing = False + self.refreshed.emit() + + def reset_product_types_filter_on_refresh(self): + self._reset_filters_on_refresh = True + + def _get_standard_items(self) -> list[QtGui.QStandardItem]: + return list(self._items_by_name.values()) + + def _clear_standard_items(self): + self._items_by_name.clear() + + def _prepare_new_value_items(self, project_name: str, _: bool) -> tuple[ + list[QtGui.QStandardItem], list[QtGui.QStandardItem] + ]: + product_type_items = self._controller.get_product_type_items( + project_name) + self._last_project = project_name + + names_to_remove = set(self._items_by_name.keys()) + items = [] + items_filter_required = {} + for product_type_item in product_type_items: + name = product_type_item.name + names_to_remove.discard(name) + item = self._items_by_name.get(name) + # Apply filter to new items or if filters reset is requested + filter_required = self._reset_filters_on_refresh + if item is None: + filter_required = True + item = QtGui.QStandardItem(name) + item.setData(name, PRODUCT_TYPE_ROLE) + item.setEditable(False) + item.setCheckable(True) + self._items_by_name[name] = item + + items.append(item) + + if filter_required: + items_filter_required[name] = item + + if items_filter_required: + product_types_filter = self._controller.get_product_types_filter() + for product_type, item in items_filter_required.items(): + matching = ( + int(product_type in product_types_filter.product_types) + + int(product_types_filter.is_allow_list) + ) + item.setCheckState( + QtCore.Qt.Checked + if matching % 2 == 0 + else QtCore.Qt.Unchecked + ) + + items_to_remove = [] + for name in names_to_remove: + items_to_remove.append( + self._items_by_name.pop(name) + ) + + # Uncheck all if all are checked (same result) + if all( + item.checkState() == QtCore.Qt.Checked + for item in items + ): + for item in items: + item.setCheckState(QtCore.Qt.Unchecked) + + return items, items_to_remove + + +class ProductTypesCombobox(CustomPaintMultiselectComboBox): + def __init__(self, controller, parent): + self._controller = controller + model = ProductTypesQtModel(controller) + super().__init__( + PRODUCT_TYPE_ROLE, + PRODUCT_TYPE_ROLE, + QtCore.Qt.ForegroundRole, + QtCore.Qt.DecorationRole, + item_type_role=ITEM_TYPE_ROLE, + model=model, + parent=parent + ) + + model.refreshed.connect(self._on_model_refresh) + + self.set_placeholder_text("Product types filter...") + self._model = model + self._last_project_name = None + self._fully_disabled_filter = False + + controller.register_event_callback( + "selection.project.changed", + self._on_project_change + ) + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh + ) + self.setToolTip("Product types filter") + self.value_changed.connect( + self._on_product_type_filter_change + ) + + def reset_product_types_filter_on_refresh(self): + self._model.reset_product_types_filter_on_refresh() + + def _on_model_refresh(self): + self.value_changed.emit() + + def _on_product_type_filter_change(self): + lines = ["Product types filter"] + for item in self.get_value_info(): + status_name, enabled = item + lines.append(f"{'✔' if enabled else '☐'} {status_name}") + + self.setToolTip("\n".join(lines)) + + def _on_project_change(self, event): + project_name = event["project_name"] + self._last_project_name = project_name + self._model.refresh(project_name) + + def _on_projects_refresh(self): + if self._last_project_name: + self._model.refresh(self._last_project_name) + self._on_product_type_filter_change() diff --git a/client/ayon_core/tools/loader/ui/product_types_widget.py b/client/ayon_core/tools/loader/ui/product_types_widget.py deleted file mode 100644 index 9b1bf6326f..0000000000 --- a/client/ayon_core/tools/loader/ui/product_types_widget.py +++ /dev/null @@ -1,256 +0,0 @@ -from qtpy import QtWidgets, QtGui, QtCore - -from ayon_core.tools.utils import get_qt_icon - -PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1 - - -class ProductTypesQtModel(QtGui.QStandardItemModel): - refreshed = QtCore.Signal() - filter_changed = QtCore.Signal() - - def __init__(self, controller): - super(ProductTypesQtModel, self).__init__() - self._controller = controller - - self._reset_filters_on_refresh = True - self._refreshing = False - self._bulk_change = False - self._last_project = None - self._items_by_name = {} - - controller.register_event_callback( - "controller.reset.finished", - self._on_controller_reset_finish, - ) - - def is_refreshing(self): - return self._refreshing - - def get_filter_info(self): - """Product types filtering info. - - Returns: - dict[str, bool]: Filtering value by product type name. False value - means to hide product type. - """ - - return { - name: item.checkState() == QtCore.Qt.Checked - for name, item in self._items_by_name.items() - } - - def refresh(self, project_name): - self._refreshing = True - product_type_items = self._controller.get_product_type_items( - project_name) - self._last_project = project_name - - items_to_remove = set(self._items_by_name.keys()) - new_items = [] - items_filter_required = {} - for product_type_item in product_type_items: - name = product_type_item.name - items_to_remove.discard(name) - item = self._items_by_name.get(name) - # Apply filter to new items or if filters reset is requested - filter_required = self._reset_filters_on_refresh - if item is None: - filter_required = True - item = QtGui.QStandardItem(name) - item.setData(name, PRODUCT_TYPE_ROLE) - item.setEditable(False) - item.setCheckable(True) - new_items.append(item) - self._items_by_name[name] = item - - if filter_required: - items_filter_required[name] = item - - icon = get_qt_icon(product_type_item.icon) - item.setData(icon, QtCore.Qt.DecorationRole) - - if items_filter_required: - product_types_filter = self._controller.get_product_types_filter() - for product_type, item in items_filter_required.items(): - matching = ( - int(product_type in product_types_filter.product_types) - + int(product_types_filter.is_allow_list) - ) - state = ( - QtCore.Qt.Checked - if matching % 2 == 0 - else QtCore.Qt.Unchecked - ) - item.setCheckState(state) - - root_item = self.invisibleRootItem() - if new_items: - root_item.appendRows(new_items) - - for name in items_to_remove: - item = self._items_by_name.pop(name) - root_item.removeRow(item.row()) - - self._reset_filters_on_refresh = False - self._refreshing = False - self.refreshed.emit() - - def reset_product_types_filter_on_refresh(self): - self._reset_filters_on_refresh = True - - def setData(self, index, value, role=None): - checkstate_changed = False - if role is None: - role = QtCore.Qt.EditRole - elif role == QtCore.Qt.CheckStateRole: - checkstate_changed = True - output = super(ProductTypesQtModel, self).setData(index, value, role) - if checkstate_changed and not self._bulk_change: - self.filter_changed.emit() - return output - - def change_state_for_all(self, checked): - if self._items_by_name: - self.change_states(checked, self._items_by_name.keys()) - - def change_states(self, checked, product_types): - product_types = set(product_types) - if not product_types: - return - - if checked is None: - state = None - elif checked: - state = QtCore.Qt.Checked - else: - state = QtCore.Qt.Unchecked - - self._bulk_change = True - - changed = False - for product_type in product_types: - item = self._items_by_name.get(product_type) - if item is None: - continue - new_state = state - item_checkstate = item.checkState() - if new_state is None: - if item_checkstate == QtCore.Qt.Checked: - new_state = QtCore.Qt.Unchecked - else: - new_state = QtCore.Qt.Checked - elif item_checkstate == new_state: - continue - changed = True - item.setCheckState(new_state) - - self._bulk_change = False - - if changed: - self.filter_changed.emit() - - def _on_controller_reset_finish(self): - self.refresh(self._last_project) - - -class ProductTypesView(QtWidgets.QListView): - filter_changed = QtCore.Signal() - - def __init__(self, controller, parent): - super(ProductTypesView, self).__init__(parent) - - self.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection - ) - self.setAlternatingRowColors(True) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - product_types_model = ProductTypesQtModel(controller) - product_types_proxy_model = QtCore.QSortFilterProxyModel() - product_types_proxy_model.setSourceModel(product_types_model) - - self.setModel(product_types_proxy_model) - - product_types_model.refreshed.connect(self._on_refresh_finished) - product_types_model.filter_changed.connect(self._on_filter_change) - self.customContextMenuRequested.connect(self._on_context_menu) - - controller.register_event_callback( - "selection.project.changed", - self._on_project_change - ) - - self._controller = controller - self._refresh_product_types_filter = False - - self._product_types_model = product_types_model - self._product_types_proxy_model = product_types_proxy_model - - def get_filter_info(self): - return self._product_types_model.get_filter_info() - - def reset_product_types_filter_on_refresh(self): - self._product_types_model.reset_product_types_filter_on_refresh() - - def _on_project_change(self, event): - project_name = event["project_name"] - self._product_types_model.refresh(project_name) - - def _on_refresh_finished(self): - # Apply product types filter on first show - self.filter_changed.emit() - - def _on_filter_change(self): - if not self._product_types_model.is_refreshing(): - self.filter_changed.emit() - - def _change_selection_state(self, checkstate): - selection_model = self.selectionModel() - product_types = { - index.data(PRODUCT_TYPE_ROLE) - for index in selection_model.selectedIndexes() - } - product_types.discard(None) - self._product_types_model.change_states(checkstate, product_types) - - def _on_enable_all(self): - self._product_types_model.change_state_for_all(True) - - def _on_disable_all(self): - self._product_types_model.change_state_for_all(False) - - def _on_context_menu(self, pos): - menu = QtWidgets.QMenu(self) - - # Add enable all action - action_check_all = QtWidgets.QAction(menu) - action_check_all.setText("Enable All") - action_check_all.triggered.connect(self._on_enable_all) - # Add disable all action - action_uncheck_all = QtWidgets.QAction(menu) - action_uncheck_all.setText("Disable All") - action_uncheck_all.triggered.connect(self._on_disable_all) - - menu.addAction(action_check_all) - menu.addAction(action_uncheck_all) - - # Get mouse position - global_pos = self.viewport().mapToGlobal(pos) - menu.exec_(global_pos) - - def event(self, event): - if event.type() == QtCore.QEvent.KeyPress: - if event.key() == QtCore.Qt.Key_Space: - self._change_selection_state(None) - return True - - if event.key() == QtCore.Qt.Key_Backspace: - self._change_selection_state(False) - return True - - if event.key() == QtCore.Qt.Key_Return: - self._change_selection_state(True) - return True - - return super(ProductTypesView, self).event(event) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index fba9b5b3ca..8cece4687f 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -19,6 +19,7 @@ from .products_model import ( ) STATUS_NAME_ROLE = QtCore.Qt.UserRole + 1 +TASK_ID_ROLE = QtCore.Qt.UserRole + 2 class VersionsModel(QtGui.QStandardItemModel): @@ -48,6 +49,7 @@ class VersionsModel(QtGui.QStandardItemModel): item.setData(version_id, QtCore.Qt.UserRole) self._items_by_id[version_id] = item item.setData(version_item.status, STATUS_NAME_ROLE) + item.setData(version_item.task_id, TASK_ID_ROLE) if item.row() != idx: root_item.insertRow(idx, item) @@ -57,17 +59,30 @@ class VersionsFilterModel(QtCore.QSortFilterProxyModel): def __init__(self): super().__init__() self._status_filter = None + self._task_ids_filter = None def filterAcceptsRow(self, row, parent): - if self._status_filter is None: - return True + if self._status_filter is not None: + if not self._status_filter: + return False - if not self._status_filter: - return False + index = self.sourceModel().index(row, 0, parent) + status = index.data(STATUS_NAME_ROLE) + if status not in self._status_filter: + return False - index = self.sourceModel().index(row, 0, parent) - status = index.data(STATUS_NAME_ROLE) - return status in self._status_filter + if self._task_ids_filter: + index = self.sourceModel().index(row, 0, parent) + task_id = index.data(TASK_ID_ROLE) + if task_id not in self._task_ids_filter: + return False + return True + + def set_tasks_filter(self, task_ids): + if self._task_ids_filter == task_ids: + return + self._task_ids_filter = task_ids + self.invalidateFilter() def set_statuses_filter(self, status_names): if self._status_filter == status_names: @@ -101,6 +116,13 @@ class VersionComboBox(QtWidgets.QComboBox): def get_product_id(self): return self._product_id + def set_tasks_filter(self, task_ids): + self._proxy_model.set_tasks_filter(task_ids) + if self.count() == 0: + return + if self.currentIndex() != 0: + self.setCurrentIndex(0) + def set_statuses_filter(self, status_names): self._proxy_model.set_statuses_filter(status_names) if self.count() == 0: @@ -149,6 +171,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): super().__init__(*args, **kwargs) self._editor_by_id: Dict[str, VersionComboBox] = {} + self._task_ids_filter = None self._statuses_filter = None def displayText(self, value, locale): @@ -156,6 +179,11 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): return "N/A" return format_version(value) + def set_tasks_filter(self, task_ids): + self._task_ids_filter = set(task_ids) + for widget in self._editor_by_id.values(): + widget.set_tasks_filter(task_ids) + def set_statuses_filter(self, status_names): self._statuses_filter = set(status_names) for widget in self._editor_by_id.values(): @@ -239,6 +267,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): version_id = index.data(VERSION_ID_ROLE) editor.update_versions(versions, version_id) + editor.set_tasks_filter(self._task_ids_filter) editor.set_statuses_filter(self._statuses_filter) def setModelData(self, editor, model, index): diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 3571788134..cebae9bca7 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -12,34 +12,35 @@ GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 1 MERGED_COLOR_ROLE = QtCore.Qt.UserRole + 2 FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 3 FOLDER_ID_ROLE = QtCore.Qt.UserRole + 4 -PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 5 -PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 6 -PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 7 -PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 8 -PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 9 -VERSION_ID_ROLE = QtCore.Qt.UserRole + 10 -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_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 +TASK_ID_ROLE = QtCore.Qt.UserRole + 5 +PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6 +PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7 +PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8 +PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9 +PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10 +VERSION_ID_ROLE = QtCore.Qt.UserRole + 11 +VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12 +VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13 +VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14 +VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18 +VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 -STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 31 +STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32 class ProductsModel(QtGui.QStandardItemModel): @@ -368,6 +369,7 @@ class ProductsModel(QtGui.QStandardItemModel): """ model_item.setData(version_item.version_id, VERSION_ID_ROLE) + model_item.setData(version_item.task_id, TASK_ID_ROLE) model_item.setData(version_item.version, VERSION_NAME_ROLE) model_item.setData(version_item.is_hero, VERSION_HERO_ROLE) model_item.setData( diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 748a1b5fb8..94d95b9026 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -1,4 +1,6 @@ +from __future__ import annotations import collections +from typing import Optional from qtpy import QtWidgets, QtCore @@ -15,6 +17,7 @@ from .products_model import ( GROUP_TYPE_ROLE, MERGED_COLOR_ROLE, FOLDER_ID_ROLE, + TASK_ID_ROLE, PRODUCT_ID_ROLE, VERSION_ID_ROLE, VERSION_STATUS_NAME_ROLE, @@ -36,8 +39,9 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) - self._product_type_filters = {} + self._product_type_filters = None self._statuses_filter = None + self._task_ids_filter = None self._ascending_sort = True def get_statuses_filter(self): @@ -45,7 +49,15 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): return None return set(self._statuses_filter) + def set_tasks_filter(self, task_ids_filter): + if self._task_ids_filter == task_ids_filter: + return + self._task_ids_filter = task_ids_filter + self.invalidateFilter() + def set_product_type_filters(self, product_type_filters): + if self._product_type_filters == product_type_filters: + return self._product_type_filters = product_type_filters self.invalidateFilter() @@ -58,29 +70,41 @@ class ProductsProxyModel(RecursiveSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): source_model = self.sourceModel() index = source_model.index(source_row, 0, source_parent) - - product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE) - product_types = [] - if product_types_s: - product_types = product_types_s.split("|") - - for product_type in product_types: - if not self._product_type_filters.get(product_type, True): - return False - - if not self._accept_row_by_statuses(index): + if not self._accept_task_ids_filter(index): return False + + if not self._accept_row_by_role_value( + index, self._product_type_filters, PRODUCT_TYPE_ROLE + ): + return False + + if not self._accept_row_by_role_value( + index, self._statuses_filter, STATUS_NAME_FILTER_ROLE + ): + return False + return super().filterAcceptsRow(source_row, source_parent) - def _accept_row_by_statuses(self, index): - if self._statuses_filter is None: + def _accept_task_ids_filter(self, index): + if not self._task_ids_filter: return True - if not self._statuses_filter: + task_id = index.data(TASK_ID_ROLE) + return task_id in self._task_ids_filter + + def _accept_row_by_role_value( + self, + index: QtCore.QModelIndex, + filter_value: Optional[set[str]], + role: int + ): + if filter_value is None: + return True + if not filter_value: return False - status_s = index.data(STATUS_NAME_FILTER_ROLE) + status_s = index.data(role) for status in status_s.split("|"): - if status in self._statuses_filter: + if status in filter_value: return True return False @@ -120,7 +144,7 @@ class ProductsWidget(QtWidgets.QWidget): 90, # Product type 130, # Folder label 60, # Version - 100, # Status + 100, # Status 125, # Time 75, # Author 75, # Frames @@ -246,6 +270,16 @@ class ProductsWidget(QtWidgets.QWidget): """ self._products_proxy_model.setFilterFixedString(name) + def set_tasks_filter(self, task_ids): + """Set filter of version tasks. + + Args: + task_ids (set[str]): Task ids. + + """ + self._version_delegate.set_tasks_filter(task_ids) + self._products_proxy_model.set_tasks_filter(task_ids) + def set_statuses_filter(self, status_names): """Set filter of version statuses. diff --git a/client/ayon_core/tools/loader/ui/statuses_combo.py b/client/ayon_core/tools/loader/ui/statuses_combo.py index 9fe7ab62a5..2f034d00de 100644 --- a/client/ayon_core/tools/loader/ui/statuses_combo.py +++ b/client/ayon_core/tools/loader/ui/statuses_combo.py @@ -1,4 +1,4 @@ -from typing import List, Dict +from __future__ import annotations from qtpy import QtCore, QtGui @@ -7,7 +7,7 @@ from ayon_core.tools.common_models import StatusItem from ._multicombobox import ( CustomPaintMultiselectComboBox, - STANDARD_ITEM_TYPE, + BaseQtModel, ) STATUS_ITEM_TYPE = 0 @@ -24,62 +24,43 @@ ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5 ITEM_SUBTYPE_ROLE = QtCore.Qt.UserRole + 6 -class StatusesQtModel(QtGui.QStandardItemModel): +class StatusesQtModel(BaseQtModel): def __init__(self, controller): - self._controller = controller - self._items_by_name: Dict[str, QtGui.QStandardItem] = {} - self._icons_by_name_n_color: Dict[str, QtGui.QIcon] = {} - self._last_project = None + self._items_by_name: dict[str, QtGui.QStandardItem] = {} + self._icons_by_name_n_color: dict[str, QtGui.QIcon] = {} + super().__init__( + ITEM_TYPE_ROLE, + ITEM_SUBTYPE_ROLE, + "No statuses...", + controller, + ) - self._select_project_item = None - self._empty_statuses_item = None + def _get_standard_items(self) -> list[QtGui.QStandardItem]: + return list(self._items_by_name.values()) - self._select_all_item = None - self._deselect_all_item = None - self._swap_states_item = None + def _clear_standard_items(self): + self._items_by_name.clear() - super().__init__() - - self.refresh(None) - - def get_placeholder_text(self): - return self._placeholder - - def refresh(self, project_name): - # New project was selected - # status filter is reset to show all statuses - uncheck_all = False - if project_name != self._last_project: - self._last_project = project_name - uncheck_all = True - - if project_name is None: - self._add_select_project_item() - return - - status_items: List[StatusItem] = ( + def _prepare_new_value_items( + self, project_name: str, project_changed: bool + ): + status_items: list[StatusItem] = ( self._controller.get_project_status_items( project_name, sender=STATUSES_FILTER_SENDER ) ) + items = [] + items_to_remove = [] if not status_items: - self._add_empty_statuses_item() - return + return items, items_to_remove - self._remove_empty_items() - - items_to_remove = set(self._items_by_name) - root_item = self.invisibleRootItem() + names_to_remove = set(self._items_by_name) for row_idx, status_item in enumerate(status_items): name = status_item.name if name in self._items_by_name: - is_new = False item = self._items_by_name[name] - if uncheck_all: - item.setCheckState(QtCore.Qt.Unchecked) - items_to_remove.discard(name) + names_to_remove.discard(name) else: - is_new = True item = QtGui.QStandardItem() item.setData(ITEM_SUBTYPE_ROLE, STATUS_ITEM_TYPE) item.setCheckState(QtCore.Qt.Unchecked) @@ -100,36 +81,14 @@ class StatusesQtModel(QtGui.QStandardItemModel): if item.data(role) != value: item.setData(value, role) - if is_new: - root_item.insertRow(row_idx, item) + if project_changed: + item.setCheckState(QtCore.Qt.Unchecked) + items.append(item) - for name in items_to_remove: - item = self._items_by_name.pop(name) - root_item.removeRow(item.row()) + for name in names_to_remove: + items_to_remove.append(self._items_by_name.pop(name)) - self._add_selection_items() - - def setData(self, index, value, role): - if role == QtCore.Qt.CheckStateRole and index.isValid(): - item_type = index.data(ITEM_SUBTYPE_ROLE) - if item_type == SELECT_ALL_TYPE: - for item in self._items_by_name.values(): - item.setCheckState(QtCore.Qt.Checked) - return True - if item_type == DESELECT_ALL_TYPE: - for item in self._items_by_name.values(): - item.setCheckState(QtCore.Qt.Unchecked) - return True - if item_type == SWAP_STATE_TYPE: - for item in self._items_by_name.values(): - current_state = item.checkState() - item.setCheckState( - QtCore.Qt.Checked - if current_state == QtCore.Qt.Unchecked - else QtCore.Qt.Unchecked - ) - return True - return super().setData(index, value, role) + return items, items_to_remove def _get_icon(self, status_item: StatusItem) -> QtGui.QIcon: name = status_item.name @@ -147,139 +106,6 @@ class StatusesQtModel(QtGui.QStandardItemModel): self._icons_by_name_n_color[unique_id] = icon return icon - def _init_default_items(self): - if self._empty_statuses_item is not None: - return - - empty_statuses_item = QtGui.QStandardItem("No statuses...") - select_project_item = QtGui.QStandardItem("Select project...") - - select_all_item = QtGui.QStandardItem("Select all") - deselect_all_item = QtGui.QStandardItem("Deselect all") - swap_states_item = QtGui.QStandardItem("Swap") - - for item in ( - empty_statuses_item, - select_project_item, - select_all_item, - deselect_all_item, - swap_states_item, - ): - item.setData(STANDARD_ITEM_TYPE, ITEM_TYPE_ROLE) - - select_all_item.setIcon(get_qt_icon({ - "type": "material-symbols", - "name": "done_all", - "color": "white" - })) - deselect_all_item.setIcon(get_qt_icon({ - "type": "material-symbols", - "name": "remove_done", - "color": "white" - })) - swap_states_item.setIcon(get_qt_icon({ - "type": "material-symbols", - "name": "swap_horiz", - "color": "white" - })) - - for item in ( - empty_statuses_item, - select_project_item, - ): - item.setFlags(QtCore.Qt.NoItemFlags) - - for item, item_type in ( - (select_all_item, SELECT_ALL_TYPE), - (deselect_all_item, DESELECT_ALL_TYPE), - (swap_states_item, SWAP_STATE_TYPE), - ): - item.setData(item_type, ITEM_SUBTYPE_ROLE) - - for item in ( - select_all_item, - deselect_all_item, - swap_states_item, - ): - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) - - self._empty_statuses_item = empty_statuses_item - self._select_project_item = select_project_item - - self._select_all_item = select_all_item - self._deselect_all_item = deselect_all_item - self._swap_states_item = swap_states_item - - def _get_empty_statuses_item(self): - self._init_default_items() - return self._empty_statuses_item - - def _get_select_project_item(self): - self._init_default_items() - return self._select_project_item - - def _get_empty_items(self): - self._init_default_items() - return [ - self._empty_statuses_item, - self._select_project_item, - ] - - def _get_selection_items(self): - self._init_default_items() - return [ - self._select_all_item, - self._deselect_all_item, - self._swap_states_item, - ] - - def _get_default_items(self): - return self._get_empty_items() + self._get_selection_items() - - def _add_select_project_item(self): - item = self._get_select_project_item() - if item.row() < 0: - self._remove_items() - root_item = self.invisibleRootItem() - root_item.appendRow(item) - - def _add_empty_statuses_item(self): - item = self._get_empty_statuses_item() - if item.row() < 0: - self._remove_items() - root_item = self.invisibleRootItem() - root_item.appendRow(item) - - def _add_selection_items(self): - root_item = self.invisibleRootItem() - items = self._get_selection_items() - for item in self._get_selection_items(): - row = item.row() - if row >= 0: - root_item.takeRow(row) - root_item.appendRows(items) - - def _remove_items(self): - root_item = self.invisibleRootItem() - for item in self._get_default_items(): - if item.row() < 0: - continue - root_item.takeRow(item.row()) - - root_item.removeRows(0, root_item.rowCount()) - self._items_by_name.clear() - - def _remove_empty_items(self): - root_item = self.invisibleRootItem() - for item in self._get_empty_items(): - if item.row() < 0: - continue - root_item.takeRow(item.row()) - class StatusesCombobox(CustomPaintMultiselectComboBox): def __init__(self, controller, parent): diff --git a/client/ayon_core/tools/loader/ui/tasks_widget.py b/client/ayon_core/tools/loader/ui/tasks_widget.py new file mode 100644 index 0000000000..5779fc2a01 --- /dev/null +++ b/client/ayon_core/tools/loader/ui/tasks_widget.py @@ -0,0 +1,405 @@ +import collections +import hashlib + +from qtpy import QtWidgets, QtCore, QtGui + +from ayon_core.style import get_default_entity_icon_color +from ayon_core.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, + TasksQtModel, + TASKS_MODEL_SENDER_NAME, +) +from ayon_core.tools.utils.tasks_widget import ( + ITEM_ID_ROLE, + ITEM_NAME_ROLE, + PARENT_ID_ROLE, + TASK_TYPE_ROLE, +) +from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon + +# Role that can't clash with default 'tasks_widget' roles +FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100 +NO_TASKS_ID = "--no-task--" + + +class LoaderTasksQtModel(TasksQtModel): + column_labels = [ + "Task name", + "Task type", + "Folder" + ] + + def __init__(self, controller): + super().__init__(controller) + + self._items_by_id = {} + self._groups_by_name = {} + self._last_folder_ids = set() + # This item is used to be able filter versions without any task + # - do not mismatch with '_empty_tasks_item' item from 'TasksQtModel' + self._no_tasks_item = None + + def refresh(self): + """Refresh tasks for selected folders.""" + + self._refresh(self._last_project_name, self._last_folder_ids) + + def set_context(self, project_name, folder_ids): + self._refresh(project_name, folder_ids) + + # Mark some functions from 'TasksQtModel' as not implemented + def get_index_by_name(self, task_name): + raise NotImplementedError( + "Method 'get_index_by_name' is not implemented." + ) + + def get_last_folder_id(self): + raise NotImplementedError( + "Method 'get_last_folder_id' is not implemented." + ) + + def flags(self, index): + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super().flags(index) + + def _get_no_tasks_item(self): + if self._no_tasks_item is None: + item = QtGui.QStandardItem("No task") + icon = get_qt_icon({ + "type": "material-symbols", + "name": "indeterminate_check_box", + "color": get_default_entity_icon_color(), + }) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(NO_TASKS_ID, ITEM_ID_ROLE) + item.setEditable(False) + self._no_tasks_item = item + return self._no_tasks_item + + def _refresh(self, project_name, folder_ids): + self._is_refreshing = True + self._last_project_name = project_name + self._last_folder_ids = folder_ids + if not folder_ids: + self._add_invalid_selection_item() + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + return + + thread_id = hashlib.sha256( + "|".join(sorted(folder_ids)).encode() + ).hexdigest() + thread = self._refresh_threads.get(thread_id) + if thread is not None: + self._current_refresh_thread = thread + return + thread = RefreshThread( + thread_id, + self._thread_getter, + project_name, + folder_ids + ) + self._current_refresh_thread = thread + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _thread_getter(self, project_name, folder_ids): + task_items = self._controller.get_task_items( + project_name, folder_ids, sender=TASKS_MODEL_SENDER_NAME + ) + task_type_items = {} + if hasattr(self._controller, "get_task_type_items"): + task_type_items = self._controller.get_task_type_items( + project_name, sender=TASKS_MODEL_SENDER_NAME + ) + folder_ids = { + task_item.parent_id + for task_item in task_items + } + folder_labels_by_id = self._controller.get_folder_labels( + project_name, folder_ids + ) + return task_items, task_type_items, folder_labels_by_id + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_data_from_thread(thread) + + root_item = self.invisibleRootItem() + self._has_content = root_item.rowCount() > 0 + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + + def _clear_items(self): + self._items_by_id = {} + self._groups_by_name = {} + super()._clear_items() + + def _fill_data_from_thread(self, thread): + task_items, task_type_items, folder_labels_by_id = thread.get_result() + # Task items are refreshed + if task_items is None: + return + + # No tasks are available on folder + if not task_items: + self._add_empty_task_item() + return + self._remove_invalid_items() + + task_type_item_by_name = { + task_type_item.name: task_type_item + for task_type_item in task_type_items + } + task_type_icon_cache = {} + current_ids = set() + items_by_name = collections.defaultdict(list) + for task_item in task_items: + task_id = task_item.task_id + current_ids.add(task_id) + item = self._items_by_id.get(task_id) + if item is None: + item = QtGui.QStandardItem() + item.setColumnCount(self.columnCount()) + item.setEditable(False) + self._items_by_id[task_id] = item + + icon = self._get_task_item_icon( + task_item, + task_type_item_by_name, + task_type_icon_cache + ) + name = task_item.name + folder_id = task_item.parent_id + folder_label = folder_labels_by_id.get(folder_id) + + item.setData(name, QtCore.Qt.DisplayRole) + item.setData(name, ITEM_NAME_ROLE) + item.setData(task_item.id, ITEM_ID_ROLE) + item.setData(task_item.task_type, TASK_TYPE_ROLE) + item.setData(folder_id, PARENT_ID_ROLE) + item.setData(folder_label, FOLDER_LABEL_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + + items_by_name[name].append(item) + + root_item = self.invisibleRootItem() + + # Make sure item is not parented + # - this is laziness to avoid re-parenting items which does + # complicate the code with no benefit + queue = collections.deque() + queue.append((None, root_item)) + while queue: + (parent, item) = queue.popleft() + if not item.hasChildren(): + if parent: + parent.takeRow(item.row()) + continue + + for row in range(item.rowCount()): + queue.append((item, item.child(row, 0))) + + queue.append((parent, item)) + + used_group_names = set() + new_root_items = [ + self._get_no_tasks_item() + ] + for name, items in items_by_name.items(): + if len(items) == 1: + new_root_items.extend(items) + continue + + used_group_names.add(name) + group_item = self._groups_by_name.get(name) + if group_item is None: + group_item = QtGui.QStandardItem() + group_item.setData(name, QtCore.Qt.DisplayRole) + group_item.setEditable(False) + group_item.setColumnCount(self.columnCount()) + self._groups_by_name[name] = group_item + + # Use icon from first item + first_item_icon = items[0].data(QtCore.Qt.DecorationRole) + task_ids = [ + item.data(ITEM_ID_ROLE) + for item in items + ] + + group_item.setData(first_item_icon, QtCore.Qt.DecorationRole) + group_item.setData("|".join(task_ids), ITEM_ID_ROLE) + + group_item.appendRows(items) + + new_root_items.append(group_item) + + # Remove unused caches + for task_id in set(self._items_by_id) - current_ids: + self._items_by_id.pop(task_id) + + for name in set(self._groups_by_name) - used_group_names: + self._groups_by_name.pop(name) + + if new_root_items: + root_item.appendRows(new_root_items) + + def data(self, index, role=None): + if not index.isValid(): + return None + + if role is None: + role = QtCore.Qt.DisplayRole + + col = index.column() + if col != 0: + index = self.index(index.row(), 0, index.parent()) + + if col == 1: + if role == QtCore.Qt.DisplayRole: + role = TASK_TYPE_ROLE + else: + return None + + if col == 2: + if role == QtCore.Qt.DisplayRole: + role = FOLDER_LABEL_ROLE + else: + return None + + return super().data(index, role) + + +class LoaderTasksProxyModel(RecursiveSortFilterProxyModel): + def lessThan(self, left, right): + if left.data(ITEM_ID_ROLE) == NO_TASKS_ID: + return False + if right.data(ITEM_ID_ROLE) == NO_TASKS_ID: + return True + return super().lessThan(left, right) + + +class LoaderTasksWidget(QtWidgets.QWidget): + refreshed = QtCore.Signal() + + def __init__(self, controller, parent): + super().__init__(parent) + + tasks_view = DeselectableTreeView(self) + tasks_view.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + + tasks_model = LoaderTasksQtModel(controller) + tasks_proxy_model = LoaderTasksProxyModel() + tasks_proxy_model.setSourceModel(tasks_model) + tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + tasks_view.setModel(tasks_proxy_model) + # Hide folder column by default + tasks_view.setColumnHidden(2, True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(tasks_view, 1) + + controller.register_event_callback( + "selection.folders.changed", + self._on_folders_selection_changed, + ) + controller.register_event_callback( + "tasks.refresh.finished", + self._on_tasks_refresh_finished + ) + + selection_model = tasks_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + tasks_model.refreshed.connect(self._on_model_refresh) + + self._controller = controller + self._tasks_view = tasks_view + self._tasks_model = tasks_model + self._tasks_proxy_model = tasks_proxy_model + + self._fisrt_show = True + + def showEvent(self, event): + super().showEvent(event) + if self._fisrt_show: + self._fisrt_show = False + header_widget = self._tasks_view.header() + header_widget.resizeSection(0, 200) + + def set_name_filter(self, name): + """Set filter of folder name. + + Args: + name (str): The string filter. + + """ + self._tasks_proxy_model.setFilterFixedString(name) + if name: + self._tasks_view.expandAll() + + def refresh(self): + self._tasks_model.refresh() + + def _clear(self): + self._tasks_model.clear() + + def _on_tasks_refresh_finished(self, event): + if event["sender"] != TASKS_MODEL_SENDER_NAME: + self._set_project_name(event["project_name"]) + + def _on_folders_selection_changed(self, event): + project_name = event["project_name"] + folder_ids = event["folder_ids"] + self._tasks_view.setColumnHidden(2, len(folder_ids) == 1) + self._tasks_model.set_context(project_name, folder_ids) + + def _on_model_refresh(self): + self._tasks_proxy_model.sort(0) + self.refreshed.emit() + + def _get_selected_item_ids(self): + selection_model = self._tasks_view.selectionModel() + item_ids = set() + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is None: + continue + if item_id == NO_TASKS_ID: + item_ids.add(None) + else: + item_ids |= set(item_id.split("|")) + return item_ids + + def _on_selection_change(self): + item_ids = self._get_selected_item_ids() + self._controller.set_selected_tasks(item_ids) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index 31c9908b23..b846484c39 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -14,8 +14,9 @@ from ayon_core.tools.utils import ProjectsCombobox from ayon_core.tools.loader.control import LoaderController from .folders_widget import LoaderFoldersWidget +from .tasks_widget import LoaderTasksWidget from .products_widget import ProductsWidget -from .product_types_widget import ProductTypesView +from .product_types_combo import ProductTypesCombobox from .product_group_dialog import ProductGroupDialog from .info_widget import InfoWidget from .repres_widget import RepresentationsWidget @@ -164,16 +165,16 @@ class LoaderWindow(QtWidgets.QWidget): folders_widget = LoaderFoldersWidget(controller, context_widget) - product_types_widget = ProductTypesView(controller, context_splitter) - context_layout = QtWidgets.QVBoxLayout(context_widget) context_layout.setContentsMargins(0, 0, 0, 0) context_layout.addWidget(context_top_widget, 0) context_layout.addWidget(folders_filter_input, 0) context_layout.addWidget(folders_widget, 1) + tasks_widget = LoaderTasksWidget(controller, context_widget) + context_splitter.addWidget(context_widget) - context_splitter.addWidget(product_types_widget) + context_splitter.addWidget(tasks_widget) context_splitter.setStretchFactor(0, 65) context_splitter.setStretchFactor(1, 35) @@ -185,6 +186,10 @@ class LoaderWindow(QtWidgets.QWidget): products_filter_input = PlaceholderLineEdit(products_inputs_widget) products_filter_input.setPlaceholderText("Product name filter...") + product_types_filter_combo = ProductTypesCombobox( + controller, products_inputs_widget + ) + product_status_filter_combo = StatusesCombobox(controller, self) product_group_checkbox = QtWidgets.QCheckBox( @@ -196,6 +201,7 @@ class LoaderWindow(QtWidgets.QWidget): products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget) products_inputs_layout.setContentsMargins(0, 0, 0, 0) products_inputs_layout.addWidget(products_filter_input, 1) + products_inputs_layout.addWidget(product_types_filter_combo, 1) products_inputs_layout.addWidget(product_status_filter_combo, 1) products_inputs_layout.addWidget(product_group_checkbox, 0) @@ -244,12 +250,12 @@ class LoaderWindow(QtWidgets.QWidget): folders_filter_input.textChanged.connect( self._on_folder_filter_change ) - product_types_widget.filter_changed.connect( - self._on_product_type_filter_change - ) products_filter_input.textChanged.connect( self._on_product_filter_change ) + product_types_filter_combo.value_changed.connect( + self._on_product_type_filter_change + ) product_status_filter_combo.value_changed.connect( self._on_status_filter_change ) @@ -280,6 +286,10 @@ class LoaderWindow(QtWidgets.QWidget): "selection.folders.changed", self._on_folders_selection_changed, ) + controller.register_event_callback( + "selection.tasks.changed", + self._on_tasks_selection_change, + ) controller.register_event_callback( "selection.versions.changed", self._on_versions_selection_changed, @@ -304,9 +314,10 @@ class LoaderWindow(QtWidgets.QWidget): self._folders_filter_input = folders_filter_input self._folders_widget = folders_widget - self._product_types_widget = product_types_widget + self._tasks_widget = tasks_widget self._products_filter_input = products_filter_input + self._product_types_filter_combo = product_types_filter_combo self._product_status_filter_combo = product_status_filter_combo self._product_group_checkbox = product_group_checkbox self._products_widget = products_widget @@ -335,7 +346,7 @@ class LoaderWindow(QtWidgets.QWidget): self._controller.reset() def showEvent(self, event): - super(LoaderWindow, self).showEvent(event) + super().showEvent(event) if self._first_show: self._on_first_show() @@ -343,9 +354,13 @@ class LoaderWindow(QtWidgets.QWidget): self._show_timer.start() def closeEvent(self, event): - super(LoaderWindow, self).closeEvent(event) + super().closeEvent(event) - self._product_types_widget.reset_product_types_filter_on_refresh() + ( + self + ._product_types_filter_combo + .reset_product_types_filter_on_refresh() + ) self._reset_on_show = True @@ -363,7 +378,7 @@ class LoaderWindow(QtWidgets.QWidget): event.setAccepted(True) return - super(LoaderWindow, self).keyPressEvent(event) + super().keyPressEvent(event) def _on_first_show(self): self._first_show = False @@ -423,14 +438,16 @@ class LoaderWindow(QtWidgets.QWidget): def _on_product_filter_change(self, text): self._products_widget.set_name_filter(text) + def _on_tasks_selection_change(self, event): + self._products_widget.set_tasks_filter(event["task_ids"]) + def _on_status_filter_change(self): status_names = self._product_status_filter_combo.get_value() self._products_widget.set_statuses_filter(status_names) def _on_product_type_filter_change(self): - self._products_widget.set_product_type_filter( - self._product_types_widget.get_filter_info() - ) + product_types = self._product_types_filter_combo.get_value() + self._products_widget.set_product_type_filter(product_types) def _on_merged_products_selection_change(self): items = self._products_widget.get_selected_merged_products() diff --git a/client/ayon_core/tools/pyblish_pype/model.py b/client/ayon_core/tools/pyblish_pype/model.py index 3a402f386e..44f951fe14 100644 --- a/client/ayon_core/tools/pyblish_pype/model.py +++ b/client/ayon_core/tools/pyblish_pype/model.py @@ -31,7 +31,6 @@ from . import settings, util from .awesome import tags as awesome from qtpy import QtCore, QtGui import qtawesome -from six import text_type from .constants import PluginStates, InstanceStates, GroupStates, Roles @@ -985,7 +984,7 @@ class TerminalModel(QtGui.QStandardItemModel): record_item = record else: record_item = { - "label": text_type(record.msg), + "label": str(record.msg), "type": "record", "levelno": record.levelno, "threadName": record.threadName, @@ -993,7 +992,7 @@ class TerminalModel(QtGui.QStandardItemModel): "filename": record.filename, "pathname": record.pathname, "lineno": record.lineno, - "msg": text_type(record.msg), + "msg": str(record.msg), "msecs": record.msecs, "levelname": record.levelname } diff --git a/client/ayon_core/tools/pyblish_pype/util.py b/client/ayon_core/tools/pyblish_pype/util.py index d24b07a409..081f7775d5 100644 --- a/client/ayon_core/tools/pyblish_pype/util.py +++ b/client/ayon_core/tools/pyblish_pype/util.py @@ -10,7 +10,6 @@ import sys import collections from qtpy import QtCore -from six import text_type import pyblish.api root = os.path.dirname(__file__) @@ -64,7 +63,7 @@ def u_print(msg, **kwargs): **kwargs: Keyword argument for `print` function. """ - if isinstance(msg, text_type): + if isinstance(msg, str): encoding = None try: encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding) diff --git a/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py index c25739aff8..ce95f9e74f 100644 --- a/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py +++ b/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py @@ -5,7 +5,6 @@ from __future__ import print_function import json import os -import six from qtpy import QtCore, QtGui @@ -152,7 +151,7 @@ class IconicFont(QtCore.QObject): def hook(obj): result = {} for key in obj: - result[key] = six.unichr(int(obj[key], 16)) + result[key] = chr(int(obj[key], 16)) return result if directory is None: diff --git a/client/ayon_core/tools/utils/host_tools.py b/client/ayon_core/tools/utils/host_tools.py index 1eff746b9e..3d356555f3 100644 --- a/client/ayon_core/tools/utils/host_tools.py +++ b/client/ayon_core/tools/utils/host_tools.py @@ -7,7 +7,7 @@ import os import pyblish.api -from ayon_core.host import ILoadHost +from ayon_core.host import ILoadHost, IPublishHost from ayon_core.lib import Logger from ayon_core.pipeline import registered_host @@ -236,7 +236,7 @@ class HostToolsHelper: from ayon_core.tools.publisher.window import PublisherWindow host = registered_host() - ILoadHost.validate_load_methods(host) + IPublishHost.validate_publish_methods(host) publisher_window = PublisherWindow( controller=controller, diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index 87a4c3db3b..30846e6cda 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -24,9 +24,14 @@ class TasksQtModel(QtGui.QStandardItemModel): """ _default_task_icon = None refreshed = QtCore.Signal() + column_labels = ["Tasks"] def __init__(self, controller): - super(TasksQtModel, self).__init__() + super().__init__() + + self.setColumnCount(len(self.column_labels)) + for idx, label in enumerate(self.column_labels): + self.setHeaderData(idx, QtCore.Qt.Horizontal, label) self._controller = controller @@ -53,7 +58,8 @@ class TasksQtModel(QtGui.QStandardItemModel): self._has_content = False self._remove_invalid_items() root_item = self.invisibleRootItem() - root_item.removeRows(0, root_item.rowCount()) + while root_item.rowCount() != 0: + root_item.takeRow(0) def refresh(self): """Refresh tasks for last project and folder.""" @@ -336,19 +342,6 @@ class TasksQtModel(QtGui.QStandardItemModel): return self._has_content - def headerData(self, section, orientation, role): - # Show nice labels in the header - if ( - role == QtCore.Qt.DisplayRole - and orientation == QtCore.Qt.Horizontal - ): - if section == 0: - return "Tasks" - - return super(TasksQtModel, self).headerData( - section, orientation, role - ) - class TasksWidget(QtWidgets.QWidget): """Tasks widget. @@ -365,7 +358,7 @@ class TasksWidget(QtWidgets.QWidget): selection_changed = QtCore.Signal() def __init__(self, controller, parent, handle_expected_selection=False): - super(TasksWidget, self).__init__(parent) + super().__init__(parent) tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) diff --git a/client/ayon_core/tools/utils/views.py b/client/ayon_core/tools/utils/views.py index d8ae94bf0c..d69be9b6a9 100644 --- a/client/ayon_core/tools/utils/views.py +++ b/client/ayon_core/tools/utils/views.py @@ -7,7 +7,6 @@ class DeselectableTreeView(QtWidgets.QTreeView): """A tree view that deselects on clicking on an empty area in the view""" def mousePressEvent(self, event): - index = self.indexAt(event.pos()) if not index.isValid(): # clear the selection @@ -15,7 +14,14 @@ class DeselectableTreeView(QtWidgets.QTreeView): # clear the current index self.setCurrentIndex(QtCore.QModelIndex()) - QtWidgets.QTreeView.mousePressEvent(self, event) + elif ( + self.selectionModel().isSelected(index) + and len(self.selectionModel().selectedRows()) == 1 + and event.modifiers() == QtCore.Qt.NoModifier + ): + event.setModifiers(QtCore.Qt.ControlModifier) + + super().mousePressEvent(event) class TreeView(QtWidgets.QTreeView): diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 909ecd7a3c..e533e08fe4 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.1.0+dev" +__version__ = "1.1.3+dev" diff --git a/client/pyproject.toml b/client/pyproject.toml index d61baee2c4..edf7f57317 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -10,7 +10,6 @@ pyblish-base = "^1.8.11" speedcopy = "^2.1" six = "^1.15" qtawesome = "0.7.3" -pydantic = "^2.9.2" [ayon.runtimeDependencies] aiohttp-middlewares = "^2.0.0" diff --git a/package.py b/package.py index 0b888f5c33..02e2f25384 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.0+dev" +version = "1.1.3+dev" client_dir = "ayon_core" diff --git a/poetry.lock b/poetry.lock index 44f41c73ea..7286ee59a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "appdirs" @@ -24,13 +13,13 @@ files = [ [[package]] name = "attrs" -version = "24.3.0" +version = "25.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] @@ -317,6 +306,22 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "mock" +version = "5.2.0" +description = "Rolling backport of unittest.mock for all Pythons" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, + {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, +] + +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + [[package]] name = "mypy" version = "1.14.0" @@ -392,6 +397,55 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "opentimelineio" +version = "0.17.0" +description = "Editorial interchange format and API" +optional = false +python-versions = "!=3.9.0,>=3.7" +files = [ + {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:2dd31a570cabfd6227c1b1dd0cc038da10787492c26c55de058326e21fe8a313"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a1da5d4803d1ba5e846b181a9e0f4a392c76b9acc5e08947772bc086f2ebfc0"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3527977aec8202789a42d60e1e0dc11b4154f585ef72921760445f43e7967a00"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3aafb4c50455832ed2627c2cac654b896473a5c1f8348ddc07c10be5cfbd59"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-win32.whl", hash = "sha256:fee45af9f6330773893cd0858e92f8256bb5bde4229b44a76f03e59a9fb1b1b6"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:d51887619689c21d67cc4b11b1088f99ae44094513315e7a144be00f1393bfa8"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:cbf05c3e8c0187969f79e91f7495d1f0dc3609557874d8e601ba2e072c70ddb1"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d3430c3f4e88c5365d7b6afbee920b0815b62ecf141abe44cd739c9eedc04284"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1912345227b0bd1654c7153863eadbcee60362aa46340678e576e5d2aa3106a"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51e06eb11a868d970c1534e39faf916228d5163bf3598076d408d8f393ab0bd4"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-win32.whl", hash = "sha256:5c3a3f4780b25a8c1a80d788becba691d12b629069ad8783d0db21027639276f"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c8726b33af30ba42928972192311ea0f986edbbd5f74651bada182d4fe805c"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:9a9af4105a088c0ab131780e49db268db7e37871aac33db842de6b2b16f14e39"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e653ad1dd3b85f5c312a742dc24b61b330964aa391dc5bc072fe8b9c85adff1"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a77823c27a1b93c6b87682372c3734ac5fddc10bfe53875e657d43c60fb885"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4f4efcf3ddd81b62c4feb49a0bcc309b50ffeb6a8c48ab173d169a029006f4d"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-win32.whl", hash = "sha256:9872ab74a20bb2bb3a50af04e80fe9238998d67d6be4e30e45aebe25d3eefac6"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:c83b78be3312d3152d7e07ab32b0086fe220acc2a5b035b70ad69a787c0ece62"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0e671a6f2a1f772445bb326c7640dc977cfc3db589fe108a783a0311939cfac8"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b931a3189b4ce064f06f15a89fe08ef4de01f7dcf0abc441fe2e02ef2a3311bb"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923cb54d806c981cf1e91916c3e57fba5664c22f37763dd012bad5a5a7bd4db4"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win32.whl", hash = "sha256:8e16598c5084dcb21df3d83978b0e5f72300af9edd4cdcb85e3b0ba5da0df4e8"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7eed5033494888fb3f802af50e60559e279b2f398802748872903c2f54efd2c9"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:118baa22b9227da5003bee653601a68686ae2823682dcd7d13c88178c63081c3"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:43389eacdee2169de454e1c79ecfea82f54a9e73b67151427a9b621349a22b7f"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17659b1e6aa42ed617a942f7a2bfc6ecc375d0464ec127ce9edf896278ecaee9"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d5ea8cfbebf3c9013cc680eef5be48bffb515aafa9dc31e99bf66052a4ca3d"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-win32.whl", hash = "sha256:cc67c74eb4b73bc0f7d135d3ff3dbbd86b2d451a9b142690a8d1631ad79c46f2"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:69b39079bee6fa4aff34c6ad6544df394bc7388483fa5ce958ecd16e243a53ad"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a33554894dea17c22feec0201991e705c2c90a679ba2a012a0c558a7130df711"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b1ad3b3155370245b851b2f7b60006b2ebbb5bb76dd0fdc49bb4dce73fa7d96"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:030454a9c0e9e82e5a153119f9afb8f3f4e64a3b27f80ac0dcde44b029fd3f3f"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce64376a28919533bd4f744ff8885118abefa73f78fd408f95fa7a9489855b6"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-win32.whl", hash = "sha256:fa8cdceb25f9003c3c0b5b32baef2c764949d88b867161ddc6f44f48f6bbfa4a"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fbcf8a000cd688633c8dc5d22e91912013c67c674329eba603358e3b54da32bf"}, + {file = "opentimelineio-0.17.0.tar.gz", hash = "sha256:10ef324e710457e9977387cd9ef91eb24a9837bfb370aec3330f9c0f146cea85"}, +] + +[package.extras] +dev = ["check-manifest", "coverage (>=4.5)", "flake8 (>=3.5)", "urllib3 (>=1.24.3)"] +view = ["PySide2 (>=5.11,<6.0)", "PySide6 (>=6.2,<7.0)"] + [[package]] name = "packaging" version = "24.2" @@ -463,138 +517,6 @@ files = [ {file = "pyblish_base-1.8.12-py2.py3-none-any.whl", hash = "sha256:2cbe956bfbd4175a2d7d22b344cd345800f4d4437153434ab658fc12646a11e8"}, ] -[[package]] -name = "pydantic" -version = "2.10.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - [[package]] name = "pytest" version = "8.3.4" @@ -719,29 +641,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.9.4" +version = "0.9.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, - {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, - {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, - {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, - {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, - {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, - {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, - {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, - {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, - {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, - {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, - {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, + {file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"}, + {file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"}, + {file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"}, + {file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"}, + {file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"}, + {file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"}, + {file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"}, ] [[package]] @@ -858,4 +780,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "1b63a1723ed91520787257791fe7d9b55557e5e12b5c41661a83f1911bc42675" +content-hash = "8e5b1f886eb608198752e1ce84f6bc89d1c8d53a5eeabff84a4272538f81403f" diff --git a/pyproject.toml b/pyproject.toml index 3e668ca3e8..591c13bb2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,23 +5,16 @@ [tool.poetry] name = "ayon-core" -version = "1.1.0+dev" +version = "1.1.3+dev" description = "" authors = ["Ynput Team "] readme = "README.md" +package-mode = false [tool.poetry.dependencies] python = ">=3.9.1,<3.10" -pydantic = "^2.9.2" -pre-commit = "^4.0.0" -clique = "^2" -pyblish-base = "^1.8" -attrs = "^24.2.0" -pydantic-core = "==2.29.0" -speedcopy = "^2.0.0" - -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] # test dependencies pytest = "^8.0" pytest-print = "^1.0" @@ -31,7 +24,12 @@ ruff = "^0.9.3" pre-commit = "^4" codespell = "^2.2.6" semver = "^3.0.2" -mypy = "==1.15.0" +mypy = "^1.14.0" +mock = "^5.0.0" +attrs = "^25.0.0" +pyblish-base = "^1.8.7" +clique = "^2.0.0" +opentimelineio = "^0.17.0" [tool.ruff] diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 18e7d67f90..d10a6b7507 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -68,6 +68,59 @@ class ContributionLayersModel(BaseSettingsModel): "layer on top.") +class CollectUSDLayerContributionsProfileModel(BaseSettingsModel): + """Profiles to define instance attribute defaults for USD contribution.""" + _layout = "expanded" + product_types: list[str] = SettingsField( + default_factory=list, + title="Product types", + description=( + "The product types to match this profile to. When matched, the" + " settings below would apply to the instance as default" + " attributes." + ), + section="Filter" + ) + task_types: list[str] = SettingsField( + default_factory=list, + title="Task Types", + enum_resolver=task_types_enum, + description=( + "The current create context task type to filter against. This" + " allows to filter the profile to only be valid if currently " + " creating from within that task type." + ), + ) + contribution_layer: str = SettingsField( + "", + title="Contribution Department Layer", + description=( + "The default contribution layer to apply the contribution to when" + " matching this profile. The layer name should be in the" + " 'Department Layer Orders' list to get a sensible order." + ), + section="Instance attribute defaults", + ) + contribution_apply_as_variant: bool = SettingsField( + True, + title="Apply as variant", + description=( + "The default 'Apply as variant' state for instances matching this" + " profile. Usually enabled for asset contributions and disabled" + " for shot contributions." + ), + ) + contribution_target_product: str = SettingsField( + "usdAsset", + title="Target Product", + description=( + "The default destination product name to apply the contribution to" + " when matching this profile." + " Usually e.g. 'usdAsset' or 'usdShot'." + ), + ) + + class CollectUSDLayerContributionsModel(BaseSettingsModel): enabled: bool = SettingsField(True, title="Enabled") contribution_layers: list[ContributionLayersModel] = SettingsField( @@ -77,6 +130,14 @@ class CollectUSDLayerContributionsModel(BaseSettingsModel): "ordering inside the USD contribution workflow." ) ) + profiles: list[CollectUSDLayerContributionsProfileModel] = SettingsField( + default_factory=list, + title="Profiles", + description=( + "Define attribute defaults for USD Contributions on publish" + " instances." + ) + ) @validator("contribution_layers") def validate_unique_outputs(cls, value): @@ -1017,6 +1078,43 @@ DEFAULT_PUBLISH_VALUES = { {"name": "fx", "order": 500}, {"name": "lighting", "order": 600}, ], + "profiles": [ + { + "product_types": ["model"], + "task_types": [], + "contribution_layer": "model", + "contribution_apply_as_variant": True, + "contribution_target_product": "usdAsset" + }, + { + "product_types": ["look"], + "task_types": [], + "contribution_layer": "look", + "contribution_apply_as_variant": True, + "contribution_target_product": "usdAsset" + }, + { + "product_types": ["groom"], + "task_types": [], + "contribution_layer": "groom", + "contribution_apply_as_variant": True, + "contribution_target_product": "usdAsset" + }, + { + "product_types": ["rig"], + "task_types": [], + "contribution_layer": "rig", + "contribution_apply_as_variant": True, + "contribution_target_product": "usdAsset" + }, + { + "product_types": ["usd"], + "task_types": [], + "contribution_layer": "assembly", + "contribution_apply_as_variant": False, + "contribution_target_product": "usdShot" + }, + ] }, "ValidateEditorialAssetName": { "enabled": True, diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py new file mode 100644 index 0000000000..e7aea7fd7d --- /dev/null +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -0,0 +1,135 @@ +import unittest +from unittest.mock import patch + +from ayon_core.lib.env_tools import ( + CycleError, + DynamicKeyClashError, + parse_env_variables_structure, + compute_env_variables_structure, +) + +# --- Test data --- +COMPUTE_SRC_ENV = { + "COMPUTE_VERSION": "1.0.0", + # Will be available only for darwin + "COMPUTE_ONE_PLATFORM": { + "darwin": "Compute macOs", + }, + "COMPUTE_LOCATION": { + "darwin": "/compute-app-{COMPUTE_VERSION}", + "linux": "/usr/compute-app-{COMPUTE_VERSION}", + "windows": "C:/Program Files/compute-app-{COMPUTE_VERSION}" + }, + "PATH_LIST": { + "darwin": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + "linux": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + "windows": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + }, + "PATH_STR": { + "darwin": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "linux": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "windows": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", + }, +} + +# --- RESULTS --- +# --- Parse results --- +PARSE_RESULT_WINDOWS = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "C:/Program Files/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", +} + +PARSE_RESULT_LINUX = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "/usr/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", +} + +PARSE_RESULT_DARWIN = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_ONE_PLATFORM": "Compute macOs", + "COMPUTE_LOCATION": "/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", +} + +# --- Compute results --- +COMPUTE_RESULT_WINDOWS = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0", + "PATH_LIST": ( + "C:/Program Files/compute-app-1.0.0/bin" + ";C:/Program Files/compute-app-1.0.0/bin2" + ), + "PATH_STR": ( + "C:/Program Files/compute-app-1.0.0/bin" + ";C:/Program Files/compute-app-1.0.0/bin2" + ) +} + +COMPUTE_RESULT_LINUX = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "/usr/compute-app-1.0.0", + "PATH_LIST": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2", + "PATH_STR": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2" +} + +COMPUTE_RESULT_DARWIN = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_ONE_PLATFORM": "Compute macOs", + "COMPUTE_LOCATION": "/compute-app-1.0.0", + "PATH_LIST": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2", + "PATH_STR": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2" +} + + +class EnvParseCompute(unittest.TestCase): + def test_parse_env(self): + with patch("platform.system", return_value="windows"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_WINDOWS + + with patch("platform.system", return_value="linux"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_LINUX + + with patch("platform.system", return_value="darwin"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_DARWIN + + def test_compute_env(self): + with patch("platform.system", return_value="windows"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_WINDOWS + + with patch("platform.system", return_value="linux"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_LINUX + + with patch("platform.system", return_value="darwin"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_DARWIN + + def test_cycle_error(self): + with self.assertRaises(CycleError): + compute_env_variables_structure({ + "KEY_1": "{KEY_2}", + "KEY_2": "{KEY_1}", + }) + + def test_dynamic_key_error(self): + with self.assertRaises(DynamicKeyClashError): + compute_env_variables_structure({ + "KEY_A": "Occupied", + "SUBKEY": "A", + "KEY_{SUBKEY}": "Resolves as occupied key", + }) diff --git a/tests/client/ayon_core/pipeline/editorial/resources/timeline/timeline.json b/tests/client/ayon_core/pipeline/editorial/resources/timeline/timeline.json new file mode 100644 index 0000000000..03ed87569b --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/timeline/timeline.json @@ -0,0 +1,2054 @@ +{ + "OTIO_SCHEMA": "Timeline.1", + "metadata": { + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "dt3", + "foundry.timeline.samplerate": "48000", + "openpype.project.lutSetting16Bit": "ACES - ACEScc", + "openpype.project.lutSetting8Bit": "Output - Rec.709", + "openpype.project.lutSettingFloat": "ACES - ACES2065-1", + "openpype.project.lutSettingLog": "ACES - ACEScc", + "openpype.project.lutSettingViewer": "ACES/Rec.709", + "openpype.project.lutSettingWorkingSpace": "ACES - ACEScg", + "openpype.project.lutUseOCIOForExport": true, + "openpype.project.ocioConfigName": "", + "openpype.project.ocioConfigPath": "C:/Program Files/Nuke12.2v3/plugins/OCIOConfigs/configs/aces_1.1/config.ocio", + "openpype.project.useOCIOEnvironmentOverride": true, + "openpype.timeline.height": 1080, + "openpype.timeline.pixelAspect": 1, + "openpype.timeline.width": 1920 + }, + "name": "sq001", + "global_start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86400.0 + }, + "tracks": { + "OTIO_SCHEMA": "Stack.1", + "metadata": {}, + "name": "tracks", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "reference", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "referenceclip_mediash010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 24.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86400.08874841638 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "referencesq01sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 52.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86424.08877306872 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "referencesq01sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86476.08882648213 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "referencesq01sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 65.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86527.08887886834 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/sq01/referencesq01sh010\", \"task\": null, \"clip_index\": \"8185DC63-DE17-F143-817B-B34C00CECDDF\", \"hierarchy\": \"shots/sq01\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"reference\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"reference\"}, \"heroTrack\": true, \"uuid\": \"5fa79821-2a65-4f3e-aec5-05471b0f145e\", \"reviewTrack\": null, \"folderName\": \"referencesq01sh010\", \"label\": \"/shots/sq01/referencesq01sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"813286be-1492-47e2-aa4e-a192fdc4294e\", \"creator_attributes\": {\"fps\": \"from_selection\", \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1066, \"clipIn\": 127, \"clipOut\": 191, \"clipDuration\": 65, \"sourceIn\": 127.0, \"sourceOut\": 191.0}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateReference\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"reference\", \"folderPath\": \"/shots/sq01/referencesq01sh010\", \"task\": null, \"clip_index\": \"8185DC63-DE17-F143-817B-B34C00CECDDF\", \"hierarchy\": \"shots/sq01\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"reference\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"sq01\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"sq01\", \"track\": \"reference\"}, \"heroTrack\": true, \"uuid\": \"5fa79821-2a65-4f3e-aec5-05471b0f145e\", \"reviewTrack\": null, \"folderName\": \"referencesq01sh010\", \"parent_instance_id\": \"813286be-1492-47e2-aa4e-a192fdc4294e\", \"label\": \"/shots/sq01/referencesq01sh010 plateReference\", \"newHierarchyIntegration\": true, \"instance_id\": \"7a9ef903-ec0c-4c0c-9b84-5d5a8cf8e72c\", \"creator_attributes\": {\"parentInstance\": \"/shots/sq01/referencesq01sh010 shotMain\", \"review\": false, \"reviewableSource\": \"clip_media\", \"publish_effects\": true}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"8185DC63-DE17-F143-817B-B34C00CECDDF\"}", + "label": "AYONdata_1fa7d197", + "note": "AYON data container" + }, + "name": "AYONdata_1fa7d197", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sq001", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 24.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86592.08894563509 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" + }, + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "P01", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Gap.1", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 24.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 52.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86535.08888708579 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "family": "task", + "hiero_source_type": "TrackItem", + "label": "comp", + "note": "Compositing", + "type": "Compositing" + }, + "name": "comp", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "60", + "foundry.source.filename": "MER_sq001_sh010_P01.%04d.exr 997-1056", + "foundry.source.filesize": "", + "foundry.source.fragments": "60", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh010_P01/MER_sq001_sh010_P01.%04d.exr 997-1056", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh010_P01.%04d.exr 997-1056", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "997", + "foundry.source.timecode": "86531", + "foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "60", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "8", + "media.exr.compressionName": "DWAA", + "media.exr.dataWindow": "0,0,1919,1079", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.dwaCompressionLevel": "90", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:03", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh010_P01/MER_sq001_sh010_P01.0997.exr", + "media.input.filereader": "exr", + "media.input.filesize": "1217052", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "1080", + "media.input.mtime": "2022-03-06 10:14:37", + "media.input.timecode": "01:00:05:11", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "ffffffffffffffff", + "media.nuke.version": "12.2v3", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 60.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86531.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh010_P01\\", + "name_prefix": "MER_sq001_sh010_P01.", + "name_suffix": ".exr", + "start_frame": 997, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 172800.17749683277 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "family": "task", + "hiero_source_type": "TrackItem", + "label": "comp", + "note": "Compositing", + "type": "Compositing" + }, + "name": "comp", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + }, + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"ayon.create.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/test_align/sh010\", \"task\": null, \"clip_index\": \"FD37E3CA-F66B-1749-80CD-212210AB1C28\", \"hierarchy\": \"shots/test_align\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\", \"shot\": \"sh010\", \"reviewableSource\": \"reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"test_align\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\"}, \"heroTrack\": true, \"uuid\": \"e2bcf862-b8b9-4c2c-806f-5ba6e227f782\", \"reviewTrack\": \"reference\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/test_align/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"888195e5-f432-4032-9ef5-3e3b7897f80d\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1052, \"clipIn\": 76, \"clipOut\": 126, \"clipDuration\": 51, \"sourceIn\": 0.0, \"sourceOut\": 50.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"ayon.create.instance\", \"productType\": \"plate\", \"productName\": \"plateP01\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"P01\", \"folderPath\": \"/shots/test_align/sh010\", \"task\": null, \"clip_index\": \"FD37E3CA-F66B-1749-80CD-212210AB1C28\", \"hierarchy\": \"shots/test_align\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\", \"shot\": \"sh010\", \"reviewableSource\": \"reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"test_align\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\"}, \"heroTrack\": true, \"uuid\": \"e2bcf862-b8b9-4c2c-806f-5ba6e227f782\", \"reviewTrack\": \"reference\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"888195e5-f432-4032-9ef5-3e3b7897f80d\", \"label\": \"/shots/test_align/sh010 plateP01\", \"newHierarchyIntegration\": true, \"instance_id\": \"16ae41aa-20c4-4c63-97c6-7666e4d1d30b\", \"creator_attributes\": {\"parentInstance\": \"/shots/test_align/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"reference\", \"publish_effects\": true}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.audio\": {\"id\": \"ayon.create.instance\", \"productType\": \"audio\", \"productName\": \"audioMain\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.hiero.audio\", \"variant\": \"main\", \"folderPath\": \"/shots/test_align/sh010\", \"task\": null, \"clip_index\": \"FD37E3CA-F66B-1749-80CD-212210AB1C28\", \"hierarchy\": \"shots/test_align\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\", \"shot\": \"sh010\", \"reviewableSource\": \"reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"test_align\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\"}, \"heroTrack\": true, \"uuid\": \"e2bcf862-b8b9-4c2c-806f-5ba6e227f782\", \"reviewTrack\": \"reference\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"888195e5-f432-4032-9ef5-3e3b7897f80d\", \"label\": \"/shots/test_align/sh010 audioMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"4f31e750-f665-4ef7-8fab-578ebc606d7e\", \"creator_attributes\": {\"parentInstance\": \"/shots/test_align/sh010 shotMain\", \"review\": true}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"FD37E3CA-F66B-1749-80CD-212210AB1C28\"}", + "label": "AYONdata_86163a19", + "note": "AYON data container" + }, + "name": "AYONdata_86163a19", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "59", + "foundry.source.filename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.filesize": "", + "foundry.source.fragments": "59", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "997", + "foundry.source.timecode": "172800", + "foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "59", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "8", + "media.exr.compressionName": "DWAA", + "media.exr.dataWindow": "0,0,1919,1079", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.dwaCompressionLevel": "90", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:03", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.0997.exr", + "media.input.filereader": "exr", + "media.input.filesize": "1235182", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "1080", + "media.input.mtime": "2022-03-06 10:14:41", + "media.input.timecode": "02:00:00:00", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "ffffffffffffffff", + "media.nuke.version": "12.2v3", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 59.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 172800.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01\\", + "name_prefix": "MER_sq001_sh020_P01.", + "name_suffix": ".exr", + "start_frame": 997, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "P01default_twsh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 65.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 345623.3550172907 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 1.0, + "lookup": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "name": "TimeWarp1", + "effect_name": "TimeWarp" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "family": "task", + "hiero_source_type": "TrackItem", + "label": "comp", + "note": "Compositing", + "type": "Compositing" + }, + "name": "comp", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 1556, + "ayon.source.pixelAspect": 2.0, + "ayon.source.width": 1828, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "73", + "foundry.source.filename": "MER_sq001_sh040_P01.%04d.exr 997-1069", + "foundry.source.filesize": "", + "foundry.source.fragments": "73", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1556", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh040_P01/MER_sq001_sh040_P01.%04d.exr 997-1069", + "foundry.source.pixelAspect": "2", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh040_P01.%04d.exr 997-1069", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "997", + "foundry.source.timecode": "345619", + "foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1828", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "73", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "9", + "media.exr.compressionName": "DWAB", + "media.exr.dataWindow": "0,0,1827,1555", + "media.exr.displayWindow": "0,0,1827,1555", + "media.exr.dwaCompressionLevel": "80", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "2", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:05", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh040_P01/MER_sq001_sh040_P01.0997.exr", + "media.input.filereader": "exr", + "media.input.filesize": "1170604", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "1556", + "media.input.mtime": "2022-03-30 13:47:47", + "media.input.timecode": "04:00:00:19", + "media.input.width": "1828", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "2a4", + "media.nuke.version": "12.2v3", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 73.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 345619.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh040_P01\\", + "name_prefix": "MER_sq001_sh040_P01.", + "name_suffix": ".exr", + "start_frame": 997, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" + }, + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "P02", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Gap.1", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 76.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 1.0000010271807451 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.plate\": {\"id\": \"ayon.create.instance\", \"productType\": \"plate\", \"productName\": \"plateP02\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"P02\", \"folderPath\": \"/shots/test_align/sh010\", \"task\": null, \"clip_index\": \"881A3D65-A052-DC45-9D3B-304990BD6488\", \"hierarchy\": \"shots/test_align\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P02\", \"shot\": \"sh020\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"test_align\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\"}, \"heroTrack\": false, \"uuid\": \"b7416739-1102-4dfd-bac3-771d43018b84\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"parent_instance_id\": \"888195e5-f432-4032-9ef5-3e3b7897f80d\", \"label\": \"/shots/test_align/sh010 plateP02\", \"newHierarchyIntegration\": true, \"instance_id\": \"6cc84e25-e2fa-4f31-9901-85d75e8fd36a\", \"creator_attributes\": {\"parentInstance\": \"/shots/test_align/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"clip_media\", \"publish_effects\": true}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"881A3D65-A052-DC45-9D3B-304990BD6488\"}", + "label": "AYONdata_05836436", + "note": "AYON data container" + }, + "name": "AYONdata_05836436", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACEScg", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 2048, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACEScg", + "foundry.source.duration": "200", + "foundry.source.filename": "MER_sq001_sh020_P02.%04d.exr 1-200", + "foundry.source.filesize": "", + "foundry.source.fragments": "200", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P02/MER_sq001_sh020_P02.%04d.exr 1-200", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh020_P02.%04d.exr 1-200", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1", + "foundry.source.timecode": "1", + "foundry.source.umid": "bdfbe576-124a-4200-a1c9-daa2dcc3e952", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "2048", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACEScg", + "foundry.timeline.duration": "200", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAQAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "A:{1 0 1 1},B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "9", + "media.exr.compressionName": "DWAB", + "media.exr.dataWindow": "0,358,2047,904", + "media.exr.displayWindow": "0,0,2047,1079", + "media.exr.dwaCompressionLevel": "80", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:03", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P02/MER_sq001_sh020_P02.0001.exr", + "media.input.filereader": "exr", + "media.input.filesize": "453070", + "media.input.frame": "1", + "media.input.height": "1080", + "media.input.mtime": "2022-03-30 11:29:25", + "media.input.width": "2048", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 200.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 1.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P02\\", + "name_prefix": "MER_sq001_sh020_P02.", + "name_suffix": ".exr", + "start_frame": 1, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" + }, + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "P03", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Gap.1", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 76.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "effects": [], + "markers": [], + "enabled": true + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "img_sequence_exr", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 26.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 87311.69068479538 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.plate\": {\"id\": \"ayon.create.instance\", \"productType\": \"plate\", \"productName\": \"plateP03\", \"active\": false, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"P03\", \"folderPath\": \"/shots/test_align/sh010\", \"task\": null, \"clip_index\": \"70A463D1-4FDD-E843-90D9-A2FC3978B06B\", \"hierarchy\": \"shots/test_align\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P03\", \"shot\": \"sh030\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"test_align\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"test_align\", \"track\": \"P01\"}, \"heroTrack\": false, \"uuid\": \"cd05c022-94ff-4527-bd3e-1533a8347f99\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"parent_instance_id\": \"888195e5-f432-4032-9ef5-3e3b7897f80d\", \"label\": \"/shots/test_align/sh010 plateP03\", \"newHierarchyIntegration\": true, \"instance_id\": \"fb5ea749-0f9b-43a0-b2b5-6dadc6f6af7e\", \"creator_attributes\": {\"parentInstance\": \"/shots/test_align/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"clip_media\", \"publish_effects\": true}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"70A463D1-4FDD-E843-90D9-A2FC3978B06B\"}", + "label": "AYONdata_0b6cdbd7", + "note": "AYON data container" + }, + "name": "AYONdata_0b6cdbd7", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 956, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "84", + "foundry.source.filename": "output.%04d.exr 1000-1083", + "foundry.source.filesize": "", + "foundry.source.fragments": "84", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_embedded_tc/output.%04d.exr 1000-1083", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%04d.exr 1000-1083", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1000", + "foundry.source.timecode": "87399", + "foundry.source.umid": "3cd0643b-4ee3-4d94-46dd-7aac61829c84", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "956", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "84", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "0,0,955,684", + "media.exr.displayWindow": "0,0,955,719", + "media.exr.lineOrder": "0", + "media.exr.nuke.input.frame_rate": "24", + "media.exr.nuke.input.timecode": "01:00:41:15", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-09-18 08:28:26", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_embedded_tc/output.1000.exr", + "media.input.filereader": "exr", + "media.input.filesize": "457525", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "720", + "media.input.mtime": "2024-09-18 08:28:26", + "media.input.timecode": "01:00:41:15", + "media.input.width": "956", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "f6b6ac187e7c550c", + "media.nuke.version": "15.0v5", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 84.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 87399.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_embedded_tc\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Video" + }, + { + "OTIO_SCHEMA": "Track.1", + "metadata": {}, + "name": "Audio", + "source_range": null, + "effects": [], + "markers": [], + "enabled": true, + "children": [ + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sq001", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 24.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86400.08874841638 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 52.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86424.08877306872 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh020", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86476.08882648213 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sq001", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 65.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86527.08887886834 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + }, + { + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sq001", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 24.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 86592.08894563509 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Output - Rec.709", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.mov64_decode_video_levels": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "ProRes422(LT)", + "foundry.source.audio": "", + "foundry.source.audiobitdepth": "4", + "foundry.source.colourtransform": "Output - Rec.709", + "foundry.source.duration": "216", + "foundry.source.filename": "sq001.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.numaudiochannels": "2", + "foundry.source.originalsamplerate": "48000", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "48000", + "foundry.source.shoottime": "3732770621", + "foundry.source.shortfilename": "sq001.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "86400", + "foundry.source.timecodedropframe": "0", + "foundry.source.type": "QuickTime ProRes422(LT)", + "foundry.source.umid": "db4a2c91-784e-394f-95e1-7bb75b709c7a", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Output - Rec.709", + "foundry.timeline.duration": "216", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAHAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABzAAAAGW1vdjY0X2RlY29kZV92aWRlb19sZXZlbHMAAAAFaW50MzIAAAAAAAAAFm1vdjY0X2ZpcnN0X3RyYWNrX29ubHkAAAAEYm9vbAEAAAASbW92NjRfeWNiY3JfbWF0cml4AAAABWludDMyAAAAAAAAAAdkZWNvZGVyAAAABWludDMyAAAAAAAAABdtb3Y2NF9tYXRjaF9tZXRhX2Zvcm1hdAAAAARib29sAQAAAA9tb3Y2NF9ub19wcmVmaXgAAAAEYm9vbAA=", + "foundry.timeline.samplerate": "48000" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 216.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 86400.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/projects/AY01_VFX_demo/resources/reference/sq001.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" + } + ], + "kind": "Audio" + } + ] + } +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_collect_otio_frame_ranges.py b/tests/client/ayon_core/pipeline/editorial/test_collect_otio_frame_ranges.py new file mode 100644 index 0000000000..20f0c05804 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/test_collect_otio_frame_ranges.py @@ -0,0 +1,128 @@ +import os + +import opentimelineio as otio + +from ayon_core.plugins.publish import collect_otio_frame_ranges + + +_RESOURCE_DIR = os.path.join( + os.path.dirname(__file__), + "resources", + "timeline" +) + + +class MockInstance(): + """ Mock pyblish instance for testing purpose. + """ + def __init__(self, data: dict): + self.data = data + self.context = self + + +def _check_expected_frame_range_values( + clip_name: str, + expected_data: dict, + handle_start: int = 10, + handle_end: int = 10, + retimed: bool = False, +): + file_path = os.path.join(_RESOURCE_DIR, "timeline.json") + otio_timeline = otio.schema.Timeline.from_json_file(file_path) + + for otio_clip in otio_timeline.find_clips(): + if otio_clip.name == clip_name: + break + + instance_data = { + "otioClip": otio_clip, + "handleStart": handle_start, + "handleEnd": handle_end, + "workfileFrameStart": 1001, + } + if retimed: + instance_data["shotDurationFromSource"] = True + + instance = MockInstance(instance_data) + + processor = collect_otio_frame_ranges.CollectOtioRanges() + processor.process(instance) + + # Assert expected data is subset of edited instance. + assert expected_data.items() <= instance.data.items() + + +def test_movie_with_timecode(): + """ + Movie clip (with embedded timecode) + available_range = 86531-86590 23.976fps + source_range = 86535-86586 23.976fps + """ + expected_data = { + 'frameStart': 1001, + 'frameEnd': 1052, + 'clipIn': 24, + 'clipOut': 75, + 'clipInH': 14, + 'clipOutH': 85, + 'sourceStart': 86535, + 'sourceStartH': 86525, + 'sourceEnd': 86586, + 'sourceEndH': 86596, + } + + _check_expected_frame_range_values( + "sh010", + expected_data, + ) + + +def test_image_sequence(): + """ + EXR image sequence. + available_range = 87399-87482 24fps + source_range = 87311-87336 23.976fps + """ + expected_data = { + 'frameStart': 1001, + 'frameEnd': 1026, + 'clipIn': 76, + 'clipOut': 101, + 'clipInH': 66, + 'clipOutH': 111, + 'sourceStart': 87399, + 'sourceStartH': 87389, + 'sourceEnd': 87424, + 'sourceEndH': 87434, + } + + _check_expected_frame_range_values( + "img_sequence_exr", + expected_data, + ) + +def test_media_retimed(): + """ + EXR image sequence. + available_range = 345619-345691 23.976fps + source_range = 345623-345687 23.976fps + TimeWarp = frozen frame. + """ + expected_data = { + 'frameStart': 1001, + 'frameEnd': 1065, + 'clipIn': 127, + 'clipOut': 191, + 'clipInH': 117, + 'clipOutH': 201, + 'sourceStart': 1001, + 'sourceStartH': 1001, + 'sourceEnd': 1065, + 'sourceEndH': 1065, + } + + _check_expected_frame_range_values( + "P01default_twsh010", + expected_data, + retimed=True, + ) diff --git a/tools/manage.ps1 b/tools/manage.ps1 index 9a9a9a2eff..8324277713 100755 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -240,6 +240,13 @@ function Run-From-Code { & $Poetry $RunArgs @arguments } +function Run-Tests { + $Poetry = "$RepoRoot\.poetry\bin\poetry.exe" + $RunArgs = @( "run", "pytest", "$($RepoRoot)/tests") + + & $Poetry $RunArgs @arguments +} + function Write-Help { <# .SYNOPSIS @@ -256,6 +263,7 @@ function Write-Help { Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan Write-Info -Text " run ", "Run a poetry command in the repository environment" -Color White, Cyan + Write-Info -Text " run-tests ", "Run ayon-core tests" -Color White, Cyan Write-Host "" } @@ -280,6 +288,9 @@ function Resolve-Function { } elseif ($FunctionName -eq "run") { Set-Cwd Run-From-Code + } elseif ($FunctionName -eq "runtests") { + Set-Cwd + Run-Tests } else { Write-Host "Unknown function ""$FunctionName""" Write-Help diff --git a/tools/manage.sh b/tools/manage.sh index 6b0a4d6978..86ae7155c5 100755 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -158,6 +158,7 @@ default_help() { echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" echo -e " ${BWhite}run${RST} ${BCyan}Run a poetry command in the repository environment${RST}" + echo -e " ${BWhite}run-tests${RST} ${BCyan}Run ayon-core tests${RST}" echo "" } @@ -182,6 +183,12 @@ run_command () { "$POETRY_HOME/bin/poetry" run "$@" } +run_tests () { + echo -e "${BIGreen}>>>${RST} Running tests..." + shift; # will remove first arg ("run-tests") from the "$@" + "$POETRY_HOME/bin/poetry" run pytest ./tests +} + main () { detect_python || return 1 @@ -218,6 +225,10 @@ main () { run_command "$@" || return_code=$? exit $return_code ;; + "runtests") + run_tests "$@" || return_code=$? + exit $return_code + ;; esac if [ "$function_name" != "" ]; then