Representation defined by traits
Changelog Description
Introducing new way of defining representations. Developer friendly, flexible yet strictly typed, easy to extend using Traits as building blocks defining what representation IS.
Additional info
New representation is holder for traits. Each trait define some properties and together with other traits describes the representation. Traits can be anything- their purpose is to help publisher, loader (and basically any other systems) to clearly identify data needed for processing. Like file system path, or frame per second, or resolution. Those properties are grouped together to logical block - frame range specific, resolution specific, etc.
This PR is introducing several new features:
TraitBase - this is a model for all Traits. It defines abstract properties like id, name, description that needs to be in all Traits.
[!NOTE] Every Trait can re-implement
validate()method. The one inTraitBasealways returnsTrue.Representationis passed to that method so every Trait can validate against all other Trait present in representation.
Representation - this is "container" of sorts to hold all Traits. It holds representationname and id. And lot of "helper" methods to work with Traits:
- methods to check if Trait exists in the Representation
- methods to add and remove Traits
- methods to get Traits
- method to return whole Representation serialized as Python dict
- method to reconstruct Representation from the dict
Most of them can run on bulk of Traits and you can access Traits by their class or by their Id. More on that below.
There is also mechanism for upgrading and versioning Traits.
Traits
There are some pre-defined Traits that should come with ayon-core to provide "common language". They are all based on TraitBase and are grouped to logical chunks or namespaces - content, lifecycle, meta, three_dimensional, time, two_dimensional. Their complete list can be found in the code and is obviously subject to change based on the need.
Practical examples
So how to work with traits and representations? To define traits (for example in Collector plugin or in Extractor plugin:
from ayon_core.pipeline.traits import (
FileLocation,
Image,,
MimeType,
PixelBased,
Representation
)
# define traits
file_location = FileLocation(
file_path=Path("/path/to/file.jpg"),
)
pixel_based = PixelBased(
display_window_width=1920,
display_window_height=1080,
pixel_aspect_ratio=1.0
)
# create representation itself
representation = Representation(
name="Image test",
traits=[file_location, Image(), pixel_based])
# add additional traits
mime_type = MimeType(mime_type="image/jpeg")
representation.add_trait(mime_type)
# remove trait
representation.remove_trait(MimeType)
if representation.contains_trait(PixelBased):
print(f"width: {representation.get_trait(PixelBased).display_window_width)}")
You can work with Traits using classes, but you can also utilize their ids. That is useful when working with representation that was serialized into "plain' dictionary:
# some pseudo-function to get representation as dict
representation = Representation.from_dict(get_representation_dict(...))
# get trait by its id
# note: the use of version in the ID. You can test if trait is present in representation, or if trait of specific version is present.
if representation.contains_trait_by_id("ayon.content.FileLocation"):
print(f"path: {representation.get_trait_by_id("ayon.content.FileLocation.v1")
# serialize back to dict
representation.traits_as_dict()
There is also feature of version upgrade on Traits. Whenever you want to de-serialize data that are using older version of trait, upgrade() method on newer trait definition is called to reconstruct new version (downgrading isn't possible). So, you can have serialized trait like so (type trait without properties):
{
ayon.2d.Image.v1: {}
}
But your current runtime has Image trait ayon.2d.Image.v2 that is also adding property foo.
Whenever you run representation.get_trait_by_id("ayon.2d.Image") without version specifier, it can try to find out the latest Trait definition available and if it differs - v1 != v2 it tries to call upgrade() method on the latest trait.
Notes
Traits are no longer Pydantic models because Pydantic 2 is based on rust and we would need to support it in all possible host (distribute it somehow in AYON too). So until this issue is resolved, it is better to have them as pure Python dataclasses.
Hey, would you like to me do some test runs for this PR?
Hey, would you like to me do some test runs for this PR?
no tests yet (apart unit tests that were passing recently). It is more work in progress now - main development is now done on the integrator. This is here to discuss traits and if we have everything we need. I guess more will be clear one we try to use them in existing workflows.
I guess more will be clear one we try to use them in existing workflows.
This is most likely how I may test run it by trying to use it in draft PR. Personally, I find it cool to try new things.
Regarding trait versions:
current idea is to store the version in trait id itself - ayon.content.FileLocation.v1 in similar manner OTIO is doing it. Trait have helper function that can parse it from the id - FileLocation.get_version().
Example:
file_location = FileLocation(file_path=Path(...))
# it is class method
file_location.get_version() == FileLocation.get_version()
print(FileLocation.id)
# ayon.content.FileLocation.v1
There are some functions on Representation that takes id without version, then are trying to use the latest available.
I considered having version encoded in id as now, but having it also as @computed_field on the model, but that was somewhat duplicating information.
Some lingering questions:
Trait inheritance
I am thinking that trait inheritance creates more issues than adds more value. Mainly in following situation:
Currently, Sequence trait is subclass of FrameRanged and Handles. Important bit is, that frames_per_second is defined in FrameRanged. To now, that Sequence has fps because it inherited it from FrameRanged is information I, as developer shouldn't track. I can explicitly check with Representation.contain_trait(FrameRanged) but that will fail because it really has just Sequence. I can of course implement logic for checking subclasses, but I don't like that idea.
class FrameRanged:
frames_per_second: float
...
class Sequence(FrameRanged):
...
representation = Representation(name="test", traits=[
Sequence(...),
])
# I need to get fps from representation
representation.contain_trait(FrameRanged) # fails
for trait in representation.get_traits().values:
The other approach I like even less is: iterate over all traits on representation, find first that defines frame_per_second and return it - because that is basically defeating purpose of pydantic models and strong types.
So if there is no inheritance, I can check for specific types and be clear about that, but the tradeoff is that I still need to track what Trait defines that property - which is IMO more straightforward than tracking this AND inheritance.
File sequence
Another question is description of file sequence. There are two ways:
- track as a file just first frame, implore the rest based on the fact that other Traits present are marking that representation as a sequence:
# sequence of files
representation = Representation(name="foo", traits=[
FileLocation(Path("/foo/bar.1001.exr", ...),
Sequence(frame_start=1001, frame_end=1050, ...)
])
# single file (not sequence of one frame)
representation = Representation(name="foo", traits=[
FileLocation(Path("/foo/bar.1001.exr", ...),
Static()
])
Therefor anyone working with such representation needs to check for Sequence and/or Static and consider it in the logic.
- be more explicit using trait, that is actually describing all files:
representation = Representation(name="foo", traits=[
FileLocations(paths=[
Path("/foo/bar.1001.exr"),
Path("/foo/bar.1002.exr"),
...
]),
Sequence(frame_start=1001, frame_end=1050, ...)
])
With this, we can track explicitly all individual files, we don't need to do all those calculations of possible frame padding, etc. and the traits can use validate() logic on them (so Sequence can check that FileLocations has matching files and vice-versa).
This was already merged in #1147