From f32a4a43bd60ed92f896b7a5c7104970ae48ca88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:52:31 +0200 Subject: [PATCH 01/19] base of load api --- client/ayon_core/pipeline/load/context.py | 80 +++++++++++ client/ayon_core/pipeline/load/plugins.py | 160 +++++++++++++++++++++- 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 client/ayon_core/pipeline/load/context.py diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py new file mode 100644 index 0000000000..bad1fcc02e --- /dev/null +++ b/client/ayon_core/pipeline/load/context.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import typing +from typing import Any, Optional +from dataclasses import dataclass + +if typing.TYPE_CHECKING: + from .plugins import LoadPlugin + + +class RepresentationContext: + project: dict[str, Any] + folder: dict[str, Any] + product: dict[str, Any] + version: dict[str, Any] + representation: dict[str, Any] + + +@dataclass +class ContainerItem: + id: str + project_name: str + representation_id: str + load_plugin: str + # How to visually display containers in scene inventory? + # namespace: str + # label: str + + +class LoadContext: + def __init__(self) -> None: + self._shared_data = {} + self._plugins = None + self._containers = [] + self._collect_containers() + + def reset(self) -> None: + self._shared_data = {} + self._plugins = {} + self._containers = [] + self._collect_plugins() + self._collect_containers() + + def get_plugins(self) -> dict[str, LoadPlugin]: + return self._plugins + + def get_plugin(self, identifier: str) -> Optional[LoadPlugin]: + return self._plugins.get(identifier) + + def add_containers(self, containers: list[ContainerItem]) -> None: + """Called by load plugins. + + Args: + containers (list[ContainerItem]): Containers to add. + + """ + self._containers.extend(containers) + + @property + def shared_data(self) -> dict[str, Any]: + """Access to shared data of load plugins. + + It is common that load plugins do store data the same way for all + containers. This helps to share data between the plugins. + + Returns: + dict[str, Any]: Shared data. + + """ + return self._shared_data + + def _collect_plugins(self) -> None: + # TODO implement + self._plugins = {} + + def _collect_containers(self) -> None: + for plugin in sorted( + self.get_plugins().values(), key=lambda p: p.order + ): + plugin.collect_containers() diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 48e860e834..d3218a39aa 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,11 +1,12 @@ """Plugins for loading representations and products into host applications.""" from __future__ import annotations -from abc import abstractmethod +from abc import ABC, abstractmethod import logging import os from typing import Any, Optional, Type +from ayon_core.lib import Logger from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -16,6 +17,11 @@ from ayon_core.pipeline.plugin_discover import ( from ayon_core.settings import get_project_settings from .utils import get_representation_path_from_context +from .context import ( + LoadContext, + RepresentationContext, + ContainerItem, +) class LoaderPlugin(list): @@ -434,6 +440,158 @@ def add_hooks_to_loader( wrap_method(method) +class LoadPlugin(ABC): + """Base class for load plugins. + + Load plugin is responsible for loading representation into + a host application and storing metadata in workfile + to be able to manage them. + + Attributes: + order (int): Order of plugins in which will be executed + collection and in which will be shown in the load menu. + settings_category (str): Settings category (addon name) used to + auto-apply settings. + settings_name (str): Key under 'load' used to auto-apply settings. + identifier (str): Plugin identifier. Must be unique. + + """ + order: int = 100 + settings_category: str = None + settings_name: str = None + + def __init__( + self, + load_context: LoadContext, + project_settings: dict[str, Any], + ) -> None: + self._log = None + self._load_context = load_context + self.apply_settings(project_settings) + + @property + @abstractmethod + def identifier(self) -> str: + """Plugin identifier.""" + pass + + @property + def log(self) -> logging.Logger: + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def apply_settings(self, project_settings: dict[str, Any]) -> None: + """Apply project settings to the plugin. + + Args: + project_settings (dict[str, Any]): Project settings. + + """ + cls_name = self.__class__.__name__ + settings_name = self.settings_name or cls_name + + settings = project_settings + for key in ( + self.settings_category, + "load", + settings_name, + ): + settings = settings.get(key) + if settings is None: + self.log.debug(f"No settings found for {cls_name}") + return + + for key, value in settings.items(): + # Log out attributes that are not defined on plugin object + # - those may be potential dangerous typos in settings + if not hasattr(self, key): + self.log.debug( + "Applying settings to unknown attribute" + f" '{key}' on '{cls_name}'." + ) + setattr(self, key, value) + + @abstractmethod + def collect_containers(self) -> None: + """Collect containers from current workfile. + + This method is called by LoadContext on initialization and on reset. + """ + pass + + @abstractmethod + def is_representation_compatible( + self, + representation_context: RepresentationContext, + ) -> bool: + """Check if representation is compatible with the plugin. + + Args: + representation_context (RepresentationContext): Representation + context. + + Returns: + bool: True if compatible, False otherwise. + + """ + pass + + @abstractmethod + def load( + self, + representation_contexts: list[RepresentationContext], + ) -> list[ContainerItem]: + """Load representations. + + Method still has to call 'add_containers' method on 'LoadContext'. + + Args: + representation_contexts (list[RepresentationContext]): List of + representation contexts. + + Returns: + list[ContainerItem]: List of loaded containers. + + """ + pass + + @abstractmethod + def change_representations( + self, + items: list[tuple[ContainerItem, RepresentationContext]], + ) -> None: + """Change representations of loaded containers. + + Can be used to switch between versions or different representations. + + Args: + items (list[tuple[ContainerItem, RepresentationContext]]): List of + containers and their new representation contexts. + + """ + pass + + @abstractmethod + def remove_containers( + self, + containers: list[ContainerItem] + ) -> None: + """Remove containers from the workfile. + + Args: + containers (list[ContainerItem]): Containers to remove. + + """ + + def _add_containers_to_context( + self, + containers: list[ContainerItem], + ) -> None: + """Helper method to add containers to load context.""" + self._load_context.add_containers(containers) + + def register_loader_plugin(plugin): return register_plugin(LoaderPlugin, plugin) From 8664092569f168db9310190b0e7293414d97a2ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:18:50 +0200 Subject: [PATCH 02/19] comment 'id' --- client/ayon_core/pipeline/load/context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index bad1fcc02e..c8d7ccb0aa 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -18,7 +18,8 @@ class RepresentationContext: @dataclass class ContainerItem: - id: str + # Do we need 'id'? + # id: str project_name: str representation_id: str load_plugin: str From ddd16e241fb1523b3d10b9148a63121a70e4b0db Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:25:21 +0200 Subject: [PATCH 03/19] change method name --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index d3218a39aa..6dab6ca7fb 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -538,7 +538,7 @@ class LoadPlugin(ABC): pass @abstractmethod - def load( + def load_representations( self, representation_contexts: list[RepresentationContext], ) -> list[ContainerItem]: From a7dce28c34a22070e6053da3d62e5162d50a2e56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:35:38 +0200 Subject: [PATCH 04/19] moved track changed wrapper into ayon core lib --- client/ayon_core/lib/__init__.py | 3 +++ .../create/changes.py => lib/track_changes.py} | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) rename client/ayon_core/{pipeline/create/changes.py => lib/track_changes.py} (96%) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 5ccc8d03e5..6d0d82b6fc 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -18,6 +18,7 @@ from .cache import ( CacheItem, NestedCacheItem, ) +from .track_changes import TrackDictChangesItem from .events import ( emit_event, register_event_callback @@ -155,6 +156,8 @@ __all__ = [ "CacheItem", "NestedCacheItem", + "TrackDictChangesItem", + "emit_event", "register_event_callback", diff --git a/client/ayon_core/pipeline/create/changes.py b/client/ayon_core/lib/track_changes.py similarity index 96% rename from client/ayon_core/pipeline/create/changes.py rename to client/ayon_core/lib/track_changes.py index c8b81cac48..051eab9d1f 100644 --- a/client/ayon_core/pipeline/create/changes.py +++ b/client/ayon_core/lib/track_changes.py @@ -3,14 +3,14 @@ import copy _EMPTY_VALUE = object() -class TrackChangesItem: +class TrackDictChangesItem: """Helper object to track changes in data. Has access to full old and new data and will create deep copy of them, so it is not needed to create copy before passed in. Can work as a dictionary if old or new value is a dictionary. In - that case received object is another object of 'TrackChangesItem'. + that case received object is another object of 'TrackDictChangesItem'. Goal is to be able to get old or new value as was or only changed values or get information about removed/changed keys, and all of that on @@ -39,7 +39,7 @@ class TrackChangesItem: ... "key_3": "value_3" ... } - >>> changes = TrackChangesItem(old_value, new_value) + >>> changes = TrackDictChangesItem(old_value, new_value) >>> changes.changed True @@ -280,7 +280,7 @@ class TrackChangesItem: old_value = self.old_value if self._old_is_dict and self._new_is_dict: for key in self.available_keys: - item = TrackChangesItem( + item = TrackDictChangesItem( old_value.get(key), new_value.get(key) ) sub_items[key] = item @@ -294,7 +294,7 @@ class TrackChangesItem: for key in available_keys: # NOTE Use '_EMPTY_VALUE' because old value could be 'None' # which would result in "unchanged" item - sub_items[key] = TrackChangesItem( + sub_items[key] = TrackDictChangesItem( old_value.get(key), _EMPTY_VALUE ) @@ -305,7 +305,7 @@ class TrackChangesItem: for key in available_keys: # NOTE Use '_EMPTY_VALUE' because new value could be 'None' # which would result in "unchanged" item - sub_items[key] = TrackChangesItem( + sub_items[key] = TrackDictChangesItem( _EMPTY_VALUE, new_value.get(key) ) From 3d36ac5706ea4402834ce0fa52ba6a99c2655aa5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:35:49 +0200 Subject: [PATCH 05/19] use track item from lib now --- client/ayon_core/pipeline/create/structures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index b2be377b42..33b9af0f7c 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -5,6 +5,7 @@ from enum import Enum import typing from typing import Optional, Dict, List, Any +from ayon_core.lib import TrackDictChangesItem from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, UnknownDef, @@ -17,7 +18,6 @@ from ayon_core.pipeline import ( ) from .exceptions import ImmutableKeyError -from .changes import TrackChangesItem if typing.TYPE_CHECKING: from .creator_plugins import BaseCreator @@ -808,7 +808,7 @@ class CreatedInstance: def changes(self): """Calculate and return changes.""" - return TrackChangesItem(self.origin_data, self.data_to_store()) + return TrackDictChangesItem(self.origin_data, self.data_to_store()) def mark_as_stored(self): """Should be called when instance data are stored. From 771f111b925df55c2df02fa57ff12aabac86b64e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:36:10 +0200 Subject: [PATCH 06/19] changed ContainerItem class --- client/ayon_core/pipeline/load/context.py | 145 +++++++++++++++++++--- 1 file changed, 130 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index c8d7ccb0aa..27bb6403b3 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -1,31 +1,146 @@ from __future__ import annotations +import copy import typing from typing import Any, Optional -from dataclasses import dataclass + +from ayon_core.lib import TrackDictChangesItem + +from .exceptions import ImmutableKeyError if typing.TYPE_CHECKING: from .plugins import LoadPlugin class RepresentationContext: - project: dict[str, Any] - folder: dict[str, Any] - product: dict[str, Any] - version: dict[str, Any] - representation: dict[str, Any] + project_entity: dict[str, Any] + folder_entity: dict[str, Any] + product_entity: dict[str, Any] + version_entity: dict[str, Any] + representation_entity: dict[str, Any] -@dataclass class ContainerItem: - # Do we need 'id'? - # id: str - project_name: str - representation_id: str - load_plugin: str - # How to visually display containers in scene inventory? - # namespace: str - # label: str + __immutable_keys = ( + "container_id", + "project_name", + "representation_id", + "load_plugin_identifier", + "version_locked", + ) + + def __init__( + self, + container_id: str, + project_name: str, + representation_id: str, + load_plugin: LoadPlugin, + version_locked: bool = False, + # UI specific data + # TODO we should look at these with "fresh eye" + # - What is their meaning and usage? Does it actually fit? + # - Should we allow to define "hierarchy" of the items? + # namespace: str, + # label: str, + data: Optional[dict[str, Any]] = None, + transient_data: Optional[dict[str, Any]] = None, + ): + if data is None: + data = {} + origin_data = copy.deepcopy(data) + data.update({ + "container_id": container_id, + "project_name": project_name, + "representation_id": representation_id, + "load_plugin_identifier": load_plugin.identifier, + "version_locked": version_locked, + }) + + if transient_data is None: + transient_data = {} + + self._data = data + self._origin_data = origin_data + self._transient_data = transient_data + self._load_plugin = load_plugin + + # --- Dictionary like methods --- + def __getitem__(self, key): + return self._data[key] + + def __contains__(self, key): + return key in self._data + + def __setitem__(self, key, value): + # Validate immutable keys + if key in self.__immutable_keys: + if value == self._data.get(key): + return + # Raise exception if key is immutable and value has changed + raise ImmutableKeyError(key) + + if key in self._data and self._data[key] == value: + return + + self._data[key] = value + + def get(self, key, default=None): + return self._data.get(key, default) + + def pop(self, key, *args, **kwargs): + # Raise exception if is trying to pop key which is immutable + if key in self.__immutable_keys: + raise ImmutableKeyError(key) + + return self._data.pop(key, *args, **kwargs) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + # ------ + + def get_container_id(self) -> str: + return self._data["container_id"] + + def get_project_name(self) -> str: + return self._data["project_name"] + + def get_representation_id(self) -> str: + return self._data["representation_id"] + + def get_load_plugin_identifier(self) -> str: + return self._data["load_plugin_identifier"] + + def get_version_locked(self) -> bool: + return self._data["version_locked"] + + def get_data(self) -> dict[str, Any]: + return copy.deepcopy(self._data) + + def get_origin_data(self) -> dict[str, Any]: + return copy.deepcopy(self._origin_data) + + def get_transient_data(self) -> dict[str, Any]: + return self._transient_data + + def get_changes(self) -> TrackDictChangesItem: + """Calculate and return changes.""" + return TrackDictChangesItem(self.origin_data, self.get_data()) + + id: str = property(get_container_id) + container_id: str = property(get_container_id) + project_name: str = property(get_project_name) + load_plugin_identifier: str = property(get_load_plugin_identifier) + representation_id: str = property(get_representation_id) + data: dict[str, Any] = property(get_data) + origin_data: dict[str, Any] = property(get_origin_data) + transient_data: dict[str, Any] = property(get_transient_data) + changes: TrackDictChangesItem = property(get_changes) class LoadContext: From 97d8118cb941d74969f9e8f8e8f1705c039592b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:41:48 +0200 Subject: [PATCH 07/19] fix one more usage of 'TrackDictChangesItem' --- client/ayon_core/pipeline/create/context.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index c9b3178fe4..3d81b8171c 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -21,7 +21,7 @@ import pyblish.api import ayon_api from ayon_core.settings import get_project_settings -from ayon_core.lib import is_func_signature_supported +from ayon_core.lib import is_func_signature_supported, TrackDictChangesItem from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import get_default_values from ayon_core.host import IWorkfileHost, IPublishHost @@ -40,7 +40,6 @@ from .exceptions import ( UnavailableSharedData, HostMissRequiredMethod, ) -from .changes import TrackChangesItem from .structures import ( PublishAttributes, ConvertorItem, @@ -1126,10 +1125,10 @@ class CreateContext: "publish_attributes": self._publish_attributes.data_to_store() } - def context_data_changes(self) -> TrackChangesItem: + def context_data_changes(self) -> TrackDictChangesItem: """Changes of attributes.""" - return TrackChangesItem( + return TrackDictChangesItem( self._original_context_data, self.context_data_to_store() ) From e70b2d2c0f9c6667218ebe3e16a0d36690d6c2c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:59:41 +0200 Subject: [PATCH 08/19] add exceptions.py to load api --- client/ayon_core/pipeline/load/exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 client/ayon_core/pipeline/load/exceptions.py diff --git a/client/ayon_core/pipeline/load/exceptions.py b/client/ayon_core/pipeline/load/exceptions.py new file mode 100644 index 0000000000..5114ee72b6 --- /dev/null +++ b/client/ayon_core/pipeline/load/exceptions.py @@ -0,0 +1,8 @@ +class ImmutableKeyError(TypeError): + """Accessed key is immutable so does not allow changes or removals.""" + + def __init__(self, key, msg=None): + self.immutable_key = key + if not msg: + msg = f"Key \"{key}\" is immutable and does not allow changes." + super().__init__(msg) From aee3ffb88240b2276812200d1b29ecb3c98b7c9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:56:29 +0200 Subject: [PATCH 09/19] added wrapper methods to load context --- client/ayon_core/pipeline/load/context.py | 48 ++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index 27bb6403b3..e65455a77f 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -161,7 +161,16 @@ class LoadContext: return self._plugins def get_plugin(self, identifier: str) -> Optional[LoadPlugin]: - return self._plugins.get(identifier) + """Get plugin by identifier. + + Args: + identifier (str): Plugin identifier. + + Returns: + Optional[LoadPlugin]: Load plugin or None if not found. + + """ + return self._get_plugin_by_identifier(identifier, validate=False) def add_containers(self, containers: list[ContainerItem]) -> None: """Called by load plugins. @@ -185,10 +194,47 @@ class LoadContext: """ return self._shared_data + def load_representations( + self, + identifier: str, + representation_contexts: list[RepresentationContext], + ) -> list[ContainerItem]: + plugin = self._get_plugin_by_identifier(identifier, validate=True) + return plugin.load_representations(representation_contexts) + + def change_representations( + self, + identifier: str, + items: list[tuple[ContainerItem, RepresentationContext]], + ) -> None: + plugin = self._get_plugin_by_identifier(identifier, validate=True) + return plugin.change_representations(items) + + def remove_containers( + self, + identifier: str, + containers: list[ContainerItem], + ) -> None: + plugin = self._get_plugin_by_identifier(identifier, validate=True) + return plugin.remove_containers(containers) + def _collect_plugins(self) -> None: # TODO implement self._plugins = {} + def _get_plugin_by_identifier( + self, identifier: str, validate: bool, + ) -> Optional[LoadPlugin]: + if self._plugins is None: + self._collect_plugins() + plugin = self._plugins.get(identifier) + if validate and plugin is None: + # QUESTION: Use custom exception? + raise ValueError( + f"Plugin with identifier '{identifier}' not found." + ) + return plugin + def _collect_containers(self) -> None: for plugin in sorted( self.get_plugins().values(), key=lambda p: p.order From 2eaf44fc3a5ecca4c9a3f1eebaa77f5258a5409b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:03:12 +0200 Subject: [PATCH 10/19] ContainerItem does not use 'data' to store "generic" data --- client/ayon_core/pipeline/load/context.py | 128 +++++++++++----------- 1 file changed, 67 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index e65455a77f..da761e8174 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -21,124 +21,130 @@ class RepresentationContext: class ContainerItem: - __immutable_keys = ( - "container_id", - "project_name", - "representation_id", - "load_plugin_identifier", - "version_locked", - ) + """Container item of loaded content. + Args: + container_id (str): Unique container id. + project_name (str): Project name. + representation_id (str): Representation id. + label (str): Label of container for UI purposes. + namespace (str): Group label of container for UI purposes. + version_locked (bool): Version is locked to ignore + the last version checks. + parent_container_id (Optional[str]): Parent container id. For visual + purposes. + scene_data (Optional[dict[str, Any]]): Additional data stored to the + scene. + transient_data (Optional[dict[str, Any]]): Internal load plugin data + related to the container. Could be any object e.g. node. + + """ def __init__( self, container_id: str, project_name: str, representation_id: str, + label: str, + namespace: str, load_plugin: LoadPlugin, + *, version_locked: bool = False, - # UI specific data - # TODO we should look at these with "fresh eye" - # - What is their meaning and usage? Does it actually fit? - # - Should we allow to define "hierarchy" of the items? - # namespace: str, - # label: str, - data: Optional[dict[str, Any]] = None, + parent_container_id: Optional[str] = None, + scene_data: Optional[dict[str, Any]] = None, transient_data: Optional[dict[str, Any]] = None, - ): - if data is None: - data = {} - origin_data = copy.deepcopy(data) - data.update({ - "container_id": container_id, - "project_name": project_name, - "representation_id": representation_id, - "load_plugin_identifier": load_plugin.identifier, - "version_locked": version_locked, - }) + ) -> None: + self._container_id = container_id + self._project_name = project_name + self._representation_id = representation_id + self._label = label + self._namespace = namespace + self._load_plugin_identifier = load_plugin.identifier + self._version_locked = version_locked + self._parent_container_id = parent_container_id if transient_data is None: transient_data = {} - self._data = data - self._origin_data = origin_data + if scene_data is None: + scene_data = {} + + self._scene_data = scene_data + self._origin_scene_data = copy.deepcopy(scene_data) self._transient_data = transient_data self._load_plugin = load_plugin # --- Dictionary like methods --- def __getitem__(self, key): - return self._data[key] + return self._scene_data[key] def __contains__(self, key): - return key in self._data + return key in self._scene_data def __setitem__(self, key, value): - # Validate immutable keys - if key in self.__immutable_keys: - if value == self._data.get(key): - return - # Raise exception if key is immutable and value has changed - raise ImmutableKeyError(key) - - if key in self._data and self._data[key] == value: + if key in self._scene_data and self._scene_data[key] == value: return - self._data[key] = value + self._scene_data[key] = value def get(self, key, default=None): - return self._data.get(key, default) + return self._scene_data.get(key, default) def pop(self, key, *args, **kwargs): - # Raise exception if is trying to pop key which is immutable - if key in self.__immutable_keys: - raise ImmutableKeyError(key) - - return self._data.pop(key, *args, **kwargs) + return self._scene_data.pop(key, *args, **kwargs) def keys(self): - return self._data.keys() + return self._scene_data.keys() def values(self): - return self._data.values() + return self._scene_data.values() def items(self): - return self._data.items() + return self._scene_data.items() # ------ def get_container_id(self) -> str: - return self._data["container_id"] + return self._container_id def get_project_name(self) -> str: - return self._data["project_name"] + return self._project_name def get_representation_id(self) -> str: - return self._data["representation_id"] - - def get_load_plugin_identifier(self) -> str: - return self._data["load_plugin_identifier"] + return self._representation_id def get_version_locked(self) -> bool: - return self._data["version_locked"] + return self._version_locked - def get_data(self) -> dict[str, Any]: - return copy.deepcopy(self._data) + def set_version_locked(self, version_locked: bool) -> None: + if self._version_locked == version_locked: + return + self._version_locked = version_locked - def get_origin_data(self) -> dict[str, Any]: - return copy.deepcopy(self._origin_data) + def get_load_plugin_identifier(self) -> str: + return self._load_plugin_identifier + + def get_scene_data(self) -> dict[str, Any]: + return copy.deepcopy(self._scene_data) + + def get_origin_scene_data(self) -> dict[str, Any]: + return copy.deepcopy(self._origin_scene_data) def get_transient_data(self) -> dict[str, Any]: return self._transient_data def get_changes(self) -> TrackDictChangesItem: """Calculate and return changes.""" - return TrackDictChangesItem(self.origin_data, self.get_data()) + return TrackDictChangesItem( + self.get_origin_scene_data(), + self.get_scene_data() + ) id: str = property(get_container_id) container_id: str = property(get_container_id) project_name: str = property(get_project_name) load_plugin_identifier: str = property(get_load_plugin_identifier) representation_id: str = property(get_representation_id) - data: dict[str, Any] = property(get_data) - origin_data: dict[str, Any] = property(get_origin_data) + scene_data: dict[str, Any] = property(get_scene_data) + origin_scene_data: dict[str, Any] = property(get_origin_scene_data) transient_data: dict[str, Any] = property(get_transient_data) changes: TrackDictChangesItem = property(get_changes) From ffb963f832991ab63ea01d358a5ce094357145e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:03:42 +0200 Subject: [PATCH 11/19] change how container items are stored --- client/ayon_core/pipeline/load/context.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index da761e8174..f573c66820 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -153,17 +153,20 @@ class LoadContext: def __init__(self) -> None: self._shared_data = {} self._plugins = None - self._containers = [] + self._containers = {} self._collect_containers() def reset(self) -> None: self._shared_data = {} - self._plugins = {} - self._containers = [] + self._plugins = None + self._containers = {} + self._collect_plugins() self._collect_containers() def get_plugins(self) -> dict[str, LoadPlugin]: + if self._plugins is None: + self._collect_plugins() return self._plugins def get_plugin(self, identifier: str) -> Optional[LoadPlugin]: @@ -185,7 +188,19 @@ class LoadContext: containers (list[ContainerItem]): Containers to add. """ - self._containers.extend(containers) + for container in containers: + if container.id in self._containers: + self._log.warning() + continue + self._containers[container.id] = container + + def get_container_by_id( + self, container_id: str + ) -> Optional[ContainerItem]: + return self._containers.get(container_id) + + def get_containers(self) -> dict[str, ContainerItem]: + return self._containers @property def shared_data(self) -> dict[str, Any]: From e5e7a11ed1dd0e736778deb771f637e053164722 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:04:07 +0200 Subject: [PATCH 12/19] added load plugin switch methods --- client/ayon_core/pipeline/load/exceptions.py | 11 ++----- client/ayon_core/pipeline/load/plugins.py | 32 +++++++++++++++++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/load/exceptions.py b/client/ayon_core/pipeline/load/exceptions.py index 5114ee72b6..d3e855489c 100644 --- a/client/ayon_core/pipeline/load/exceptions.py +++ b/client/ayon_core/pipeline/load/exceptions.py @@ -1,8 +1,3 @@ -class ImmutableKeyError(TypeError): - """Accessed key is immutable so does not allow changes or removals.""" - - def __init__(self, key, msg=None): - self.immutable_key = key - if not msg: - msg = f"Key \"{key}\" is immutable and does not allow changes." - super().__init__(msg) +class UnsupportedSwitchError(Exception): + """Raised when load plugin does not support switching containers.""" + pass diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index b397d7a252..b780c9c888 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -16,6 +16,7 @@ from ayon_core.pipeline.plugin_discover import ( ) from ayon_core.settings import get_project_settings +from .exceptions import UnsupportedSwitchError from .utils import get_representation_path_from_context from .context import ( LoadContext, @@ -513,7 +514,7 @@ class LoadPlugin(ABC): @abstractmethod def collect_containers(self) -> None: - """Collect containers from current workfile. + """Collect containers from the current workfile. This method is called by LoadContext on initialization and on reset. """ @@ -583,6 +584,35 @@ class LoadPlugin(ABC): """ + def can_switch_container(self, container: ContainerItem) -> bool: + """Check if container can be switched. + + Args: + container (ContainerItem): Container to check. + + Returns: + bool: True if container can be switched, False otherwise. + + """ + return False + + def switch_containers( + self, containers: list[ContainerItem] + ) -> list[ContainerItem]: + """Switch containers of other load plugins. + + Args: + containers (list[ContainerItem]): Containers to switch. + + Raises: + UnsupportedSwitchError: If switching is not supported. + + Returns: + list[ContainerItem]: New containers after switching. + + """ + raise UnsupportedSwitchError() + def _add_containers_to_context( self, containers: list[ContainerItem], From e71d1312d521b88ea72482ceba848b3b99bf6128 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:04:21 +0200 Subject: [PATCH 13/19] added new wrappers --- client/ayon_core/pipeline/load/context.py | 53 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index f573c66820..032a5c74f0 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -4,9 +4,7 @@ import copy import typing from typing import Any, Optional -from ayon_core.lib import TrackDictChangesItem - -from .exceptions import ImmutableKeyError +from ayon_core.lib import TrackDictChangesItem, Logger if typing.TYPE_CHECKING: from .plugins import LoadPlugin @@ -150,6 +148,16 @@ class ContainerItem: class LoadContext: + """Context of logic related to loading. + + To be able to load anything in a DCC using AYON is to have load plugins. + Load plugin is responsible for loading representation. To maintain the + loaded content it is usually necessary to store some metadata in workfile. + + Loaded content is refered to as a 'container' which is a helper wrapper + to manage loaded the content, to be able to switch versions or switch to + different representation (png -> exr), or to remove them from the scene. + """ def __init__(self) -> None: self._shared_data = {} self._plugins = None @@ -239,6 +247,45 @@ class LoadContext: plugin = self._get_plugin_by_identifier(identifier, validate=True) return plugin.remove_containers(containers) + def can_switch_container( + self, + identifier: str, + container: ContainerItem, + ) -> bool: + """Check if container can be switched. + + Args: + identifier: Load plugin identifier. + container (ContainerItem): Container to check. + + Returns: + bool: True if container can be switched, False otherwise. + + """ + plugin = self._get_plugin_by_identifier(identifier, validate=True) + return plugin.can_switch_container(container) + + def switch_containers( + self, + identifier: str, + containers: list[ContainerItem], + ) -> list[ContainerItem]: + """Switch containers of other load plugins. + + Args: + identifier: Load plugin identifier. + containers (list[ContainerItem]): Containers to switch. + + Raises: + UnsupportedSwitchError: If switching is not supported. + + Returns: + list[ContainerItem]: New containers after switching. + + """ + plugin = self._get_plugin_by_identifier(identifier, validate=True) + raise plugin.switch_containers(containers) + def _collect_plugins(self) -> None: # TODO implement self._plugins = {} From b6cf5a19f8c4238f634a863ebd8381a68eb381a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:04:30 +0200 Subject: [PATCH 14/19] add missing logger --- client/ayon_core/pipeline/load/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index 032a5c74f0..b7a947215f 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -163,6 +163,7 @@ class LoadContext: self._plugins = None self._containers = {} self._collect_containers() + self._log = Logger.get_logger(self.__class__.__name__) def reset(self) -> None: self._shared_data = {} From 2ca59239bf298cc71a0ae19510912f51724b1e5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:07:12 +0200 Subject: [PATCH 15/19] add is dirty --- client/ayon_core/pipeline/load/context.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index b7a947215f..369d492a5f 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -47,6 +47,7 @@ class ContainerItem: load_plugin: LoadPlugin, *, version_locked: bool = False, + is_dirty: bool = False, parent_container_id: Optional[str] = None, scene_data: Optional[dict[str, Any]] = None, transient_data: Optional[dict[str, Any]] = None, @@ -58,6 +59,7 @@ class ContainerItem: self._namespace = namespace self._load_plugin_identifier = load_plugin.identifier self._version_locked = version_locked + self._is_dirty = is_dirty self._parent_container_id = parent_container_id if transient_data is None: @@ -109,6 +111,15 @@ class ContainerItem: def get_representation_id(self) -> str: return self._representation_id + def get_is_dirty(self) -> bool: + return self._is_dirty + + def set_is_dirty(self, dirty: bool) -> None: + if dirty is self._is_dirty: + return + self._is_dirty = dirty + # TODO trigger event + def get_version_locked(self) -> bool: return self._version_locked @@ -116,6 +127,7 @@ class ContainerItem: if self._version_locked == version_locked: return self._version_locked = version_locked + # TODO trigger event def get_load_plugin_identifier(self) -> str: return self._load_plugin_identifier From b2cdf42667ed93d432e35706d433570a91cc5cb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:03:17 +0200 Subject: [PATCH 16/19] add changes calculation --- client/ayon_core/pipeline/load/context.py | 35 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index 369d492a5f..c398ecc0ef 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -68,6 +68,17 @@ class ContainerItem: if scene_data is None: scene_data = {} + self._orig_generic_data = { + "container_id": self._container_id, + "project_name": self._project_name, + "representation_id": self._representation_id, + "label": self._label, + "namespace": self._namespace, + "load_plugin_identifier": self._load_plugin_identifier, + "version_locked": self._version_locked, + "is_dirty": self._is_dirty, + "parent_container_id": self._parent_container_id, + } self._scene_data = scene_data self._origin_scene_data = copy.deepcopy(scene_data) self._transient_data = transient_data @@ -142,11 +153,25 @@ class ContainerItem: return self._transient_data def get_changes(self) -> TrackDictChangesItem: - """Calculate and return changes.""" - return TrackDictChangesItem( - self.get_origin_scene_data(), - self.get_scene_data() - ) + """Calculate and return changes. + + + """ + new_data = { + "container_id": self._container_id, + "project_name": self._project_name, + "representation_id": self._representation_id, + "label": self._label, + "namespace": self._namespace, + "load_plugin_identifier": self._load_plugin_identifier, + "version_locked": self._version_locked, + "is_dirty": self._is_dirty, + "parent_container_id": self._parent_container_id, + "scene_data": self.get_scene_data(), + } + orig_data = copy.deepcopy(self._orig_generic_data) + orig_data["scene_data"] = self.get_origin_scene_data() + return TrackDictChangesItem(orig_data, new_data) id: str = property(get_container_id) container_id: str = property(get_container_id) From bc485189d0feb9cacc85bee28ca256cf015d6f47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:05:47 +0200 Subject: [PATCH 17/19] added docstrings --- client/ayon_core/pipeline/load/context.py | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index c398ecc0ef..84bef3d78e 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -11,6 +11,16 @@ if typing.TYPE_CHECKING: class RepresentationContext: + """Representation context used for loading. + + Attributes: + project_entity (dict[str, Any]): Project entity. + folder_entity (dict[str, Any]): Folder entity. + product_entity (dict[str, Any]): Product entity. + version_entity (dict[str, Any]): Version entity. + representation_entity (dict[str, Any]): Representation entity. + + """ project_entity: dict[str, Any] folder_entity: dict[str, Any] product_entity: dict[str, Any] @@ -150,11 +160,19 @@ class ContainerItem: return copy.deepcopy(self._origin_scene_data) def get_transient_data(self) -> dict[str, Any]: + """Transient data are manager by load plugin. + + Should be used for any arbitrary data needed for a container + management. + + """ return self._transient_data def get_changes(self) -> TrackDictChangesItem: """Calculate and return changes. + Returns: + TrackDictChangesItem: Calculated changes on container. """ new_data = { @@ -266,6 +284,17 @@ class LoadContext: identifier: str, representation_contexts: list[RepresentationContext], ) -> list[ContainerItem]: + """Load representations. + + Args: + identifier (str): Load plugin identifier. + representation_contexts (list[RepresentationContext]): List of + representation contexts. + + Returns: + list[ContainerItem]: List of loaded containers. + + """ plugin = self._get_plugin_by_identifier(identifier, validate=True) return plugin.load_representations(representation_contexts) @@ -274,6 +303,14 @@ class LoadContext: identifier: str, items: list[tuple[ContainerItem, RepresentationContext]], ) -> None: + """Change representations of loaded containers. + + Args: + identifier (str): Load plugin identifier. + items (list[tuple[ContainerItem, RepresentationContext]]): List + of containers and their new representation contexts. + + """ plugin = self._get_plugin_by_identifier(identifier, validate=True) return plugin.change_representations(items) @@ -282,6 +319,13 @@ class LoadContext: identifier: str, containers: list[ContainerItem], ) -> None: + """Remove containers content with metadata from scene. + + Args: + identifier (str): Load plugin identifier. + containers (list[ContainerItem]): Containers to remove. + + """ plugin = self._get_plugin_by_identifier(identifier, validate=True) return plugin.remove_containers(containers) From e778e78e2422cd9bc57789ab6dd39a6b98575e7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:05:56 +0200 Subject: [PATCH 18/19] added typehints --- client/ayon_core/pipeline/load/context.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index 84bef3d78e..de7c61ee2b 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -2,7 +2,7 @@ from __future__ import annotations import copy import typing -from typing import Any, Optional +from typing import Any, Optional, Iterable from ayon_core.lib import TrackDictChangesItem, Logger @@ -95,31 +95,31 @@ class ContainerItem: self._load_plugin = load_plugin # --- Dictionary like methods --- - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return self._scene_data[key] - def __contains__(self, key): + def __contains__(self, key: str) -> bool: return key in self._scene_data - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: if key in self._scene_data and self._scene_data[key] == value: return self._scene_data[key] = value - def get(self, key, default=None): + def get(self, key: str, default: Any = None) -> Any: return self._scene_data.get(key, default) - def pop(self, key, *args, **kwargs): + def pop(self, key: str, *args, **kwargs) -> Any: return self._scene_data.pop(key, *args, **kwargs) - def keys(self): + def keys(self) -> Iterable[str]: return self._scene_data.keys() - def values(self): + def values(self) -> Iterable[Any]: return self._scene_data.values() - def items(self): + def items(self) -> Iterable[tuple[str, Any]]: return self._scene_data.items() # ------ From d2eab754aac445cbb65edfdf9f956374f537cb56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:47:26 +0100 Subject: [PATCH 19/19] use 'scene_identifier' instead of 'container_id' --- client/ayon_core/pipeline/load/context.py | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/pipeline/load/context.py b/client/ayon_core/pipeline/load/context.py index de7c61ee2b..4227c79a67 100644 --- a/client/ayon_core/pipeline/load/context.py +++ b/client/ayon_core/pipeline/load/context.py @@ -32,14 +32,14 @@ class ContainerItem: """Container item of loaded content. Args: - container_id (str): Unique container id. + scene_identifier (str): Unique container id. project_name (str): Project name. representation_id (str): Representation id. label (str): Label of container for UI purposes. namespace (str): Group label of container for UI purposes. version_locked (bool): Version is locked to ignore the last version checks. - parent_container_id (Optional[str]): Parent container id. For visual + parent_scene_identifier (Optional[str]): Parent container id. For visual purposes. scene_data (Optional[dict[str, Any]]): Additional data stored to the scene. @@ -49,7 +49,7 @@ class ContainerItem: """ def __init__( self, - container_id: str, + scene_identifier: str, project_name: str, representation_id: str, label: str, @@ -58,11 +58,11 @@ class ContainerItem: *, version_locked: bool = False, is_dirty: bool = False, - parent_container_id: Optional[str] = None, + parent_scene_identifier: Optional[str] = None, scene_data: Optional[dict[str, Any]] = None, transient_data: Optional[dict[str, Any]] = None, ) -> None: - self._container_id = container_id + self._scene_identifier = scene_identifier self._project_name = project_name self._representation_id = representation_id self._label = label @@ -70,7 +70,7 @@ class ContainerItem: self._load_plugin_identifier = load_plugin.identifier self._version_locked = version_locked self._is_dirty = is_dirty - self._parent_container_id = parent_container_id + self._parent_scene_identifier = parent_scene_identifier if transient_data is None: transient_data = {} @@ -79,7 +79,7 @@ class ContainerItem: scene_data = {} self._orig_generic_data = { - "container_id": self._container_id, + "scene_identifier": self._scene_identifier, "project_name": self._project_name, "representation_id": self._representation_id, "label": self._label, @@ -87,7 +87,7 @@ class ContainerItem: "load_plugin_identifier": self._load_plugin_identifier, "version_locked": self._version_locked, "is_dirty": self._is_dirty, - "parent_container_id": self._parent_container_id, + "parent_scene_identifier": self._parent_scene_identifier, } self._scene_data = scene_data self._origin_scene_data = copy.deepcopy(scene_data) @@ -123,8 +123,8 @@ class ContainerItem: return self._scene_data.items() # ------ - def get_container_id(self) -> str: - return self._container_id + def get_scene_identifier(self) -> str: + return self._scene_identifier def get_project_name(self) -> str: return self._project_name @@ -176,7 +176,7 @@ class ContainerItem: """ new_data = { - "container_id": self._container_id, + "scene_identifier": self._scene_identifier, "project_name": self._project_name, "representation_id": self._representation_id, "label": self._label, @@ -184,15 +184,15 @@ class ContainerItem: "load_plugin_identifier": self._load_plugin_identifier, "version_locked": self._version_locked, "is_dirty": self._is_dirty, - "parent_container_id": self._parent_container_id, + "parent_scene_identifier": self._parent_scene_identifier, "scene_data": self.get_scene_data(), } orig_data = copy.deepcopy(self._orig_generic_data) orig_data["scene_data"] = self.get_origin_scene_data() return TrackDictChangesItem(orig_data, new_data) - id: str = property(get_container_id) - container_id: str = property(get_container_id) + id: str = property(get_scene_identifier) + scene_identifier: str = property(get_scene_identifier) project_name: str = property(get_project_name) load_plugin_identifier: str = property(get_load_plugin_identifier) representation_id: str = property(get_representation_id) @@ -259,9 +259,9 @@ class LoadContext: self._containers[container.id] = container def get_container_by_id( - self, container_id: str + self, scene_identifier: str ) -> Optional[ContainerItem]: - return self._containers.get(container_id) + return self._containers.get(scene_identifier) def get_containers(self) -> dict[str, ContainerItem]: return self._containers