Merge remote-tracking branch 'origin/feature/911-new-traits-based-integrator' into feature/911-new-traits-based-integrator

This commit is contained in:
Ondřej Samohel 2025-03-31 15:43:15 +02:00
commit 44f56e23ba
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
4 changed files with 136 additions and 12 deletions

View file

@ -391,7 +391,7 @@ class Representation(Generic[T]): # noqa: PLR0904
"""
return {
trait_id: trait.model_dump()
trait_id: trait.as_dict()
for trait_id, trait in self._data.items()
if trait and trait_id
}
@ -593,10 +593,6 @@ class Representation(Generic[T]): # noqa: PLR0904
)
raise IncompatibleTraitVersionError(msg) from e
if requested_version is None:
trait_class = e.found_trait
requested_version = found_version
if found_version is None:
msg = (
f"Trait {e.found_trait.id} found with no version, "
@ -604,6 +600,10 @@ class Representation(Generic[T]): # noqa: PLR0904
)
raise IncompatibleTraitVersionError(msg) from e
if requested_version is None:
trait_class = e.found_trait
requested_version = found_version
if requested_version > found_version:
error_msg = (
f"Requested trait version {requested_version} is "

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Generic, Optional, TypeVar
if TYPE_CHECKING:
@ -79,6 +79,15 @@ class TraitBase(ABC):
"""
return re.sub(r"\.v\d+$", "", str(cls.id))
def as_dict(self) -> dict:
"""Return trait as dictionary.
Returns:
dict: Trait as dictionary.
"""
return asdict(self)
class IncompatibleTraitVersionError(Exception):
"""Incompatible trait version exception.

View file

@ -4,9 +4,10 @@ from __future__ import annotations
import contextlib
import copy
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, List
from typing import TYPE_CHECKING, Any
import pyblish.api
from ayon_api import (
@ -88,6 +89,7 @@ class TransferItem:
template: str
template_data: dict[str, Any]
representation: Representation
related_trait: FileLocation
@staticmethod
def get_size(file_path: Path) -> int:
@ -149,7 +151,7 @@ class RepresentationEntity:
status: str
def get_instance_families(instance: pyblish.api.Instance) -> List[str]:
def get_instance_families(instance: pyblish.api.Instance) -> list[str]:
"""Get all families of the instance.
Todo:
@ -159,7 +161,7 @@ def get_instance_families(instance: pyblish.api.Instance) -> List[str]:
instance (pyblish.api.Instance): Instance to get families from.
Returns:
List[str]: List of families.
list[str]: List of families.
"""
family = instance.data.get("family")
@ -210,10 +212,39 @@ def get_changed_attributes(
return changes
def prepare_for_json(data: dict[str, Any]) -> dict[str, Any]:
"""Prepare data for JSON serialization.
If there are values that json cannot serialize, this function will
convert them to strings.
Args:
data (dict[str, Any]): Data to prepare.
Returns:
dict[str, Any]: Prepared data.
Raises:
TypeError: If the data cannot be converted to JSON.
"""
prepared = {}
for key, value in data.items():
if isinstance(value, dict):
value = prepare_for_json(value)
try:
json.dumps(value)
except TypeError:
value = value.as_posix() if issubclass(
value.__class__, Path) else str(value)
prepared[key] = value
return prepared
class IntegrateTraits(pyblish.api.InstancePlugin):
"""Integrate representations with traits."""
label = "Integrate Asset"
label = "Integrate Traits of an Asset"
order = pyblish.api.IntegratorOrder
log: logging.Logger
@ -229,7 +260,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
"""
# 1) skip farm and integrate == False
if not instance.data.get("integrate"):
if instance.data.get("integrate", True) is False:
self.log.debug("Instance is marked to skip integrating. Skipping")
return
@ -290,6 +321,10 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
self.log.debug(
"Transferred files %s", [file_transactions.transferred])
# replace original paths with the destination in traits.
for transfer in transfers:
transfer.related_trait.file_path = transfer.destination
# 9) Create representation entities
for representation in representations:
representation_entity = new_representation_entity(
@ -306,8 +341,14 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
)
# add traits to representation entity
representation_entity["traits"] = representation.traits_as_dict()
op_session.create_entity(
project_name=instance.context.data["projectName"],
entity_type="representation",
data=prepare_for_json(representation_entity),
)
# 10) Commit the session to AYON
self.log.debug("{}".format(op_session.to_data()))
op_session.commit()
def get_transfers_from_representations(
@ -811,7 +852,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
template_data = copy.deepcopy(instance.data["anatomyData"])
template_data["representation"] = representation.name
template_data["version"] = instance.data["version"]
template_data["hierarchy"] = instance.data["hierarchy"]
# template_data["hierarchy"] = instance.data["hierarchy"]
# add colorspace data to template data
if representation.contains_trait(ColorManaged):
@ -944,6 +985,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
template=template_item.template,
template_data=template_item.template_data,
representation=representation,
related_trait=file_loc
)
)
@ -1001,6 +1043,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
template=template_item.template,
template_data=template_item.template_data,
representation=representation,
related_trait=file_loc
)
)
# add template path and the data to resolve it
@ -1058,6 +1101,7 @@ class IntegrateTraits(pyblish.api.InstancePlugin):
template=template_item.template,
template_data=template_item.template_data,
representation=representation,
related_trait=file_loc
)
)
# add template path and the data to resolve it

View file

@ -237,6 +237,8 @@ def mock_context(
return context
def test_get_template_name(mock_context: pyblish.api.Context) -> None:
"""Test get_template_name.
@ -252,6 +254,75 @@ def test_get_template_name(mock_context: pyblish.api.Context) -> None:
assert template_name == "default"
class TestGetSize:
@staticmethod
def get_size(file_path: Path) -> int:
"""Get size of the file.
Args:
file_path (Path): File path.
Returns:
int: Size of the file.
"""
return file_path.stat().st_size
@pytest.mark.parametrize(
"file_path, expected_size",
[
(Path("./test_file_1.txt"), 10), # id: happy_path_small_file
(Path("./test_file_2.txt"), 1024), # id: happy_path_medium_file
(Path("./test_file_3.txt"), 10485760) # id: happy_path_large_file
],
ids=["happy_path_small_file", "happy_path_medium_file", "happy_path_large_file"]
)
def test_get_size_happy_path(self, file_path: Path, expected_size: int, tmp_path: Path):
# Arrange
file_path = tmp_path / file_path
file_path.write_bytes(b"\0" * expected_size)
# Act
size = self.get_size(file_path)
# Assert
assert size == expected_size
@pytest.mark.parametrize(
"file_path, expected_size",
[
(Path("./test_file_empty.txt"), 0) # id: edge_case_empty_file
],
ids=["edge_case_empty_file"]
)
def test_get_size_edge_cases(self, file_path: Path, expected_size: int, tmp_path: Path):
# Arrange
file_path = tmp_path / file_path
file_path.touch() # Create an empty file
# Act
size = self.get_size(file_path)
# Assert
assert size == expected_size
@pytest.mark.parametrize(
"file_path, expected_exception",
[
(Path("./non_existent_file.txt"), FileNotFoundError), # id: error_file_not_found
(123, TypeError) # id: error_invalid_input_type
],
ids=["error_file_not_found", "error_invalid_input_type"]
)
def test_get_size_error_cases(self, file_path, expected_exception, tmp_path):
# Act & Assert
with pytest.raises(expected_exception):
file_path = tmp_path / file_path
self.get_size(file_path)
def test_filter_lifecycle() -> None:
"""Test filter_lifecycle."""
integrator = IntegrateTraits()