diff --git a/automated_api.py b/automated_api.py index 766cd4bc3..ab6ced2d9 100644 --- a/automated_api.py +++ b/automated_api.py @@ -197,40 +197,9 @@ def _get_typehint(annotation, api_globals): # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) return typehint - except NameError: - print("Unknown typehint:", typehint) - - _typehint = typehint - _typehing_parents = [] - while True: - # Too hard to manage typehints with commas - if "[" not in _typehint: - break - - parts = _typehint.split("[") - parent = parts.pop(0) - - try: - # Test if typehint is valid for known '_api' content - exec(f"_: {parent} = None", api_globals) - except NameError: - _typehint = parent - break - - _typehint = "[".join(parts)[:-1] - if "," in _typehint: - _typing = parent - break - - _typehing_parents.append(parent) - - if _typehing_parents: - typehint = _typehint - for parent in reversed(_typehing_parents): - typehint = f"{parent}[{typehint}]" - return typehint - - return typehint + except Exception: + print("Error while processing typehint:", typehint) + raise def _get_param_typehint(param, api_globals): @@ -452,12 +421,20 @@ def main(): formatting_init_content = prepare_init_without_api(init_filepath) # Read content of first part of `_api.py` to get global variables - # - disable type checking so imports done only during typechecking are - # not executed + # - first with disabled type checking so other files from ayon_api are + # loded without any issues typing.TYPE_CHECKING = False api_globals = {"__name__": "ayon_api._api"} exec(parts[0], api_globals) + # - second with enabled type checking to get all available types in the + # file + # NOTE The file contains 'from __future__ import annotations' so any + # typehints can be used, but we should validate if are available. + typing.TYPE_CHECKING = True + api_globals = {"__name__": "ayon_api._api"} + exec(parts[0], api_globals) + for attr_name in dir(__builtins__): api_globals[attr_name] = getattr(__builtins__, attr_name) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index a125e8993..3d5f89311 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -298,6 +298,13 @@ update_entity_list_items, update_entity_list_item, delete_entity_list_item, + get_entity_list_entities, + get_entity_list_folders_raw, + get_entity_list_folders, + create_entity_list_folder, + update_entity_list_folder, + delete_entity_list_folder, + set_entity_list_folders_order, get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, @@ -609,6 +616,13 @@ "update_entity_list_items", "update_entity_list_item", "delete_entity_list_item", + "get_entity_list_entities", + "get_entity_list_folders_raw", + "get_entity_list_folders", + "create_entity_list_folder", + "update_entity_list_folder", + "delete_entity_list_folder", + "set_entity_list_folders_order", "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 5c843360f..42eac9827 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -48,8 +48,10 @@ ActivityReferenceType, EntityListEntityType, EntityListItemMode, + EntityListScope, BackgroundOperationTask, LinkDirection, + CreateLinkData, EventFilter, EventStatus, EnrollEventData, @@ -85,7 +87,6 @@ EntityListAttributeDefinitionDict, AdvancedFilterDict, ) - from ._api_helpers.links import CreateLinkData class GlobalServerAPI(ServerAPI): @@ -7981,6 +7982,7 @@ def create_entity_list( data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, + entity_list_folder_id: Optional[str] = None, owner: Optional[str] = None, active: Optional[bool] = None, items: Optional[list[dict[str, Any]]] = None, @@ -8000,6 +8002,7 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. + entity_list_folder_id (Optional[str]): Entity list folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -8018,6 +8021,7 @@ def create_entity_list( data=data, tags=tags, template=template, + entity_list_folder_id=entity_list_folder_id, owner=owner, active=active, items=items, @@ -8034,6 +8038,7 @@ def update_entity_list( attrib: Optional[list[dict[str, Any]]] = None, data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, + entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -8048,6 +8053,9 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. + entity_list_folder_id (str | None | type[NOT_SET]): New entity + list folder id. Use ``None`` to move entity list to root. + Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. @@ -8061,6 +8069,7 @@ def update_entity_list( attrib=attrib, data=data, tags=tags, + entity_list_folder_id=entity_list_folder_id, owner=owner, active=active, ) @@ -8259,6 +8268,183 @@ def delete_entity_list_item( ) +def get_entity_list_entities( + project_name: str, + entity_list_id: str, +) -> dict[str, Any]: + """Get entity list items using REST API. + + Args: + project_name (str): Project name. + entity_list_id (str): Entity list id. + + Returns: + dict[str, Any]: Information about entities on the list. + + """ + con = get_server_api_connection() + return con.get_entity_list_entities( + project_name=project_name, + entity_list_id=entity_list_id, + ) + + +def get_entity_list_folders_raw( + project_name: str, +) -> dict[str, Any]: + """Get entity list folders. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Raw output of entity list folders output. At this + moment contains only "folders" key with list of folders, + but it can be extended in the future. + + """ + con = get_server_api_connection() + return con.get_entity_list_folders_raw( + project_name=project_name, + ) + + +def get_entity_list_folders( + project_name: str, +) -> list[dict[str, Any]]: + """Get entity list folders. + + Returns: + list[dict[str, Any]]: List of entity list folders. + + """ + con = get_server_api_connection() + return con.get_entity_list_folders( + project_name=project_name, + ) + + +def create_entity_list_folder( + project_name: str, + label: str, + *, + parent_id: str | None = None, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, + entity_list_folder_id: str | None = None, +) -> str: + """Create entity list folder. + + Args: + project_name (str): Project name. + label (str): Folder label. + parent_id (str | None): Parent folder id. If None, the folder will + be created in root. + color (str | None): Folder color. + icon (str | None): Folder icon. + scope (list[EntityListScope] | None): Folder scope. Empty list can + be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. + entity_list_folder_id (str | None): Id of folder that will be + created. If None, a new id will be generated. + + Returns: + str: Created entity list folder id. + + """ + con = get_server_api_connection() + return con.create_entity_list_folder( + project_name=project_name, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, + data=data, + access=access, + entity_list_folder_id=entity_list_folder_id, + ) + + +def update_entity_list_folder( + project_name: str, + entity_list_folder_id: str, + *, + label: str | None = None, + parent_id: str | None | type[NOT_SET] = NOT_SET, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, +) -> None: + """Update entity list folder. + + Args: + project_name (str): Project name. + entity_list_folder_id (str): Folder id that will be updated. + label (str | None): New label of entity list folder. + parent_id (str | None | type[NOT_SET]): New parent id of entity + list folder. If None, the folder will be moved to root. + color (str | None): New color of entity list folder. + icon (str | None): New icon of entity list folder. + scope (list[EntityListScope] | None): New scope of entity list + folder. Empty list can be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. + + """ + con = get_server_api_connection() + return con.update_entity_list_folder( + project_name=project_name, + entity_list_folder_id=entity_list_folder_id, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, + data=data, + access=access, + ) + + +def delete_entity_list_folder( + project_name: str, + entity_list_folder_id: str, +) -> None: + """Delete entity list folder. + """ + con = get_server_api_connection() + return con.delete_entity_list_folder( + project_name=project_name, + entity_list_folder_id=entity_list_folder_id, + ) + + +def set_entity_list_folders_order( + project_name: str, + order: list[str], +) -> None: + """Change order of entity list folders. + + Args: + project_name (str): Project name. + order (list[str]): List of folder ids in desired order. + + """ + con = get_server_api_connection() + return con.set_entity_list_folders_order( + project_name=project_name, + order=order, + ) + + def get_thumbnail_by_id( project_name: str, thumbnail_id: str, diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index b2b8d0973..a50b21817 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -15,11 +15,7 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from typing import TypedDict - from ayon_api.typing import LinkDirection - - class CreateLinkData(TypedDict): - id: str + from ayon_api.typing import LinkDirection, CreateLinkData class LinksAPI(BaseServerAPI): diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 17827266d..329ccdf66 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -4,16 +4,18 @@ import typing from typing import Optional, Iterable, Any, Generator -from ayon_api.utils import create_entity_id +from ayon_api.utils import NOT_SET, create_entity_id from ayon_api.graphql_queries import entity_lists_graphql_query from .base import BaseServerAPI + if typing.TYPE_CHECKING: from ayon_api.typing import ( EntityListEntityType, EntityListAttributeDefinitionDict, EntityListItemMode, + EntityListScope, ) @@ -159,6 +161,7 @@ def create_entity_list( data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, + entity_list_folder_id: Optional[str] = None, owner: Optional[str] = None, active: Optional[bool] = None, items: Optional[list[dict[str, Any]]] = None, @@ -178,6 +181,7 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. + entity_list_folder_id (Optional[str]): Entity list folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -199,6 +203,7 @@ def create_entity_list( ("template", template), ("tags", tags), ("owner", owner), + ("entityListFolderId", entity_list_folder_id), ("data", data), ("active", active), ("items", items), @@ -223,6 +228,7 @@ def update_entity_list( attrib: Optional[list[dict[str, Any]]] = None, data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, + entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -237,6 +243,9 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. + entity_list_folder_id (str | None | type[NOT_SET]): New entity + list folder id. Use ``None`` to move entity list to root. + Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. @@ -254,6 +263,9 @@ def update_entity_list( ) if value is not None } + if entity_list_folder_id is not NOT_SET: + kwargs["entityListFolderId"] = entity_list_folder_id + response = self.patch( f"projects/{project_name}/lists/{list_id}", **kwargs @@ -454,3 +466,212 @@ def delete_entity_list_item( f"projects/{project_name}/lists/{list_id}/items/{item_id}", ) response.raise_for_status() + + def get_entity_list_entities( + self, project_name: str, entity_list_id: str + ) -> dict[str, Any]: + """Get entity list items using REST API. + + Args: + project_name (str): Project name. + entity_list_id (str): Entity list id. + + Returns: + dict[str, Any]: Information about entities on the list. + + """ + response = self.get( + f"projects/{project_name}/lists/{entity_list_id}/entities" + ) + response.raise_for_status() + return response.data + + def get_entity_list_folders_raw( + self, project_name: str + ) -> dict[str, Any]: + """Get entity list folders. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Raw output of entity list folders output. At this + moment contains only "folders" key with list of folders, + but it can be extended in the future. + + """ + response = self.get(f"projects/{project_name}/entityListFolders") + response.raise_for_status() + return response.data + + def get_entity_list_folders( + self, project_name: str + ) -> list[dict[str, Any]]: + """Get entity list folders. + + Returns: + list[dict[str, Any]]: List of entity list folders. + + """ + data = self.get_entity_list_folders_raw(project_name) + return data["folders"] + + def create_entity_list_folder( + self, + project_name: str, + label: str, + *, + parent_id: str | None = None, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, + entity_list_folder_id: str | None = None, + ) -> str: + """Create entity list folder. + + Args: + project_name (str): Project name. + label (str): Folder label. + parent_id (str | None): Parent folder id. If None, the folder will + be created in root. + color (str | None): Folder color. + icon (str | None): Folder icon. + scope (list[EntityListScope] | None): Folder scope. Empty list can + be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. + entity_list_folder_id (str | None): Id of folder that will be + created. If None, a new id will be generated. + + Returns: + str: Created entity list folder id. + + """ + if data is None: + data = {} + + for key, value in ( + ("color", color), + ("icon", icon), + ("scope", scope), + ): + if value: + data[key] = value + + if not entity_list_folder_id: + entity_list_folder_id = create_entity_id() + body = { + "id": entity_list_folder_id, + "label": label, + } + if parent_id: + body["parentId"] = parent_id + + if data: + body["data"] = data + + if access: + body["access"] = access + + response = self.post( + f"projects/{project_name}/entityListFolders", + **body + ) + response.raise_for_status() + return entity_list_folder_id + + def update_entity_list_folder( + self, + project_name: str, + entity_list_folder_id: str, + *, + label: str | None = None, + parent_id: str | None | type[NOT_SET] = NOT_SET, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, + ) -> None: + """Update entity list folder. + + Args: + project_name (str): Project name. + entity_list_folder_id (str): Folder id that will be updated. + label (str | None): New label of entity list folder. + parent_id (str | None | type[NOT_SET]): New parent id of entity + list folder. If None, the folder will be moved to root. + color (str | None): New color of entity list folder. + icon (str | None): New icon of entity list folder. + scope (list[EntityListScope] | None): New scope of entity list + folder. Empty list can be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. + + """ + if data is None: + data = {} + + for key, value in ( + ("color", color), + ("icon", icon), + ): + if value: + data[key] = value + + if scope is not None: + data["scope"] = scope + + body = {} + if data: + body["data"] = data + if label: + body["label"] = label + if access is not None: + body["access"] = access + if parent_id is not NOT_SET: + body["parentId"] = parent_id + + if not body: + return + + response = self.patch( + ( + f"projects/{project_name}/" + f"entityListFolders/{entity_list_folder_id}" + ), + **body + ) + response.raise_for_status() + + def delete_entity_list_folder( + self, + project_name: str, + entity_list_folder_id: str, + ) -> None: + """Delete entity list folder.""" + response = self.delete( + f"projects/{project_name}/" + f"entityListFolders/{entity_list_folder_id}" + ) + response.raise_for_status() + + def set_entity_list_folders_order( + self, project_name: str, order: list[str] + ) -> None: + """Change order of entity list folders. + + Args: + project_name (str): Project name. + order (list[str]): List of folder ids in desired order. + + """ + response = self.post( + f"projects/{project_name}/entityListFolders/order", + order=order, + ) + response.raise_for_status() diff --git a/ayon_api/typing.py b/ayon_api/typing.py index f8016524f..e620de085 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -47,6 +47,11 @@ "delete", ] +EntityListScope = Literal[ + "generic", + "review-session", +] + EventFilterValueType = Union[ None, str, int, float, @@ -123,6 +128,10 @@ class BackgroundOperationTask(TypedDict): LinkDirection = Literal["in", "out"] +class CreateLinkData(TypedDict): + id: str + + class AttributeEnumItemDict(TypedDict): value: Union[str, int, float, bool] label: str