ayon-core/openpype/tools/ayon_loader/abstract.py
Petr Kalis 523f023033
SiteSync: implemented in Ayon Loader (#5962)
* Added new SiteSync model

Used to get information from SiteSync module to enhance Loader UI.

* Added new SiteSync method to controller

Other models will be using these to get information pertaining SiteSync

* Added missed commit

* Implemented collection of SiteSync info

* Added AvailabilityDelegate

Shows how many representations are present locally and remotely in Loader summary page.

* Added fields to store progress info

* Fix HiddenAttr to carry value

* Refactored to internal variable

Changes made after discussion

* Implemented ActionItems for upload/download/remove

Replaced old Launcher approach, now it is not necessary after refactor of Ayon launcher.

* Update openpype/tools/ayon_loader/abstract.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Hound

* Refactor better retrieval of icon

* Refactor better readability

* Refactor renamed delegate

* Refactor better retrieval of icons

* Refactor better readability

* Refactor removed unneeded explicit refresh

* Hound

* Hound

* Hound

* Fix used wrong type

* Update openpype/tools/ayon_loader/ui/products_delegates.py

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>

* Refactor renamed variable name

* Refactor formatting

* Added progress for representations

* cache version availability

* cache representations sync status

* changed representations count logic and moved it to products model

* site sync enabled is cached

* active and remote site names are cached

* small tweaks in site sync model

* change methods called by controller

* hide site sync columns if site sync not enabled

* use string conversion before iteration

* smal formatting changes

* updated abstract class with abstract methods

* renamed site sync model variable

* fixed method name

* fix used method name

* rename '_sitesync_addon' to '_site_sync_addon'

* fix remote site name cache

* small formatting changes in delegate

* modify site sync delegate to be more dynamic

* fix delegate painting

* do not handle repre progress in products model

* Add comma back

* simplify delegate code

---------

Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com>
Co-authored-by: Jakub Trllo <jakub.trllo@gmail.com>
2023-12-04 11:27:43 +01:00

944 lines
25 KiB
Python

from abc import ABCMeta, abstractmethod
import six
from openpype.lib.attribute_definitions import (
AbstractAttrDef,
serialize_attr_defs,
deserialize_attr_defs,
)
class ProductTypeItem:
"""Item representing product type.
Args:
name (str): Product type name.
icon (dict[str, Any]): Product type icon definition.
checked (bool): Is product type checked for filtering.
"""
def __init__(self, name, icon, checked):
self.name = name
self.icon = icon
self.checked = checked
def to_data(self):
return {
"name": self.name,
"icon": self.icon,
"checked": self.checked,
}
@classmethod
def from_data(cls, data):
return cls(**data)
class ProductItem:
"""Product item with it versions.
Args:
product_id (str): Product id.
product_type (str): Product type.
product_name (str): Product name.
product_icon (dict[str, Any]): Product icon definition.
product_type_icon (dict[str, Any]): Product type icon definition.
product_in_scene (bool): Is product in scene (only when used in DCC).
group_name (str): Group name.
folder_id (str): Folder id.
folder_label (str): Folder label.
version_items (dict[str, VersionItem]): Version items by id.
"""
def __init__(
self,
product_id,
product_type,
product_name,
product_icon,
product_type_icon,
product_in_scene,
group_name,
folder_id,
folder_label,
version_items,
):
self.product_id = product_id
self.product_type = product_type
self.product_name = product_name
self.product_icon = product_icon
self.product_type_icon = product_type_icon
self.product_in_scene = product_in_scene
self.group_name = group_name
self.folder_id = folder_id
self.folder_label = folder_label
self.version_items = version_items
def to_data(self):
return {
"product_id": self.product_id,
"product_type": self.product_type,
"product_name": self.product_name,
"product_icon": self.product_icon,
"product_type_icon": self.product_type_icon,
"product_in_scene": self.product_in_scene,
"group_name": self.group_name,
"folder_id": self.folder_id,
"folder_label": self.folder_label,
"version_items": {
version_id: version_item.to_data()
for version_id, version_item in self.version_items.items()
},
}
@classmethod
def from_data(cls, data):
version_items = {
version_id: VersionItem.from_data(version)
for version_id, version in data["version_items"].items()
}
data["version_items"] = version_items
return cls(**data)
class VersionItem:
"""Version item.
Object have implemented comparison operators to be sortable.
Args:
version_id (str): Version id.
version (int): Version. Can be negative when is hero version.
is_hero (bool): Is hero version.
product_id (str): Product id.
thumbnail_id (Union[str, None]): Thumbnail id.
published_time (Union[str, None]): Published time in format
'%Y%m%dT%H%M%SZ'.
author (Union[str, None]): Author.
frame_range (Union[str, None]): Frame range.
duration (Union[int, None]): Duration.
handles (Union[str, None]): Handles.
step (Union[int, None]): Step.
comment (Union[str, None]): Comment.
source (Union[str, None]): Source.
"""
def __init__(
self,
version_id,
version,
is_hero,
product_id,
thumbnail_id,
published_time,
author,
frame_range,
duration,
handles,
step,
comment,
source,
):
self.version_id = version_id
self.product_id = product_id
self.thumbnail_id = thumbnail_id
self.version = version
self.is_hero = is_hero
self.published_time = published_time
self.author = author
self.frame_range = frame_range
self.duration = duration
self.handles = handles
self.step = step
self.comment = comment
self.source = source
def __eq__(self, other):
if not isinstance(other, VersionItem):
return False
return (
self.is_hero == other.is_hero
and self.version == other.version
and self.version_id == other.version_id
and self.product_id == other.product_id
)
def __ne__(self, other):
return not self.__eq__(other)
def __gt__(self, other):
if not isinstance(other, VersionItem):
return False
if (
other.version == self.version
and self.is_hero
):
return True
return other.version < self.version
def to_data(self):
return {
"version_id": self.version_id,
"product_id": self.product_id,
"thumbnail_id": self.thumbnail_id,
"version": self.version,
"is_hero": self.is_hero,
"published_time": self.published_time,
"author": self.author,
"frame_range": self.frame_range,
"duration": self.duration,
"handles": self.handles,
"step": self.step,
"comment": self.comment,
"source": self.source,
}
@classmethod
def from_data(cls, data):
return cls(**data)
class RepreItem:
"""Representation item.
Args:
representation_id (str): Representation id.
representation_name (str): Representation name.
representation_icon (dict[str, Any]): Representation icon definition.
product_name (str): Product name.
folder_label (str): Folder label.
"""
def __init__(
self,
representation_id,
representation_name,
representation_icon,
product_name,
folder_label
):
self.representation_id = representation_id
self.representation_name = representation_name
self.representation_icon = representation_icon
self.product_name = product_name
self.folder_label = folder_label
def to_data(self):
return {
"representation_id": self.representation_id,
"representation_name": self.representation_name,
"representation_icon": self.representation_icon,
"product_name": self.product_name,
"folder_label": self.folder_label,
}
@classmethod
def from_data(cls, data):
return cls(**data)
class ActionItem:
"""Action item that can be triggered.
Action item is defined for a specific context. To trigger the action
use 'identifier' and context, it necessary also use 'options'.
Args:
identifier (str): Action identifier.
label (str): Action label.
icon (dict[str, Any]): Action icon definition.
tooltip (str): Action tooltip.
options (Union[list[AbstractAttrDef], list[qargparse.QArgument]]):
Action options. Note: 'qargparse' is considered as deprecated.
order (int): Action order.
project_name (str): Project name.
folder_ids (list[str]): Folder ids.
product_ids (list[str]): Product ids.
version_ids (list[str]): Version ids.
representation_ids (list[str]): Representation ids.
"""
def __init__(
self,
identifier,
label,
icon,
tooltip,
options,
order,
project_name,
folder_ids,
product_ids,
version_ids,
representation_ids,
):
self.identifier = identifier
self.label = label
self.icon = icon
self.tooltip = tooltip
self.options = options
self.order = order
self.project_name = project_name
self.folder_ids = folder_ids
self.product_ids = product_ids
self.version_ids = version_ids
self.representation_ids = representation_ids
def _options_to_data(self):
options = self.options
if not options:
return options
if isinstance(options[0], AbstractAttrDef):
return serialize_attr_defs(options)
# NOTE: Data conversion is not used by default in loader tool. But for
# future development of detached UI tools it would be better to be
# prepared for it.
raise NotImplementedError(
"{}.to_data is not implemented. Use Attribute definitions"
" from 'openpype.lib' instead of 'qargparse'.".format(
self.__class__.__name__
)
)
def to_data(self):
options = self._options_to_data()
return {
"identifier": self.identifier,
"label": self.label,
"icon": self.icon,
"tooltip": self.tooltip,
"options": options,
"order": self.order,
"project_name": self.project_name,
"folder_ids": self.folder_ids,
"product_ids": self.product_ids,
"version_ids": self.version_ids,
"representation_ids": self.representation_ids,
}
@classmethod
def from_data(cls, data):
options = data["options"]
if options:
options = deserialize_attr_defs(options)
data["options"] = options
return cls(**data)
@six.add_metaclass(ABCMeta)
class _BaseLoaderController(object):
"""Base loader controller abstraction.
Abstract base class that is required for both frontend and backed.
"""
@abstractmethod
def get_current_context(self):
"""Current context is a context of the current scene.
Example output:
{
"project_name": "MyProject",
"folder_id": "0011223344-5566778-99",
"task_name": "Compositing",
}
Returns:
dict[str, Union[str, None]]: Context data.
"""
pass
@abstractmethod
def reset(self):
"""Reset all cached data to reload everything.
Triggers events "controller.reset.started" and
"controller.reset.finished".
"""
pass
# Model wrappers
@abstractmethod
def get_folder_items(self, project_name, sender=None):
"""Folder items for a project.
Args:
project_name (str): Project name.
sender (Optional[str]): Sender who requested the name.
Returns:
list[FolderItem]: Folder items for the project.
"""
pass
# Expected selection helpers
@abstractmethod
def get_expected_selection_data(self):
"""Full expected selection information.
Expected selection is a selection that may not be yet selected in UI
e.g. because of refreshing, this data tell the UI what should be
selected when they finish their refresh.
Returns:
dict[str, Any]: Expected selection data.
"""
pass
@abstractmethod
def set_expected_selection(self, project_name, folder_id):
"""Set expected selection.
Args:
project_name (str): Name of project to be selected.
folder_id (str): Id of folder to be selected.
"""
pass
class BackendLoaderController(_BaseLoaderController):
"""Backend loader controller abstraction.
What backend logic requires from a controller for proper logic.
"""
@abstractmethod
def emit_event(self, topic, data=None, source=None):
"""Emit event with a certain topic, data and source.
The event should be sent to both frontend and backend.
Args:
topic (str): Event topic name.
data (Optional[dict[str, Any]]): Event data.
source (Optional[str]): Event source.
"""
pass
@abstractmethod
def get_loaded_product_ids(self):
"""Return set of loaded product ids.
Returns:
set[str]: Set of loaded product ids.
"""
pass
class FrontendLoaderController(_BaseLoaderController):
@abstractmethod
def register_event_callback(self, topic, callback):
"""Register callback for an event topic.
Args:
topic (str): Event topic name.
callback (func): Callback triggered when the event is emitted.
"""
pass
# Expected selection helpers
@abstractmethod
def expected_project_selected(self, project_name):
"""Expected project was selected in frontend.
Args:
project_name (str): Project name.
"""
pass
@abstractmethod
def expected_folder_selected(self, folder_id):
"""Expected folder was selected in frontend.
Args:
folder_id (str): Folder id.
"""
pass
# Model wrapper calls
@abstractmethod
def get_project_items(self, sender=None):
"""Items for all projects available on server.
Triggers event topics "projects.refresh.started" and
"projects.refresh.finished" with data:
{
"sender": sender
}
Notes:
Filtering of projects is done in UI.
Args:
sender (Optional[str]): Sender who requested the items.
Returns:
list[ProjectItem]: List of project items.
"""
pass
@abstractmethod
def get_product_items(self, project_name, folder_ids, sender=None):
"""Product items for folder ids.
Triggers event topics "products.refresh.started" and
"products.refresh.finished" with data:
{
"project_name": project_name,
"folder_ids": folder_ids,
"sender": sender
}
Args:
project_name (str): Project name.
folder_ids (Iterable[str]): Folder ids.
sender (Optional[str]): Sender who requested the items.
Returns:
list[ProductItem]: List of product items.
"""
pass
@abstractmethod
def get_product_item(self, project_name, product_id):
"""Receive single product item.
Args:
project_name (str): Project name.
product_id (str): Product id.
Returns:
Union[ProductItem, None]: Product info or None if not found.
"""
pass
@abstractmethod
def get_product_type_items(self, project_name):
"""Product type items for a project.
Product types have defined if are checked for filtering or not.
Returns:
list[ProductTypeItem]: List of product type items for a project.
"""
pass
@abstractmethod
def get_representation_items(
self, project_name, version_ids, sender=None
):
"""Representation items for version ids.
Triggers event topics "model.representations.refresh.started" and
"model.representations.refresh.finished" with data:
{
"project_name": project_name,
"version_ids": version_ids,
"sender": sender
}
Args:
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
sender (Optional[str]): Sender who requested the items.
Returns:
list[RepreItem]: List of representation items.
"""
pass
@abstractmethod
def get_version_thumbnail_ids(self, project_name, version_ids):
"""Get thumbnail ids for version ids.
Args:
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
Returns:
dict[str, Union[str, Any]]: Thumbnail id by version id.
"""
pass
@abstractmethod
def get_folder_thumbnail_ids(self, project_name, folder_ids):
"""Get thumbnail ids for folder ids.
Args:
project_name (str): Project name.
folder_ids (Iterable[str]): Folder ids.
Returns:
dict[str, Union[str, Any]]: Thumbnail id by folder id.
"""
pass
@abstractmethod
def get_versions_representation_count(
self, project_name, version_ids, sender=None
):
"""
Args:
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
sender (Optional[str]): Sender who requested the items.
Returns:
dict[str, int]: Representation count by version id.
"""
pass
@abstractmethod
def get_thumbnail_path(self, project_name, thumbnail_id):
"""Get thumbnail path for thumbnail id.
This method should get a path to a thumbnail based on thumbnail id.
Which probably means to download the thumbnail from server and store
it locally.
Args:
project_name (str): Project name.
thumbnail_id (str): Thumbnail id.
Returns:
Union[str, None]: Thumbnail path or None if not found.
"""
pass
# Selection model wrapper calls
@abstractmethod
def get_selected_project_name(self):
"""Get selected project name.
The information is based on last selection from UI.
Returns:
Union[str, None]: Selected project name.
"""
pass
@abstractmethod
def get_selected_folder_ids(self):
"""Get selected folder ids.
The information is based on last selection from UI.
Returns:
list[str]: Selected folder ids.
"""
pass
@abstractmethod
def get_selected_version_ids(self):
"""Get selected version ids.
The information is based on last selection from UI.
Returns:
list[str]: Selected version ids.
"""
pass
@abstractmethod
def get_selected_representation_ids(self):
"""Get selected representation ids.
The information is based on last selection from UI.
Returns:
list[str]: Selected representation ids.
"""
pass
@abstractmethod
def set_selected_project(self, project_name):
"""Set selected project.
Project selection changed in UI. Method triggers event with topic
"selection.project.changed" with data:
{
"project_name": self._project_name
}
Args:
project_name (Union[str, None]): Selected project name.
"""
pass
@abstractmethod
def set_selected_folders(self, folder_ids):
"""Set selected folders.
Folder selection changed in UI. Method triggers event with topic
"selection.folders.changed" with data:
{
"project_name": project_name,
"folder_ids": folder_ids
}
Args:
folder_ids (Iterable[str]): Selected folder ids.
"""
pass
@abstractmethod
def set_selected_versions(self, version_ids):
"""Set selected versions.
Version selection changed in UI. Method triggers event with topic
"selection.versions.changed" with data:
{
"project_name": project_name,
"folder_ids": folder_ids,
"version_ids": version_ids
}
Args:
version_ids (Iterable[str]): Selected version ids.
"""
pass
@abstractmethod
def set_selected_representations(self, repre_ids):
"""Set selected representations.
Representation selection changed in UI. Method triggers event with
topic "selection.representations.changed" with data:
{
"project_name": project_name,
"folder_ids": folder_ids,
"version_ids": version_ids,
"representation_ids": representation_ids
}
Args:
repre_ids (Iterable[str]): Selected representation ids.
"""
pass
# Load action items
@abstractmethod
def get_versions_action_items(self, project_name, version_ids):
"""Action items for versions selection.
Args:
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
Returns:
list[ActionItem]: List of action items.
"""
pass
@abstractmethod
def get_representations_action_items(
self, project_name, representation_ids
):
"""Action items for representations selection.
Args:
project_name (str): Project name.
representation_ids (Iterable[str]): Representation ids.
Returns:
list[ActionItem]: List of action items.
"""
pass
@abstractmethod
def trigger_action_item(
self,
identifier,
options,
project_name,
version_ids,
representation_ids
):
"""Trigger action item.
Triggers event "load.started" with data:
{
"identifier": identifier,
"id": <Random UUID>,
}
And triggers "load.finished" with data:
{
"identifier": identifier,
"id": <Random UUID>,
"error_info": [...],
}
Args:
identifier (str): Action identifier.
options (dict[str, Any]): Action option values from UI.
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
representation_ids (Iterable[str]): Representation ids.
"""
pass
@abstractmethod
def change_products_group(self, project_name, product_ids, group_name):
"""Change group of products.
Triggers event "products.group.changed" with data:
{
"project_name": project_name,
"folder_ids": folder_ids,
"product_ids": product_ids,
"group_name": group_name,
}
Args:
project_name (str): Project name.
product_ids (Iterable[str]): Product ids.
group_name (str): New group name.
"""
pass
@abstractmethod
def fill_root_in_source(self, source):
"""Fill root in source path.
Args:
source (Union[str, None]): Source of a published version. Usually
rootless workfile path.
"""
pass
# NOTE: Methods 'is_loaded_products_supported' and
# 'is_standard_projects_filter_enabled' are both based on being in host
# or not. Maybe we could implement only single method 'is_in_host'?
@abstractmethod
def is_loaded_products_supported(self):
"""Is capable to get information about loaded products.
Returns:
bool: True if it is supported.
"""
pass
@abstractmethod
def is_standard_projects_filter_enabled(self):
"""Is standard projects filter enabled.
This is used for filtering out when loader tool is used in a host. In
that case only current project and library projects should be shown.
Returns:
bool: Frontend should filter out non-library projects, except
current context project.
"""
pass
# Site sync functions
@abstractmethod
def is_site_sync_enabled(self, project_name=None):
"""Is site sync enabled.
Site sync addon can be enabled but can be disabled per project.
When asked for enabled state without project name, it should return
True if site sync addon is available and enabled.
Args:
project_name (Optional[str]): Project name.
Returns:
bool: True if site sync is enabled.
"""
pass
@abstractmethod
def get_active_site_icon_def(self, project_name):
"""Active site icon definition.
Args:
project_name (Union[str, None]): Project name.
Returns:
Union[dict[str, Any], None]: Icon definition or None if site sync
is not enabled for the project.
"""
pass
@abstractmethod
def get_remote_site_icon_def(self, project_name):
"""Remote site icon definition.
Args:
project_name (Union[str, None]): Project name.
Returns:
Union[dict[str, Any], None]: Icon definition or None if site sync
is not enabled for the project.
"""
pass
@abstractmethod
def get_version_sync_availability(self, project_name, version_ids):
"""Version sync availability.
Args:
project_name (str): Project name.
version_ids (Iterable[str]): Version ids.
Returns:
dict[str, tuple[int, int]]: Sync availability by version id.
"""
pass
@abstractmethod
def get_representations_sync_status(
self, project_name, representation_ids
):
"""Representations sync status.
Args:
project_name (str): Project name.
representation_ids (Iterable[str]): Representation ids.
Returns:
dict[str, tuple[int, int]]: Sync status by representation id.
"""
pass