diff --git a/automated_api.py b/automated_api.py index 16b3ed9cb..9565717bd 100644 --- a/automated_api.py +++ b/automated_api.py @@ -20,7 +20,6 @@ import typing # Fake modules to avoid import errors - requests = type(sys)("requests") requests.__dict__["Response"] = type( "Response", (), {"__module__": "requests"} @@ -29,14 +28,7 @@ sys.modules["requests"] = requests sys.modules["unidecode"] = type(sys)("unidecode") -import ayon_api # noqa: E402 -from ayon_api.server_api import ( # noqa: E402 - ServerAPI, - _PLACEHOLDER, - _ActionsAPI, - _ListsAPI, -) -from ayon_api.utils import NOT_SET # noqa: E402 +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) EXCLUDED_METHODS = { "get_default_service_username", @@ -125,34 +117,38 @@ def prepare_docstring(func): return f'"""{docstring}{line_char}\n"""' +def _find_obj(obj_full, api_globals): + parts = list(reversed(obj_full.split("."))) + _name = None + for part in parts: + if _name is None: + _name = part + else: + _name = f"{part}.{_name}" + try: + # Test if typehint is valid for known '_api' content + exec(f"_: {_name} = None", api_globals) + return _name + except NameError: + pass + return None + + def _get_typehint(annotation, api_globals): + if isinstance(annotation, str): + annotation = annotation.replace("'", "") + if inspect.isclass(annotation): - module_name_parts = list(str(annotation.__module__).split(".")) - module_name_parts.append(annotation.__name__) - module_name_parts.reverse() - options = [] - _name = None - for name in module_name_parts: - if _name is None: - _name = name - options.append(name) - else: - _name = f"{name}.{_name}" - options.append(_name) - - options.reverse() - for option in options: - try: - # Test if typehint is valid for known '_api' content - exec(f"_: {option} = None", api_globals) - return option - except NameError: - pass - - typehint = options[0] - print("Unknown typehint:", typehint) - typehint = f'"{typehint}"' - return typehint + module_name = str(annotation.__module__) + full_name = annotation.__name__ + if module_name: + full_name = f"{module_name}.{full_name}" + obj_name = _find_obj(full_name, api_globals) + if obj_name is not None: + return obj_name + + print("Unknown typehint:", full_name) + return full_name typehint = ( str(annotation) @@ -161,9 +157,15 @@ def _get_typehint(annotation, api_globals): full_path_regex = re.compile( r"(?P(?P[a-zA-Z0-9_\.]+))" ) + for item in full_path_regex.finditer(str(typehint)): groups = item.groupdict() - name = groups["name"].split(".")[-1] + name = groups["name"] + obj_name = _find_obj(name, api_globals) + if obj_name: + name = obj_name + else: + name = name.split(".")[-1] typehint = typehint.replace(groups["full"], name) forwardref_regex = re.compile( @@ -171,15 +173,51 @@ def _get_typehint(annotation, api_globals): ) for item in forwardref_regex.finditer(str(typehint)): groups = item.groupdict() - name = groups["name"].split(".")[-1] - typehint = typehint.replace(groups["full"], f'"{name}"') + name = groups["name"] + obj_name = _find_obj(name, api_globals) + if obj_name: + name = obj_name + else: + name = name.split(".")[-1] + typehint = typehint.replace(groups["full"], name) try: # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) + return typehint except NameError: print("Unknown typehint:", typehint) - typehint = f'"{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 @@ -197,6 +235,9 @@ def _add_typehint(param_name, param, api_globals): def _kw_default_to_str(param_name, param, api_globals): + from ayon_api._api_helpers.base import _PLACEHOLDER + from ayon_api.utils import NOT_SET + if param.default is inspect.Parameter.empty: return _add_typehint(param_name, param, api_globals) @@ -292,11 +333,54 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): def prepare_api_functions(api_globals): + from ayon_api.server_api import ( # noqa: E402 + ServerAPI, + InstallersAPI, + DependencyPackagesAPI, + SecretsAPI, + BundlesAddonsAPI, + EventsAPI, + AttributesAPI, + ProjectsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + RepresentationsAPI, + WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, + LinksAPI, + ListsAPI, + ) + functions = [] _items = list(ServerAPI.__dict__.items()) - _items.extend(_ActionsAPI.__dict__.items()) - _items.extend(_ListsAPI.__dict__.items()) + _items.extend(InstallersAPI.__dict__.items()) + _items.extend(DependencyPackagesAPI.__dict__.items()) + _items.extend(SecretsAPI.__dict__.items()) + _items.extend(ActionsAPI.__dict__.items()) + _items.extend(ActivitiesAPI.__dict__.items()) + _items.extend(BundlesAddonsAPI.__dict__.items()) + _items.extend(EventsAPI.__dict__.items()) + _items.extend(AttributesAPI.__dict__.items()) + _items.extend(ProjectsAPI.__dict__.items()) + _items.extend(FoldersAPI.__dict__.items()) + _items.extend(TasksAPI.__dict__.items()) + _items.extend(ProductsAPI.__dict__.items()) + _items.extend(VersionsAPI.__dict__.items()) + _items.extend(RepresentationsAPI.__dict__.items()) + _items.extend(WorkfilesAPI.__dict__.items()) + _items.extend(LinksAPI.__dict__.items()) + _items.extend(ListsAPI.__dict__.items()) + _items.extend(ThumbnailsAPI.__dict__.items()) + + processed = set() for attr_name, attr in _items: + if attr_name in processed: + continue + processed.add(attr_name) if ( attr_name.startswith("_") or attr_name in EXCLUDED_METHODS @@ -334,10 +418,7 @@ def prepare_api_functions(api_globals): def main(): print("Creating public API functions based on ServerAPI methods") # TODO order methods in some order - dirpath = os.path.dirname(os.path.dirname( - os.path.abspath(ayon_api.__file__) - )) - ayon_api_root = os.path.join(dirpath, "ayon_api") + ayon_api_root = os.path.join(CURRENT_DIR, "ayon_api") init_filepath = os.path.join(ayon_api_root, "__init__.py") api_filepath = os.path.join(ayon_api_root, "_api.py") @@ -361,15 +442,13 @@ def main(): # Read content of first part of `_api.py` to get global variables # - disable type checking so imports done only during typechecking are # not executed - old_value = typing.TYPE_CHECKING typing.TYPE_CHECKING = False 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) - typing.TYPE_CHECKING = old_value - # print(api_globals) print("(3/5) Preparing functions body based on 'ServerAPI' class") result = prepare_api_functions(api_globals) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 719c54a1c..a51f12bc4 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -1,5 +1,6 @@ from .version import __version__ from .utils import ( + RequestTypes, TransferProgress, slugify_string, create_dependency_package_basename, @@ -12,7 +13,6 @@ SortOrder, ) from .server_api import ( - RequestTypes, ServerAPI, ) @@ -67,17 +67,6 @@ patch, get, delete, - get_event, - get_events, - update_event, - dispatch_event, - delete_event, - enroll_event_job, - get_activities, - get_activity_by_id, - create_activity, - update_activity, - delete_activity, download_file_to_stream, download_file, upload_file_from_stream, @@ -88,17 +77,9 @@ get_graphql_schema, get_server_schema, get_schemas, - get_attributes_schema, - reset_attributes_schema, - set_attribute_config, - remove_attribute_config, - get_attributes_for_type, - get_attributes_fields_for_type, get_default_fields_for_type, - get_addons_info, - get_addon_endpoint, - get_addon_url, - download_addon_private_file, + get_rest_entity_by_id, + send_batch_operations, get_installers, create_installer, update_installer, @@ -111,25 +92,34 @@ delete_dependency_package, download_dependency_package, upload_dependency_package, - delete_addon, - delete_addon_version, - upload_addon_zip, + get_secrets, + get_secret, + save_secret, + delete_secret, + get_actions, + trigger_action, + get_action_config, + set_action_config, + take_action, + abort_action, + get_activities, + get_activity_by_id, + create_activity, + update_activity, + delete_activity, + send_activities_batch_operations, get_bundles, create_bundle, update_bundle, check_bundle_compatibility, delete_bundle, - get_project_anatomy_presets, - get_default_anatomy_preset_name, - get_project_anatomy_preset, - get_built_in_anatomy_preset, - get_build_in_anatomy_preset, - get_project_root_overrides, - get_project_roots_by_site, - get_project_root_overrides_by_site_id, - get_project_roots_for_site, - get_project_roots_by_site_id, - get_project_roots_by_platform, + get_addon_endpoint, + get_addons_info, + get_addon_url, + delete_addon, + delete_addon_version, + upload_addon_zip, + download_addon_private_file, get_addon_settings_schema, get_addon_site_settings_schema, get_addon_studio_settings, @@ -140,22 +130,40 @@ get_addons_studio_settings, get_addons_project_settings, get_addons_settings, - get_secrets, - get_secret, - save_secret, - delete_secret, + get_event, + get_events, + update_event, + dispatch_event, + create_event, + delete_event, + enroll_event_job, + get_attributes_schema, + reset_attributes_schema, + set_attribute_config, + remove_attribute_config, + get_attributes_for_type, + get_attributes_fields_for_type, + get_project_anatomy_presets, + get_default_anatomy_preset_name, + get_project_anatomy_preset, + get_built_in_anatomy_preset, + get_build_in_anatomy_preset, get_rest_project, get_rest_projects, - get_rest_entity_by_id, - get_rest_folder, - get_rest_folders, - get_rest_task, - get_rest_product, - get_rest_version, - get_rest_representation, get_project_names, get_projects, get_project, + create_project, + update_project, + delete_project, + get_project_root_overrides, + get_project_roots_by_site, + get_project_root_overrides_by_site_id, + get_project_roots_for_site, + get_project_roots_by_site_id, + get_project_roots_by_platform, + get_rest_folder, + get_rest_folders, get_folders_hierarchy, get_folders_rest, get_folders, @@ -166,6 +174,7 @@ create_folder, update_folder, delete_folder, + get_rest_task, get_tasks, get_task_by_name, get_task_by_id, @@ -175,6 +184,7 @@ create_task, update_task, delete_task, + get_rest_product, get_products, get_product_by_id, get_product_by_name, @@ -184,6 +194,7 @@ create_product, update_product, delete_product, + get_rest_version, get_versions, get_version_by_id, get_version_by_name, @@ -197,6 +208,7 @@ create_version, update_version, delete_version, + get_rest_representation, get_representations, get_representation_by_id, get_representation_by_name, @@ -211,17 +223,6 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, - get_thumbnail_by_id, - get_thumbnail, - get_folder_thumbnail, - get_task_thumbnail, - get_version_thumbnail, - get_workfile_thumbnail, - create_thumbnail, - update_thumbnail, - create_project, - update_project, - delete_project, get_full_link_type_name, get_link_types, get_link_type, @@ -241,14 +242,6 @@ get_version_links, get_representations_links, get_representation_links, - send_batch_operations, - send_activities_batch_operations, - get_actions, - trigger_action, - get_action_config, - set_action_config, - take_action, - abort_action, get_entity_lists, get_entity_list_rest, get_entity_list_by_id, @@ -261,12 +254,21 @@ update_entity_list_items, update_entity_list_item, delete_entity_list_item, + get_thumbnail_by_id, + get_thumbnail, + get_folder_thumbnail, + get_task_thumbnail, + get_version_thumbnail, + get_workfile_thumbnail, + create_thumbnail, + update_thumbnail, ) __all__ = ( "__version__", + "RequestTypes", "TransferProgress", "slugify_string", "create_dependency_package_basename", @@ -278,7 +280,6 @@ "abort_web_action_event", "SortOrder", - "RequestTypes", "ServerAPI", "GlobalServerAPI", @@ -331,17 +332,6 @@ "patch", "get", "delete", - "get_event", - "get_events", - "update_event", - "dispatch_event", - "delete_event", - "enroll_event_job", - "get_activities", - "get_activity_by_id", - "create_activity", - "update_activity", - "delete_activity", "download_file_to_stream", "download_file", "upload_file_from_stream", @@ -352,17 +342,9 @@ "get_graphql_schema", "get_server_schema", "get_schemas", - "get_attributes_schema", - "reset_attributes_schema", - "set_attribute_config", - "remove_attribute_config", - "get_attributes_for_type", - "get_attributes_fields_for_type", "get_default_fields_for_type", - "get_addons_info", - "get_addon_endpoint", - "get_addon_url", - "download_addon_private_file", + "get_rest_entity_by_id", + "send_batch_operations", "get_installers", "create_installer", "update_installer", @@ -375,25 +357,34 @@ "delete_dependency_package", "download_dependency_package", "upload_dependency_package", - "delete_addon", - "delete_addon_version", - "upload_addon_zip", + "get_secrets", + "get_secret", + "save_secret", + "delete_secret", + "get_actions", + "trigger_action", + "get_action_config", + "set_action_config", + "take_action", + "abort_action", + "get_activities", + "get_activity_by_id", + "create_activity", + "update_activity", + "delete_activity", + "send_activities_batch_operations", "get_bundles", "create_bundle", "update_bundle", "check_bundle_compatibility", "delete_bundle", - "get_project_anatomy_presets", - "get_default_anatomy_preset_name", - "get_project_anatomy_preset", - "get_built_in_anatomy_preset", - "get_build_in_anatomy_preset", - "get_project_root_overrides", - "get_project_roots_by_site", - "get_project_root_overrides_by_site_id", - "get_project_roots_for_site", - "get_project_roots_by_site_id", - "get_project_roots_by_platform", + "get_addon_endpoint", + "get_addons_info", + "get_addon_url", + "delete_addon", + "delete_addon_version", + "upload_addon_zip", + "download_addon_private_file", "get_addon_settings_schema", "get_addon_site_settings_schema", "get_addon_studio_settings", @@ -404,22 +395,40 @@ "get_addons_studio_settings", "get_addons_project_settings", "get_addons_settings", - "get_secrets", - "get_secret", - "save_secret", - "delete_secret", + "get_event", + "get_events", + "update_event", + "dispatch_event", + "create_event", + "delete_event", + "enroll_event_job", + "get_attributes_schema", + "reset_attributes_schema", + "set_attribute_config", + "remove_attribute_config", + "get_attributes_for_type", + "get_attributes_fields_for_type", + "get_project_anatomy_presets", + "get_default_anatomy_preset_name", + "get_project_anatomy_preset", + "get_built_in_anatomy_preset", + "get_build_in_anatomy_preset", "get_rest_project", "get_rest_projects", - "get_rest_entity_by_id", - "get_rest_folder", - "get_rest_folders", - "get_rest_task", - "get_rest_product", - "get_rest_version", - "get_rest_representation", "get_project_names", "get_projects", "get_project", + "create_project", + "update_project", + "delete_project", + "get_project_root_overrides", + "get_project_roots_by_site", + "get_project_root_overrides_by_site_id", + "get_project_roots_for_site", + "get_project_roots_by_site_id", + "get_project_roots_by_platform", + "get_rest_folder", + "get_rest_folders", "get_folders_hierarchy", "get_folders_rest", "get_folders", @@ -430,6 +439,7 @@ "create_folder", "update_folder", "delete_folder", + "get_rest_task", "get_tasks", "get_task_by_name", "get_task_by_id", @@ -439,6 +449,7 @@ "create_task", "update_task", "delete_task", + "get_rest_product", "get_products", "get_product_by_id", "get_product_by_name", @@ -448,6 +459,7 @@ "create_product", "update_product", "delete_product", + "get_rest_version", "get_versions", "get_version_by_id", "get_version_by_name", @@ -461,6 +473,7 @@ "create_version", "update_version", "delete_version", + "get_rest_representation", "get_representations", "get_representation_by_id", "get_representation_by_name", @@ -475,17 +488,6 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", - "get_thumbnail_by_id", - "get_thumbnail", - "get_folder_thumbnail", - "get_task_thumbnail", - "get_version_thumbnail", - "get_workfile_thumbnail", - "create_thumbnail", - "update_thumbnail", - "create_project", - "update_project", - "delete_project", "get_full_link_type_name", "get_link_types", "get_link_type", @@ -505,14 +507,6 @@ "get_version_links", "get_representations_links", "get_representation_links", - "send_batch_operations", - "send_activities_batch_operations", - "get_actions", - "trigger_action", - "get_action_config", - "set_action_config", - "take_action", - "abort_action", "get_entity_lists", "get_entity_list_rest", "get_entity_list_by_id", @@ -525,4 +519,12 @@ "update_entity_list_items", "update_entity_list_item", "delete_entity_list_item", + "get_thumbnail_by_id", + "get_thumbnail", + "get_folder_thumbnail", + "get_task_thumbnail", + "get_version_thumbnail", + "get_workfile_thumbnail", + "create_thumbnail", + "update_thumbnail", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 585c6d44d..500955995 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -9,23 +9,21 @@ automatically, and changing them manually can cause issues. """ +from __future__ import annotations + import os import socket import typing -from typing import Optional, Set, List, Tuple, Dict, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any + import requests from .constants import ( SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY, ) -from .server_api import ( - ServerAPI, - RequestType, - GraphQlResponse, - _PLACEHOLDER, -) from .exceptions import FailedServiceInit +from ._api_helpers.base import _PLACEHOLDER from .utils import ( NOT_SET, SortOrder, @@ -34,17 +32,26 @@ get_default_settings_variant as _get_default_settings_variant, RepresentationParents, RepresentationHierarchy, + RestApiResponse, +) +from .server_api import ( + ServerAPI, + RequestType, + GraphQlResponse, ) if typing.TYPE_CHECKING: from typing import Union from .typing import ( + ServerVersion, ActivityType, ActivityReferenceType, EntityListEntityType, EntityListItemMode, LinkDirection, EventFilter, + EventStatus, + EnrollEventData, AttributeScope, AttributeSchemaDataDict, AttributeSchemaDict, @@ -76,6 +83,7 @@ StreamType, EntityListAttributeDefinitionDict, ) + from ._api_helpers.links import CreateLinkData class GlobalServerAPI(ServerAPI): @@ -306,7 +314,7 @@ def get_service_addon_settings(project_name=None): project_name (Optional[str]): Project name. Returns: - Dict[str, Any]: Addon settings. + dict[str, Any]: Addon settings. Raises: ValueError: When service was not initialized. @@ -665,7 +673,7 @@ def set_sender_type( ) -def get_info() -> Dict[str, Any]: +def get_info() -> dict[str, Any]: """Get information about current used api key. By default, the 'info' contains only 'uptime' and 'version'. With @@ -696,7 +704,7 @@ def get_server_version() -> str: return con.get_server_version() -def get_server_version_tuple() -> Tuple[int, int, int, str, str]: +def get_server_version_tuple() -> ServerVersion: """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -704,8 +712,7 @@ def get_server_version_tuple() -> Tuple[int, int, int, str, str]: This function only returns first three numbers of version. Returns: - Tuple[int, int, int, Union[str, None], Union[str, None]]: Server - version. + ServerVersion: Server version. """ con = get_server_api_connection() @@ -717,7 +724,7 @@ def get_users( usernames: Optional[Iterable[str]] = None, emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: +) -> Generator[dict[str, Any], None, None]: """Get Users. Only administrators and managers can fetch all users. For other users @@ -747,7 +754,7 @@ def get_user_by_name( username: str, project_name: Optional[str] = None, fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -774,7 +781,7 @@ def get_user_by_name( def get_user( username: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. @@ -783,7 +790,7 @@ def get_user( username (Optional[str]): Username. Returns: - Optional[Dict[str, Any]]: User info or None if user is not + Optional[dict[str, Any]]: User info or None if user is not found. """ @@ -903,1408 +910,1226 @@ def delete( ) -def get_event( - event_id: str, -) -> Optional[Dict[str, Any]]: - """Query full event data by id. +def download_file_to_stream( + endpoint: str, + stream: StreamType, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download file from AYON server to IOStream. - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. + Endpoint can be full url (must start with 'base_url' of api object). - Args: - event_id (str): Event id. + Progress object can be used to track download. Can be used when + download happens in thread and other thread want to catch changes over + time. - Returns: - dict[str, Any]: Full event data. + Todos: + Use retries and timeout. + Return RestApiResponse. + + Args: + endpoint (str): Endpoint or URL to file that should be downloaded. + stream (StreamType): Stream where output will + be stored. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. """ con = get_server_api_connection() - return con.get_event( - event_id=event_id, + return con.download_file_to_stream( + endpoint=endpoint, + stream=stream, + chunk_size=chunk_size, + progress=progress, ) -def get_events( - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: - """Get events from server with filtering options. - - Notes: - Not all event happen on a project. - - Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. - fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending - or descending order. It is recommended to set 'limit' - when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. - - Returns: - Generator[dict[str, Any]]: Available events matching filters. +def download_file( + endpoint: str, + filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download file from AYON server. - """ - con = get_server_api_connection() - return con.get_events( - topics=topics, - event_ids=event_ids, - project_names=project_names, - statuses=statuses, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields, - limit=limit, - order=order, - states=states, - ) + Endpoint can be full url (must start with 'base_url' of api object). + Progress object can be used to track download. Can be used when + download happens in thread and other thread want to catch changes over + time. -def update_event( - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, -): - """Update event data. + Todos: + Use retries and timeout. + Return RestApiResponse. Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. + endpoint (str): Endpoint or URL to file that should be downloaded. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. """ con = get_server_api_connection() - return con.update_event( - event_id=event_id, - sender=sender, - project_name=project_name, - username=username, - status=status, - description=description, - summary=summary, - payload=payload, + return con.download_file( + endpoint=endpoint, + filepath=filepath, + chunk_size=chunk_size, progress=progress, - retries=retries, ) -def dispatch_event( - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[List[str]] = None, -): - """Dispatch event to server. +def upload_file_from_stream( + endpoint: str, + stream: StreamType, + progress: Optional[TransferProgress] = None, + request_type: Optional[RequestType] = None, + **kwargs, +) -> requests.Response: + """Upload file to server from bytes. + + Todos: + Use retries and timeout. + Return RestApiResponse. Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. + endpoint (str): Endpoint or url where file will be uploaded. + stream (StreamType): File content stream. + progress (Optional[TransferProgress]): Object that gives ability + to track upload progress. + request_type (Optional[RequestType]): Type of request that will + be used to upload file. + **kwargs (Any): Additional arguments that will be passed + to request function. Returns: - RestApiResponse: Response from server. + requests.Response: Response object """ con = get_server_api_connection() - return con.dispatch_event( - topic=topic, - sender=sender, - event_hash=event_hash, - project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + return con.upload_file_from_stream( + endpoint=endpoint, + stream=stream, + progress=progress, + request_type=request_type, + **kwargs, ) -def delete_event( - event_id: str, -): - """Delete event by id. +def upload_file( + endpoint: str, + filepath: str, + progress: Optional[TransferProgress] = None, + request_type: Optional[RequestType] = None, + **kwargs, +) -> requests.Response: + """Upload file to server. - Supported since AYON server 1.6.0. + Todos: + Use retries and timeout. + Return RestApiResponse. Args: - event_id (str): Event id. - - Returns: - RestApiResponse: Response from server. - - """ + endpoint (str): Endpoint or url where file will be uploaded. + filepath (str): Source filepath. + progress (Optional[TransferProgress]): Object that gives ability + to track upload progress. + request_type (Optional[RequestType]): Type of request that will + be used to upload file. + **kwargs (Any): Additional arguments that will be passed + to request function. + + Returns: + requests.Response: Response object + + """ con = get_server_api_connection() - return con.delete_event( - event_id=event_id, + return con.upload_file( + endpoint=endpoint, + filepath=filepath, + progress=progress, + request_type=request_type, + **kwargs, ) -def enroll_event_job( - source_topic: "Union[str, List[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, -): - """Enroll job based on events. +def upload_reviewable( + project_name: str, + version_id: str, + filepath: str, + label: Optional[str] = None, + content_type: Optional[str] = None, + filename: Optional[str] = None, + progress: Optional[TransferProgress] = None, + headers: Optional[dict[str, Any]] = None, + **kwargs, +) -> requests.Response: + """Upload reviewable file to server. - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. + Args: + project_name (str): Project name. + version_id (str): Version id. + filepath (str): Reviewable file path to upload. + label (Optional[str]): Reviewable label. Filled automatically + server side with filename. + content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): User as original filename. Filename from + 'filepath' is used when not filled. + progress (Optional[TransferProgress]): Progress. + headers (Optional[dict[str, Any]]): Headers. - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. + Returns: + requests.Response: Server response. - Make sure the new event has updated status to '"finished"' status - when you're done with logic + """ + con = get_server_api_connection() + return con.upload_reviewable( + project_name=project_name, + version_id=version_id, + filepath=filepath, + label=label, + content_type=content_type, + filename=filename, + progress=progress, + headers=headers, + **kwargs, + ) - Target topic should not clash with other processes/services. - Created target event have 'dependsOn' key where is id of source topic. +def trigger_server_restart(): + """Trigger server restart. - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' + Restart may be required when a change of specific value happened on + server. + + """ + con = get_server_api_connection() + return con.trigger_server_restart() + + +def query_graphql( + query: str, + variables: Optional[dict[str, Any]] = None, +) -> GraphQlResponse: + """Execute GraphQl query. Args: - source_topic (Union[str, List[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[List[str]]): Ignore events triggered - by given sender types. + query (str): GraphQl query string. + variables (Optional[dict[str, Any]): Variables that can be + used in query. Returns: - Union[None, dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. + GraphQlResponse: Response from server. """ con = get_server_api_connection() - return con.enroll_event_job( - source_topic=source_topic, - target_topic=target_topic, - sender=sender, - description=description, - sequential=sequential, - events_filter=events_filter, - max_retries=max_retries, - ignore_older_than=ignore_older_than, - ignore_sender_types=ignore_sender_types, + return con.query_graphql( + query=query, + variables=variables, ) -def get_activities( - project_name: str, - activity_ids: Optional[Iterable[str]] = None, - activity_types: Optional[Iterable["ActivityType"]] = None, - entity_ids: Optional[Iterable[str]] = None, - entity_names: Optional[Iterable[str]] = None, - entity_type: Optional[str] = None, - changed_after: Optional[str] = None, - changed_before: Optional[str] = None, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, -) -> Generator[Dict[str, Any], None, None]: - """Get activities from server with filtering options. +def get_graphql_schema() -> dict[str, Any]: + con = get_server_api_connection() + return con.get_graphql_schema() - Args: - project_name (str): Project on which activities happened. - activity_ids (Optional[Iterable[str]]): Activity ids. - activity_types (Optional[Iterable[ActivityType]]): Activity types. - entity_ids (Optional[Iterable[str]]): Entity ids. - entity_names (Optional[Iterable[str]]): Entity names. - entity_type (Optional[str]): Entity type. - changed_after (Optional[str]): Return only activities changed - after given iso datetime string. - changed_before (Optional[str]): Return only activities changed - before given iso datetime string. - reference_types (Optional[Iterable[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. + +def get_server_schema() -> Optional[dict[str, Any]]: + """Get server schema with info, url paths, components etc. + + Todos: + Cache schema - How to find out it is outdated? Returns: - Generator[dict[str, Any]]: Available activities matching filters. + dict[str, Any]: Full server schema. """ con = get_server_api_connection() - return con.get_activities( - project_name=project_name, - activity_ids=activity_ids, - activity_types=activity_types, - entity_ids=entity_ids, - entity_names=entity_names, - entity_type=entity_type, - changed_after=changed_after, - changed_before=changed_before, - reference_types=reference_types, - fields=fields, - limit=limit, - order=order, - ) + return con.get_server_schema() -def get_activity_by_id( - project_name: str, - activity_id: str, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: - """Get activity by id. +def get_schemas() -> dict[str, Any]: + """Get components schema. - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. + Name of components does not match entity type names e.g. 'project' is + under 'ProjectModel'. We should find out some mapping. Also, there + are properties which don't have information about reference to object + e.g. 'config' has just object definition without reference schema. Returns: - Optional[Dict[str, Any]]: Activity data or None if activity is not - found. + dict[str, Any]: Component schemas. """ con = get_server_api_connection() - return con.get_activity_by_id( - project_name=project_name, - activity_id=activity_id, - reference_types=reference_types, - fields=fields, - ) + return con.get_schemas() -def create_activity( - project_name: str, - entity_id: str, +def get_default_fields_for_type( entity_type: str, - activity_type: "ActivityType", - activity_id: Optional[str] = None, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - timestamp: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, -) -> str: - """Create activity on a project. +) -> set[str]: + """Default fields for entity type. + + Returns most of commonly used fields from server. Args: - project_name (str): Project on which activity happened. - entity_id (str): Entity id. - entity_type (str): Entity type. - activity_type (ActivityType): Activity type. - activity_id (Optional[str]): Activity id. - body (Optional[str]): Activity body. - file_ids (Optional[List[str]]): List of file ids attached - to activity. - timestamp (Optional[str]): Activity timestamp. - data (Optional[Dict[str, Any]]): Additional data. + entity_type (str): Name of entity type. Returns: - str: Activity id. + set[str]: Fields that should be queried from server. """ con = get_server_api_connection() - return con.create_activity( - project_name=project_name, - entity_id=entity_id, + return con.get_default_fields_for_type( entity_type=entity_type, - activity_type=activity_type, - activity_id=activity_id, - body=body, - file_ids=file_ids, - timestamp=timestamp, - data=data, ) -def update_activity( +def get_rest_entity_by_id( project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[Dict[str, Any]] = None, -): - """Update activity by id. + entity_type: str, + entity_id: str, +) -> Optional[AnyEntityDict]: + """Get entity using REST on a project by its id. Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - body (str): Activity body. - file_ids (Optional[List[str]]): List of file ids attached - to activity. - append_file_ids (Optional[bool]): Append file ids to existing - list of file ids. - data (Optional[Dict[str, Any]]): Update data in activity. + project_name (str): Name of project where entity is. + entity_type (Literal["folder", "task", "product", "version"]): The + entity type which should be received. + entity_id (str): Id of entity. + + Returns: + Optional[AnyEntityDict]: Received entity data. """ con = get_server_api_connection() - return con.update_activity( + return con.get_rest_entity_by_id( project_name=project_name, - activity_id=activity_id, - body=body, - file_ids=file_ids, - append_file_ids=append_file_ids, - data=data, + entity_type=entity_type, + entity_id=entity_id, ) -def delete_activity( +def send_batch_operations( project_name: str, - activity_id: str, -): - """Delete activity by id. + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list[dict[str, Any]]: + """Post multiple CRUD operations to server. + + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id to remove. + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. + + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. + + Returns: + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.delete_activity( + return con.send_batch_operations( project_name=project_name, - activity_id=activity_id, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def download_file_to_stream( - endpoint: str, - stream: "StreamType", - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> TransferProgress: - """Download file from AYON server to IOStream. +def get_installers( + version: Optional[str] = None, + platform_name: Optional[str] = None, +) -> InstallersInfoDict: + """Information about desktop application installers on server. - Endpoint can be full url (must start with 'base_url' of api object). + Desktop application installers are helpers to download/update AYON + desktop application for artists. - Progress object can be used to track download. Can be used when - download happens in thread and other thread want to catch changes over - time. + Args: + version (Optional[str]): Filter installers by version. + platform_name (Optional[str]): Filter installers by platform name. - Todos: - Use retries and timeout. - Return RestApiResponse. + Returns: + InstallersInfoDict: Information about installers known for server. + + """ + con = get_server_api_connection() + return con.get_installers( + version=version, + platform_name=platform_name, + ) + + +def create_installer( + filename: str, + version: str, + python_version: str, + platform_name: str, + python_modules: dict[str, str], + runtime_python_modules: dict[str, str], + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, +) -> None: + """Create new installer information on server. + + This step will create only metadata. Make sure to upload installer + to the server using 'upload_installer' method. + + Runtime python modules are modules that are required to run AYON + desktop application, but are not added to PYTHONPATH for any + subprocess. Args: - endpoint (str): Endpoint or URL to file that should be downloaded. - stream (StreamType): Stream where output will - be stored. - chunk_size (Optional[int]): Size of chunks that are received - in single loop. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + filename (str): Installer filename. + version (str): Version of installer. + python_version (str): Version of Python. + platform_name (str): Name of platform. + python_modules (dict[str, str]): Python modules that are available + in installer. + runtime_python_modules (dict[str, str]): Runtime python modules + that are available in installer. + checksum (str): Installer file checksum. + checksum_algorithm (str): Type of checksum used to create checksum. + file_size (int): File size. + sources (Optional[list[dict[str, Any]]]): List of sources that + can be used to download file. """ con = get_server_api_connection() - return con.download_file_to_stream( - endpoint=endpoint, - stream=stream, - chunk_size=chunk_size, - progress=progress, + return con.create_installer( + filename=filename, + version=version, + python_version=python_version, + platform_name=platform_name, + python_modules=python_modules, + runtime_python_modules=runtime_python_modules, + checksum=checksum, + checksum_algorithm=checksum_algorithm, + file_size=file_size, + sources=sources, ) -def download_file( - endpoint: str, - filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> TransferProgress: - """Download file from AYON server. +def update_installer( + filename: str, + sources: list[dict[str, Any]], +) -> None: + """Update installer information on server. - Endpoint can be full url (must start with 'base_url' of api object). + Args: + filename (str): Installer filename. + sources (list[dict[str, Any]]): List of sources that + can be used to download file. Fully replaces existing sources. - Progress object can be used to track download. Can be used when - download happens in thread and other thread want to catch changes over - time. + """ + con = get_server_api_connection() + return con.update_installer( + filename=filename, + sources=sources, + ) - Todos: - Use retries and timeout. - Return RestApiResponse. + +def delete_installer( + filename: str, +) -> None: + """Delete installer from server. Args: - endpoint (str): Endpoint or URL to file that should be downloaded. - filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received - in single loop. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + filename (str): Installer filename. """ con = get_server_api_connection() - return con.download_file( - endpoint=endpoint, - filepath=filepath, - chunk_size=chunk_size, - progress=progress, + return con.delete_installer( + filename=filename, ) -def upload_file_from_stream( - endpoint: str, - stream: "StreamType", +def download_installer( + filename: str, + dst_filepath: str, + chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, - **kwargs, -) -> requests.Response: - """Upload file to server from bytes. - - Todos: - Use retries and timeout. - Return RestApiResponse. +) -> TransferProgress: + """Download installer file from server. Args: - endpoint (str): Endpoint or url where file will be uploaded. - stream (StreamType): File content stream. + filename (str): Installer filename. + dst_filepath (str): Destination filepath. + chunk_size (Optional[int]): Download chunk size. progress (Optional[TransferProgress]): Object that gives ability - to track upload progress. - request_type (Optional[RequestType]): Type of request that will - be used to upload file. - **kwargs (Any): Additional arguments that will be passed - to request function. + to track download progress. Returns: - requests.Response: Response object + TransferProgress: Progress object. """ con = get_server_api_connection() - return con.upload_file_from_stream( - endpoint=endpoint, - stream=stream, + return con.download_installer( + filename=filename, + dst_filepath=dst_filepath, + chunk_size=chunk_size, progress=progress, - request_type=request_type, - **kwargs, ) -def upload_file( - endpoint: str, - filepath: str, +def upload_installer( + src_filepath: str, + dst_filename: str, progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, - **kwargs, ) -> requests.Response: - """Upload file to server. - - Todos: - Use retries and timeout. - Return RestApiResponse. + """Upload installer file to server. Args: - endpoint (str): Endpoint or url where file will be uploaded. - filepath (str): Source filepath. + src_filepath (str): Source filepath. + dst_filename (str): Destination filename. progress (Optional[TransferProgress]): Object that gives ability - to track upload progress. - request_type (Optional[RequestType]): Type of request that will - be used to upload file. - **kwargs (Any): Additional arguments that will be passed - to request function. + to track download progress. Returns: - requests.Response: Response object + requests.Response: Response object. """ con = get_server_api_connection() - return con.upload_file( - endpoint=endpoint, - filepath=filepath, + return con.upload_installer( + src_filepath=src_filepath, + dst_filename=dst_filename, progress=progress, - request_type=request_type, - **kwargs, ) -def upload_reviewable( - project_name: str, - version_id: str, - filepath: str, - label: Optional[str] = None, - content_type: Optional[str] = None, - filename: Optional[str] = None, - progress: Optional[TransferProgress] = None, - headers: Optional[Dict[str, Any]] = None, - **kwargs, -) -> requests.Response: - """Upload reviewable file to server. +def get_dependency_packages() -> DependencyPackagesDict: + """Information about dependency packages on server. - Args: - project_name (str): Project name. - version_id (str): Version id. - filepath (str): Reviewable file path to upload. - label (Optional[str]): Reviewable label. Filled automatically - server side with filename. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): User as original filename. Filename from - 'filepath' is used when not filled. - progress (Optional[TransferProgress]): Progress. - headers (Optional[Dict[str, Any]]): Headers. + To download dependency package, use 'download_dependency_package' + method and pass in 'filename'. + + Example data structure:: + + { + "packages": [ + { + "filename": str, + "platform": str, + "checksum": str, + "checksumAlgorithm": str, + "size": int, + "sources": list[dict[str, Any]], + "supportedAddons": dict[str, str], + "pythonModules": dict[str, str] + } + ] + } Returns: - requests.Response: Server response. + DependencyPackagesDict: Information about dependency packages + known for server. """ con = get_server_api_connection() - return con.upload_reviewable( - project_name=project_name, - version_id=version_id, - filepath=filepath, - label=label, - content_type=content_type, - filename=filename, - progress=progress, - headers=headers, - **kwargs, - ) + return con.get_dependency_packages() -def trigger_server_restart(): - """Trigger server restart. +def create_dependency_package( + filename: str, + python_modules: dict[str, str], + source_addons: dict[str, str], + installer_version: str, + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, + platform_name: Optional[str] = None, +) -> None: + """Create dependency package on server. - Restart may be required when a change of specific value happened on - server. + The package will be created on a server, it is also required to upload + the package archive file (using :meth:`upload_dependency_package`). - """ - con = get_server_api_connection() - return con.trigger_server_restart() + Args: + filename (str): Filename of dependency package. + python_modules (dict[str, str]): Python modules in dependency + package:: + {"": "", ...} -def query_graphql( - query: str, - variables: Optional[Dict[str, Any]] = None, -) -> GraphQlResponse: - """Execute GraphQl query. + source_addons (dict[str, str]): Name of addons for which is + dependency package created:: - Args: - query (str): GraphQl query string. - variables (Optional[dict[str, Any]): Variables that can be - used in query. + {"": "", ...} - Returns: - GraphQlResponse: Response from server. + installer_version (str): Version of installer for which was + package created. + checksum (str): Checksum of archive file where dependencies are. + checksum_algorithm (str): Algorithm used to calculate checksum. + file_size (Optional[int]): Size of file. + sources (Optional[list[dict[str, Any]]]): Information about + sources from where it is possible to get file. + platform_name (Optional[str]): Name of platform for which is + dependency package targeted. Default value is + current platform. """ con = get_server_api_connection() - return con.query_graphql( - query=query, - variables=variables, + return con.create_dependency_package( + filename=filename, + python_modules=python_modules, + source_addons=source_addons, + installer_version=installer_version, + checksum=checksum, + checksum_algorithm=checksum_algorithm, + file_size=file_size, + sources=sources, + platform_name=platform_name, ) -def get_graphql_schema() -> Dict[str, Any]: - con = get_server_api_connection() - return con.get_graphql_schema() - - -def get_server_schema() -> Optional[Dict[str, Any]]: - """Get server schema with info, url paths, components etc. - - Todos: - Cache schema - How to find out it is outdated? +def update_dependency_package( + filename: str, + sources: list[dict[str, Any]], +) -> None: + """Update dependency package metadata on server. - Returns: - dict[str, Any]: Full server schema. + Args: + filename (str): Filename of dependency package. + sources (list[dict[str, Any]]): Information about + sources from where it is possible to get file. Fully replaces + existing sources. """ con = get_server_api_connection() - return con.get_server_schema() - + return con.update_dependency_package( + filename=filename, + sources=sources, + ) -def get_schemas() -> Dict[str, Any]: - """Get components schema. - Name of components does not match entity type names e.g. 'project' is - under 'ProjectModel'. We should find out some mapping. Also, there - are properties which don't have information about reference to object - e.g. 'config' has just object definition without reference schema. +def delete_dependency_package( + filename: str, + platform_name: Optional[str] = None, +) -> None: + """Remove dependency package for specific platform. - Returns: - dict[str, Any]: Component schemas. + Args: + filename (str): Filename of dependency package. + platform_name (Optional[str]): Deprecated. """ con = get_server_api_connection() - return con.get_schemas() + return con.delete_dependency_package( + filename=filename, + platform_name=platform_name, + ) -def get_attributes_schema( - use_cache: bool = True, -) -> "AttributesSchemaDict": - con = get_server_api_connection() - return con.get_attributes_schema( - use_cache=use_cache, - ) +def download_dependency_package( + src_filename: str, + dst_directory: str, + dst_filename: str, + platform_name: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download dependency package from server. + This method requires to have authorized token available. The package + is only downloaded. -def reset_attributes_schema(): - con = get_server_api_connection() - return con.reset_attributes_schema() + Args: + src_filename (str): Filename of dependency pacakge. + For server version 0.2.0 and lower it is name of package + to download. + dst_directory (str): Where the file should be downloaded. + dst_filename (str): Name of destination filename. + platform_name (Optional[str]): Deprecated. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + Returns: + str: Filepath to downloaded file. -def set_attribute_config( - attribute_name: str, - data: "AttributeSchemaDataDict", - scope: List["AttributeScope"], - position: Optional[int] = None, - builtin: bool = False, -): + """ con = get_server_api_connection() - return con.set_attribute_config( - attribute_name=attribute_name, - data=data, - scope=scope, - position=position, - builtin=builtin, + return con.download_dependency_package( + src_filename=src_filename, + dst_directory=dst_directory, + dst_filename=dst_filename, + platform_name=platform_name, + chunk_size=chunk_size, + progress=progress, ) -def remove_attribute_config( - attribute_name: str, -): - """Remove attribute from server. - - This can't be un-done, please use carefully. +def upload_dependency_package( + src_filepath: str, + dst_filename: str, + platform_name: Optional[str] = None, + progress: Optional[TransferProgress] = None, +) -> None: + """Upload dependency package to server. Args: - attribute_name (str): Name of attribute to remove. + src_filepath (str): Path to a package file. + dst_filename (str): Dependency package filename or name of package + for server version 0.2.0 or lower. Must be unique. + platform_name (Optional[str]): Deprecated. + progress (Optional[TransferProgress]): Object to keep track about + upload state. """ con = get_server_api_connection() - return con.remove_attribute_config( - attribute_name=attribute_name, + return con.upload_dependency_package( + src_filepath=src_filepath, + dst_filename=dst_filename, + platform_name=platform_name, + progress=progress, ) -def get_attributes_for_type( - entity_type: "AttributeScope", -) -> Dict[str, "AttributeSchemaDict"]: - """Get attribute schemas available for an entity type. +def get_secrets() -> list[SecretDict]: + """Get all secrets. - Example:: + Example output:: + + [ + { + "name": "secret_1", + "value": "secret_value_1", + }, + { + "name": "secret_2", + "value": "secret_value_2", + } + ] + + Returns: + list[SecretDict]: List of secret entities. + + """ + con = get_server_api_connection() + return con.get_secrets() + + +def get_secret( + secret_name: str, +) -> SecretDict: + """Get secret by name. + + Example output:: - ``` - # Example attribute schema { - # Common - "type": "integer", - "title": "Clip Out", - "description": null, - "example": 1, - "default": 1, - # These can be filled based on value of 'type' - "gt": null, - "ge": null, - "lt": null, - "le": null, - "minLength": null, - "maxLength": null, - "minItems": null, - "maxItems": null, - "regex": null, - "enum": null + "name": "secret_name", + "value": "secret_value", } - ``` Args: - entity_type (str): Entity type for which should be attributes - received. + secret_name (str): Name of secret. Returns: - dict[str, dict[str, Any]]: Attribute schemas that are available - for entered entity type. + dict[str, str]: Secret entity data. """ con = get_server_api_connection() - return con.get_attributes_for_type( - entity_type=entity_type, + return con.get_secret( + secret_name=secret_name, ) -def get_attributes_fields_for_type( - entity_type: "AttributeScope", -) -> Set[str]: - """Prepare attribute fields for entity type. +def save_secret( + secret_name: str, + secret_value: str, +) -> None: + """Save secret. - Returns: - set[str]: Attributes fields for entity type. + This endpoint can create and update secret. + + Args: + secret_name (str): Name of secret. + secret_value (str): Value of secret. """ con = get_server_api_connection() - return con.get_attributes_fields_for_type( - entity_type=entity_type, + return con.save_secret( + secret_name=secret_name, + secret_value=secret_value, ) -def get_default_fields_for_type( - entity_type: str, -) -> Set[str]: - """Default fields for entity type. - - Returns most of commonly used fields from server. +def delete_secret( + secret_name: str, +) -> None: + """Delete secret by name. Args: - entity_type (str): Name of entity type. - - Returns: - set[str]: Fields that should be queried from server. + secret_name (str): Name of secret to delete. """ con = get_server_api_connection() - return con.get_default_fields_for_type( - entity_type=entity_type, + return con.delete_secret( + secret_name=secret_name, ) -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional[ActionModeType] = None, +) -> list[ActionManifestDict]: + """Get actions for a context. Args: - details (Optional[bool]): Detailed data with information how - to get client code. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. + + Returns: + list[ActionManifestDict]: list of action manifests. """ con = get_server_api_connection() - return con.get_addons_info( - details=details, + return con.get_actions( + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def get_addon_endpoint( +def trigger_action( + identifier: str, addon_name: str, addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' + project_name: Optional[str] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> ActionTriggerResponse: + """Trigger action. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - - Returns: - str: Final url. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.get_addon_endpoint( + return con.trigger_action( + identifier=identifier, addon_name=addon_name, addon_version=addon_version, - *subpaths, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_addon_url( +def get_action_config( + identifier: str, addon_name: str, addon_version: str, - *subpaths, - use_rest: bool = True, -) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + project_name: Optional[str] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> ActionConfigResponse: + """Get action configuration. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - str: Final url. + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() - return con.get_addon_url( + return con.get_action_config( + identifier=identifier, addon_name=addon_name, addon_version=addon_version, - *subpaths, - use_rest=use_rest, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def download_addon_private_file( +def set_action_config( + identifier: str, addon_name: str, addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. + value: dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> ActionConfigResponse: + """Set action configuration. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - str: Filepath to downloaded file. + ActionConfigResponse: New action configuration data. """ con = get_server_api_connection() - return con.download_addon_private_file( + return con.set_action_config( + identifier=identifier, addon_name=addon_name, addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, + value=value, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_installers( - version: Optional[str] = None, - platform_name: Optional[str] = None, -) -> "InstallersInfoDict": - """Information about desktop application installers on server. - - Desktop application installers are helpers to download/update AYON - desktop application for artists. +def take_action( + action_token: str, +) -> ActionTakeResponse: + """Take action metadata using an action token. Args: - version (Optional[str]): Filter installers by version. - platform_name (Optional[str]): Filter installers by platform name. + action_token (str): AYON launcher action token. Returns: - InstallersInfoDict: Information about installers known for server. + ActionTakeResponse: Action metadata describing how to launch + action. """ con = get_server_api_connection() - return con.get_installers( - version=version, - platform_name=platform_name, + return con.take_action( + action_token=action_token, ) -def create_installer( - filename: str, - version: str, - python_version: str, - platform_name: str, - python_modules: Dict[str, str], - runtime_python_modules: Dict[str, str], - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, -): - """Create new installer information on server. - - This step will create only metadata. Make sure to upload installer - to the server using 'upload_installer' method. - - Runtime python modules are modules that are required to run AYON - desktop application, but are not added to PYTHONPATH for any - subprocess. +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. Args: - filename (str): Installer filename. - version (str): Version of installer. - python_version (str): Version of Python. - platform_name (str): Name of platform. - python_modules (dict[str, str]): Python modules that are available - in installer. - runtime_python_modules (dict[str, str]): Runtime python modules - that are available in installer. - checksum (str): Installer file checksum. - checksum_algorithm (str): Type of checksum used to create checksum. - file_size (int): File size. - sources (Optional[list[dict[str, Any]]]): List of sources that - can be used to download file. + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. """ con = get_server_api_connection() - return con.create_installer( - filename=filename, - version=version, - python_version=python_version, - platform_name=platform_name, - python_modules=python_modules, - runtime_python_modules=runtime_python_modules, - checksum=checksum, - checksum_algorithm=checksum_algorithm, - file_size=file_size, - sources=sources, + return con.abort_action( + action_token=action_token, + message=message, ) -def update_installer( - filename: str, - sources: List[Dict[str, Any]], -): - """Update installer information on server. +def get_activities( + project_name: str, + activity_ids: Optional[Iterable[str]] = None, + activity_types: Optional[Iterable[ActivityType]] = None, + entity_ids: Optional[Iterable[str]] = None, + entity_names: Optional[Iterable[str]] = None, + entity_type: Optional[str] = None, + changed_after: Optional[str] = None, + changed_before: Optional[str] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, +) -> Generator[dict[str, Any], None, None]: + """Get activities from server with filtering options. Args: - filename (str): Installer filename. - sources (list[dict[str, Any]]): List of sources that - can be used to download file. Fully replaces existing sources. + project_name (str): Project on which activities happened. + activity_ids (Optional[Iterable[str]]): Activity ids. + activity_types (Optional[Iterable[ActivityType]]): Activity types. + entity_ids (Optional[Iterable[str]]): Entity ids. + entity_names (Optional[Iterable[str]]): Entity names. + entity_type (Optional[str]): Entity type. + changed_after (Optional[str]): Return only activities changed + after given iso datetime string. + changed_before (Optional[str]): Return only activities changed + before given iso datetime string. + reference_types (Optional[Iterable[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. """ con = get_server_api_connection() - return con.update_installer( - filename=filename, - sources=sources, + return con.get_activities( + project_name=project_name, + activity_ids=activity_ids, + activity_types=activity_types, + entity_ids=entity_ids, + entity_names=entity_names, + entity_type=entity_type, + changed_after=changed_after, + changed_before=changed_before, + reference_types=reference_types, + fields=fields, + limit=limit, + order=order, ) -def delete_installer( - filename: str, -): - """Delete installer from server. +def get_activity_by_id( + project_name: str, + activity_id: str, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, + fields: Optional[Iterable[str]] = None, +) -> Optional[dict[str, Any]]: + """Get activity by id. Args: - filename (str): Installer filename. + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Optional[dict[str, Any]]: Activity data or None if activity is not + found. """ con = get_server_api_connection() - return con.delete_installer( - filename=filename, + return con.get_activity_by_id( + project_name=project_name, + activity_id=activity_id, + reference_types=reference_types, + fields=fields, ) -def download_installer( - filename: str, - dst_filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -): - """Download installer file from server. +def create_activity( + project_name: str, + entity_id: str, + entity_type: str, + activity_type: ActivityType, + activity_id: Optional[str] = None, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + timestamp: Optional[str] = None, + data: Optional[dict[str, Any]] = None, +) -> str: + """Create activity on a project. Args: - filename (str): Installer filename. - dst_filepath (str): Destination filepath. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + project_name (str): Project on which activity happened. + entity_id (str): Entity id. + entity_type (str): Entity type. + activity_type (ActivityType): Activity type. + activity_id (Optional[str]): Activity id. + body (Optional[str]): Activity body. + file_ids (Optional[list[str]]): List of file ids attached + to activity. + timestamp (Optional[str]): Activity timestamp. + data (Optional[dict[str, Any]]): Additional data. + + Returns: + str: Activity id. """ con = get_server_api_connection() - return con.download_installer( - filename=filename, - dst_filepath=dst_filepath, - chunk_size=chunk_size, - progress=progress, + return con.create_activity( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + activity_type=activity_type, + activity_id=activity_id, + body=body, + file_ids=file_ids, + timestamp=timestamp, + data=data, ) -def upload_installer( - src_filepath: str, - dst_filename: str, - progress: Optional[TransferProgress] = None, -): - """Upload installer file to server. +def update_activity( + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = None, +) -> None: + """Update activity by id. Args: - src_filepath (str): Source filepath. - dst_filename (str): Destination filename. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - requests.Response: Response object. + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + body (str): Activity body. + file_ids (Optional[list[str]]): List of file ids attached + to activity. + append_file_ids (Optional[bool]): Append file ids to existing + list of file ids. + data (Optional[dict[str, Any]]): Update data in activity. """ con = get_server_api_connection() - return con.upload_installer( - src_filepath=src_filepath, - dst_filename=dst_filename, - progress=progress, + return con.update_activity( + project_name=project_name, + activity_id=activity_id, + body=body, + file_ids=file_ids, + append_file_ids=append_file_ids, + data=data, ) -def get_dependency_packages() -> "DependencyPackagesDict": - """Information about dependency packages on server. - - To download dependency package, use 'download_dependency_package' - method and pass in 'filename'. - - Example data structure:: - - { - "packages": [ - { - "filename": str, - "platform": str, - "checksum": str, - "checksumAlgorithm": str, - "size": int, - "sources": list[dict[str, Any]], - "supportedAddons": dict[str, str], - "pythonModules": dict[str, str] - } - ] - } - - Returns: - DependencyPackagesDict: Information about dependency packages - known for server. - - """ - con = get_server_api_connection() - return con.get_dependency_packages() - - -def create_dependency_package( - filename: str, - python_modules: Dict[str, str], - source_addons: Dict[str, str], - installer_version: str, - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, - platform_name: Optional[str] = None, -): - """Create dependency package on server. - - The package will be created on a server, it is also required to upload - the package archive file (using :meth:`upload_dependency_package`). +def delete_activity( + project_name: str, + activity_id: str, +) -> None: + """Delete activity by id. Args: - filename (str): Filename of dependency package. - python_modules (dict[str, str]): Python modules in dependency - package:: - - {"": "", ...} - - source_addons (dict[str, str]): Name of addons for which is - dependency package created:: - - {"": "", ...} - - installer_version (str): Version of installer for which was - package created. - checksum (str): Checksum of archive file where dependencies are. - checksum_algorithm (str): Algorithm used to calculate checksum. - file_size (Optional[int]): Size of file. - sources (Optional[list[dict[str, Any]]]): Information about - sources from where it is possible to get file. - platform_name (Optional[str]): Name of platform for which is - dependency package targeted. Default value is - current platform. - - """ - con = get_server_api_connection() - return con.create_dependency_package( - filename=filename, - python_modules=python_modules, - source_addons=source_addons, - installer_version=installer_version, - checksum=checksum, - checksum_algorithm=checksum_algorithm, - file_size=file_size, - sources=sources, - platform_name=platform_name, - ) - - -def update_dependency_package( - filename: str, - sources: List[Dict[str, Any]], -): - """Update dependency package metadata on server. - - Args: - filename (str): Filename of dependency package. - sources (list[dict[str, Any]]): Information about - sources from where it is possible to get file. Fully replaces - existing sources. - - """ - con = get_server_api_connection() - return con.update_dependency_package( - filename=filename, - sources=sources, - ) - - -def delete_dependency_package( - filename: str, - platform_name: Optional[str] = None, -): - """Remove dependency package for specific platform. - - Args: - filename (str): Filename of dependency package. - platform_name (Optional[str]): Deprecated. - - """ - con = get_server_api_connection() - return con.delete_dependency_package( - filename=filename, - platform_name=platform_name, - ) - - -def download_dependency_package( - src_filename: str, - dst_directory: str, - dst_filename: str, - platform_name: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download dependency package from server. - - This method requires to have authorized token available. The package - is only downloaded. - - Args: - src_filename (str): Filename of dependency pacakge. - For server version 0.2.0 and lower it is name of package - to download. - dst_directory (str): Where the file should be downloaded. - dst_filename (str): Name of destination filename. - platform_name (Optional[str]): Deprecated. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - con = get_server_api_connection() - return con.download_dependency_package( - src_filename=src_filename, - dst_directory=dst_directory, - dst_filename=dst_filename, - platform_name=platform_name, - chunk_size=chunk_size, - progress=progress, - ) - - -def upload_dependency_package( - src_filepath: str, - dst_filename: str, - platform_name: Optional[str] = None, - progress: Optional[TransferProgress] = None, -): - """Upload dependency package to server. - - Args: - src_filepath (str): Path to a package file. - dst_filename (str): Dependency package filename or name of package - for server version 0.2.0 or lower. Must be unique. - platform_name (Optional[str]): Deprecated. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - """ - con = get_server_api_connection() - return con.upload_dependency_package( - src_filepath=src_filepath, - dst_filename=dst_filename, - platform_name=platform_name, - progress=progress, - ) - - -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, -): - """Delete addon from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. """ con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, + return con.delete_activity( + project_name=project_name, + activity_id=activity_id, ) -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, -): - """Delete addon version from server. +def send_activities_batch_operations( + project_name: str, + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list[dict[str, Any]]: + """Post multiple CRUD activities operations to server. - Delete all versions of addon from server. + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. - - """ - con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, - ) - - -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, -): - """Upload addon zip file to server. - - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). - - Example output:: - - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. - Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. Returns: - dict[str, Any]: Response data from server. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, + return con.send_activities_batch_operations( + project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_bundles() -> "BundlesInfoDict": +def get_bundles() -> BundlesInfoDict: """Server bundles with basic information. This is example output:: @@ -2341,15 +2166,15 @@ def get_bundles() -> "BundlesInfoDict": def create_bundle( name: str, - addon_versions: Dict[str, str], + addon_versions: dict[str, str], installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, -): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, +) -> None: """Create bundle on server. Bundle cannot be changed once is created. Only isProduction, isStaging @@ -2404,15 +2229,15 @@ def create_bundle( def update_bundle( bundle_name: str, - addon_versions: Optional[Dict[str, str]] = None, + addon_versions: Optional[dict[str, str]] = None, installer_version: Optional[str] = None, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, -): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, +) -> None: """Update bundle on server. Dependency packages can be update only for single platform. Others @@ -2451,303 +2276,264 @@ def update_bundle( ) -def check_bundle_compatibility( - name: str, - addon_versions: Dict[str, str], - installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, -) -> Dict[str, Any]: - """Check bundle compatibility. - - Can be used as per-flight validation before creating bundle. - - Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - Returns: - Dict[str, Any]: Server response, with 'success' and 'issues'. - - """ - con = get_server_api_connection() - return con.check_bundle_compatibility( - name=name, - addon_versions=addon_versions, - installer_version=installer_version, - dependency_packages=dependency_packages, - is_production=is_production, - is_staging=is_staging, - is_dev=is_dev, - dev_active_user=dev_active_user, - dev_addons_config=dev_addons_config, - ) - - -def delete_bundle( - bundle_name: str, -): - """Delete bundle from server. - - Args: - bundle_name (str): Name of bundle to delete. - - """ - con = get_server_api_connection() - return con.delete_bundle( - bundle_name=bundle_name, - ) - - -def get_project_anatomy_presets() -> List["AnatomyPresetDict"]: - """Anatomy presets available on server. - - Content has basic information about presets. Example output:: - - [ - { - "name": "netflix_VFX", - "primary": false, - "version": "1.0.0" - }, - { - ... - }, - ... - ] - - Returns: - list[dict[str, str]]: Anatomy presets available on server. - - """ - con = get_server_api_connection() - return con.get_project_anatomy_presets() - - -def get_default_anatomy_preset_name() -> str: - """Name of default anatomy preset. - - Primary preset is used as default preset. But when primary preset is - not set a built-in is used instead. Built-in preset is named '_'. - - Returns: - str: Name of preset that can be used by - 'get_project_anatomy_preset'. - - """ - con = get_server_api_connection() - return con.get_default_anatomy_preset_name() - - -def get_project_anatomy_preset( - preset_name: Optional[str] = None, -) -> "AnatomyPresetDict": - """Anatomy preset values by name. +def check_bundle_compatibility( + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, +) -> dict[str, Any]: + """Check bundle compatibility. - Get anatomy preset values by preset name. Primary preset is returned - if preset name is set to 'None'. + Can be used as per-flight validation before creating bundle. Args: - preset_name (Optional[str]): Preset name. + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. Returns: - AnatomyPresetDict: Anatomy preset values. + dict[str, Any]: Server response, with 'success' and 'issues'. """ con = get_server_api_connection() - return con.get_project_anatomy_preset( - preset_name=preset_name, + return con.check_bundle_compatibility( + name=name, + addon_versions=addon_versions, + installer_version=installer_version, + dependency_packages=dependency_packages, + is_production=is_production, + is_staging=is_staging, + is_dev=is_dev, + dev_active_user=dev_active_user, + dev_addons_config=dev_addons_config, ) -def get_built_in_anatomy_preset() -> "AnatomyPresetDict": - """Get built-in anatomy preset. +def delete_bundle( + bundle_name: str, +) -> None: + """Delete bundle from server. - Returns: - AnatomyPresetDict: Built-in anatomy preset. + Args: + bundle_name (str): Name of bundle to delete. """ con = get_server_api_connection() - return con.get_built_in_anatomy_preset() - - -def get_build_in_anatomy_preset() -> "AnatomyPresetDict": - con = get_server_api_connection() - return con.get_build_in_anatomy_preset() - + return con.delete_bundle( + bundle_name=bundle_name, + ) -def get_project_root_overrides( - project_name: str, -) -> Dict[str, Dict[str, str]]: - """Root overrides per site name. - Method is based on logged user and can't be received for any other - user on server. +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. - Output will contain only roots per site id used by logged user. + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' Args: - project_name (str): Name of project. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. Returns: - dict[str, dict[str, str]]: Root values by root name by site id. + str: Final url. """ con = get_server_api_connection() - return con.get_project_root_overrides( - project_name=project_name, + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, ) -def get_project_roots_by_site( - project_name: str, -) -> Dict[str, Dict[str, str]]: - """Root overrides per site name. +def get_addons_info( + details: bool = True, +) -> AddonsInfoDict: + """Get information about addons available on server. - Method is based on logged user and can't be received for any other - user on server. + Args: + details (Optional[bool]): Detailed data with information how + to get client code. - Output will contain only roots per site id used by logged user. + """ + con = get_server_api_connection() + return con.get_addons_info( + details=details, + ) - Deprecated: - Use 'get_project_root_overrides' instead. Function - deprecated since 1.0.6 + +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' Args: - project_name (str): Name of project. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. Returns: - dict[str, dict[str, str]]: Root values by root name by site id. + str: Final url. """ con = get_server_api_connection() - return con.get_project_roots_by_site( - project_name=project_name, + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, ) -def get_project_root_overrides_by_site_id( - project_name: str, - site_id: Optional[str] = None, -) -> Dict[str, str]: - """Root overrides for site. +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from server. - If site id is not passed a site set in current api object is used - instead. + Delete all versions of addon from server. Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name or None if - site does not have overrides. + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_project_root_overrides_by_site_id( - project_name=project_name, - site_id=site_id, + return con.delete_addon( + addon_name=addon_name, + purge=purge, ) -def get_project_roots_for_site( - project_name: str, - site_id: Optional[str] = None, -) -> Dict[str, str]: - """Root overrides for site. +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. - If site id is not passed a site set in current api object is used - instead. + Delete all versions of addon from server. - Deprecated: - Use 'get_project_root_overrides_by_site_id' instead. Function - deprecated since 1.0.6 Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name, root name is not - available if it does not have overrides. + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_project_roots_for_site( - project_name=project_name, - site_id=site_id, + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, ) -def get_project_roots_by_site_id( - project_name: str, - site_id: Optional[str] = None, -) -> Dict[str, str]: - """Root values for a site. +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file to server. - If site id is not passed a site set in current api object is used - instead. If site id is not available, default roots are returned - for current platform. + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output:: + + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - root values. + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. Returns: - dict[str, str]: Root values. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.get_project_roots_by_site_id( - project_name=project_name, - site_id=site_id, + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, ) -def get_project_roots_by_platform( - project_name: str, - platform_name: Optional[str] = None, -) -> Dict[str, str]: - """Root values for a site. - - If platform name is not passed current platform name is used instead. +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. - This function does return root values without site overrides. It is - possible to use the function to receive default root values. + This method requires to have authorized token available. Private files + are not under '/api' restpoint. Args: - project_name (str): Name of project. - platform_name (Optional[Literal["windows", "linux", "darwin"]]): - Platform name for which want to receive root values. Current - platform name is used if not passed. + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: - dict[str, str]: Root values. + str: Filepath to downloaded file. """ con = get_server_api_connection() - return con.get_project_roots_by_platform( - project_name=project_name, - platform_name=platform_name, + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, ) @@ -2755,7 +2541,7 @@ def get_addon_settings_schema( addon_name: str, addon_version: str, project_name: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Sudio/Project settings schema of an addon. Project schema may look differently as some enums are based on project @@ -2782,7 +2568,7 @@ def get_addon_settings_schema( def get_addon_site_settings_schema( addon_name: str, addon_version: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Site settings schema of an addon. Args: @@ -2804,7 +2590,7 @@ def get_addon_studio_settings( addon_name: str, addon_version: str, variant: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Addon studio settings. Receive studio settings for specific version of an addon. @@ -2834,7 +2620,7 @@ def get_addon_project_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Addon project settings. Receive project settings for specific version of an addon. The settings @@ -2879,7 +2665,7 @@ def get_addon_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Receive addon settings. Receive addon settings based on project name value. Some arguments may @@ -2919,7 +2705,7 @@ def get_addon_site_settings( addon_name: str, addon_version: str, site_id: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Site settings of an addon. If site id is not available an empty dictionary is returned. @@ -2948,7 +2734,7 @@ def get_bundle_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get complete set of settings for given data. If project is not passed then studio settings are returned. If variant @@ -2996,7 +2782,7 @@ def get_addons_studio_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """All addons settings in one bulk. Warnings: @@ -3038,7 +2824,7 @@ def get_addons_project_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Project settings of all addons. Server returns information about used addon versions, so full output @@ -3098,7 +2884,7 @@ def get_addons_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Universal function to receive all addon settings. Based on 'project_name' will receive studio settings or project @@ -3136,279 +2922,594 @@ def get_addons_settings( ) -def get_secrets() -> List["SecretDict"]: - """Get all secrets. +def get_event( + event_id: str, +) -> Optional[dict[str, Any]]: + """Query full event data by id. - Example output:: + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. - [ - { - "name": "secret_1", - "value": "secret_value_1", - }, - { - "name": "secret_2", - "value": "secret_value_2", - } - ] + Args: + event_id (str): Event id. Returns: - list[SecretDict]: List of secret entities. + dict[str, Any]: Full event data. """ con = get_server_api_connection() - return con.get_secrets() + return con.get_event( + event_id=event_id, + ) -def get_secret( - secret_name: str, -) -> "SecretDict": - """Get secret by name. +def get_events( + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[EventStatus]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, +) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. - Example output:: + Notes: + Not all event happen on a project. - { - "name": "secret_name", - "value": "secret_value", - } + Args: + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. + + Returns: + Generator[dict[str, Any]]: Available events matching filters. + + """ + con = get_server_api_connection() + return con.get_events( + topics=topics, + event_ids=event_ids, + project_names=project_names, + statuses=statuses, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields, + limit=limit, + order=order, + states=states, + ) + + +def update_event( + event_id: str, + sender: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + status: Optional[EventStatus] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, +) -> None: + """Update event data. + + Args: + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[EventStatus]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. + + """ + con = get_server_api_connection() + return con.update_event( + event_id=event_id, + sender=sender, + project_name=project_name, + username=username, + status=status, + description=description, + summary=summary, + payload=payload, + progress=progress, + retries=retries, + ) + + +def dispatch_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +) -> RestApiResponse: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + RestApiResponse: Response from server. + + """ + con = get_server_api_connection() + return con.dispatch_event( + topic=topic, + sender=sender, + event_hash=event_hash, + project_name=project_name, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, + ) + + +def create_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +) -> str: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + str: Event id. + + """ + con = get_server_api_connection() + return con.create_event( + topic=topic, + sender=sender, + event_hash=event_hash, + project_name=project_name, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, + ) + + +def delete_event( + event_id: str, +) -> None: + """Delete event by id. + + Supported since AYON server 1.6.0. Args: - secret_name (str): Name of secret. + event_id (str): Event id. Returns: - dict[str, str]: Secret entity data. + RestApiResponse: Response from server. """ con = get_server_api_connection() - return con.get_secret( - secret_name=secret_name, + return con.delete_event( + event_id=event_id, ) -def save_secret( - secret_name: str, - secret_value: str, -): - """Save secret. +def enroll_event_job( + source_topic: Union[str, list[str]], + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional[EventFilter] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, +) -> Optional[EnrollEventData]: + """Enroll job based on events. - This endpoint can create and update secret. + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. - Args: - secret_name (str): Name of secret. - secret_value (str): Value of secret. + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. - """ - con = get_server_api_connection() - return con.save_secret( - secret_name=secret_name, - secret_value=secret_value, - ) + Make sure the new event has updated status to '"finished"' status + when you're done with logic + Target topic should not clash with other processes/services. -def delete_secret( - secret_name: str, -): - """Delete secret by name. + Created target event have 'dependsOn' key where is id of source topic. + + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' Args: - secret_name (str): Name of secret to delete. + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. + + Returns: + Optional[EnrollEventData]: None if there is no event matching + filters. Created event with 'target_topic'. """ con = get_server_api_connection() - return con.delete_secret( - secret_name=secret_name, + return con.enroll_event_job( + source_topic=source_topic, + target_topic=target_topic, + sender=sender, + description=description, + sequential=sequential, + events_filter=events_filter, + max_retries=max_retries, + ignore_older_than=ignore_older_than, + ignore_sender_types=ignore_sender_types, ) -def get_rest_project( - project_name: str, -) -> Optional["ProjectDict"]: - """Query project by name. +def get_attributes_schema( + use_cache: bool = True, +) -> AttributesSchemaDict: + con = get_server_api_connection() + return con.get_attributes_schema( + use_cache=use_cache, + ) - This call returns project with anatomy data. - Args: - project_name (str): Name of project. +def reset_attributes_schema() -> None: + con = get_server_api_connection() + return con.reset_attributes_schema() - Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. - """ +def set_attribute_config( + attribute_name: str, + data: AttributeSchemaDataDict, + scope: list[AttributeScope], + position: Optional[int] = None, + builtin: bool = False, +) -> None: con = get_server_api_connection() - return con.get_rest_project( - project_name=project_name, + return con.set_attribute_config( + attribute_name=attribute_name, + data=data, + scope=scope, + position=position, + builtin=builtin, ) -def get_rest_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: - """Query available project entities. +def remove_attribute_config( + attribute_name: str, +) -> None: + """Remove attribute from server. - User must be logged in. + This can't be un-done, please use carefully. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - Generator[ProjectDict, None, None]: Available projects. + attribute_name (str): Name of attribute to remove. """ con = get_server_api_connection() - return con.get_rest_projects( - active=active, - library=library, + return con.remove_attribute_config( + attribute_name=attribute_name, ) -def get_rest_entity_by_id( - project_name: str, - entity_type: str, - entity_id: str, -) -> Optional["AnyEntityDict"]: - """Get entity using REST on a project by its id. +def get_attributes_for_type( + entity_type: AttributeScope, +) -> dict[str, AttributeSchemaDict]: + """Get attribute schemas available for an entity type. + + Example:: + + ``` + # Example attribute schema + { + # Common + "type": "integer", + "title": "Clip Out", + "description": null, + "example": 1, + "default": 1, + # These can be filled based on value of 'type' + "gt": null, + "ge": null, + "lt": null, + "le": null, + "minLength": null, + "maxLength": null, + "minItems": null, + "maxItems": null, + "regex": null, + "enum": null + } + ``` Args: - project_name (str): Name of project where entity is. - entity_type (Literal["folder", "task", "product", "version"]): The - entity type which should be received. - entity_id (str): Id of entity. + entity_type (str): Entity type for which should be attributes + received. Returns: - Optional[AnyEntityDict]: Received entity data. + dict[str, dict[str, Any]]: Attribute schemas that are available + for entered entity type. """ con = get_server_api_connection() - return con.get_rest_entity_by_id( - project_name=project_name, + return con.get_attributes_for_type( entity_type=entity_type, - entity_id=entity_id, ) -def get_rest_folder( - project_name: str, - folder_id: str, -) -> Optional["FolderDict"]: +def get_attributes_fields_for_type( + entity_type: AttributeScope, +) -> set[str]: + """Prepare attribute fields for entity type. + + Returns: + set[str]: Attributes fields for entity type. + + """ con = get_server_api_connection() - return con.get_rest_folder( - project_name=project_name, - folder_id=folder_id, + return con.get_attributes_fields_for_type( + entity_type=entity_type, ) -def get_rest_folders( - project_name: str, - include_attrib: bool = False, -) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. +def get_project_anatomy_presets() -> list[AnatomyPresetDict]: + """Anatomy presets available on server. - Example:: + Content has basic information about presets. Example output:: [ { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", + "name": "netflix_VFX", + "primary": false, + "version": "1.0.0" + }, + { + ... }, ... ] + Returns: + list[dict[str, str]]: Anatomy presets available on server. + + """ + con = get_server_api_connection() + return con.get_project_anatomy_presets() + + +def get_default_anatomy_preset_name() -> str: + """Name of default anatomy preset. + + Primary preset is used as default preset. But when primary preset is + not set a built-in is used instead. Built-in preset is named '_'. + + Returns: + str: Name of preset that can be used by + 'get_project_anatomy_preset'. + + """ + con = get_server_api_connection() + return con.get_default_anatomy_preset_name() + + +def get_project_anatomy_preset( + preset_name: Optional[str] = None, +) -> AnatomyPresetDict: + """Anatomy preset values by name. + + Get anatomy preset values by preset name. Primary preset is returned + if preset name is set to 'None'. + Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + preset_name (Optional[str]): Preset name. Returns: - List[FlatFolderDict]: List of folder entities. + AnatomyPresetDict: Anatomy preset values. """ con = get_server_api_connection() - return con.get_rest_folders( - project_name=project_name, - include_attrib=include_attrib, + return con.get_project_anatomy_preset( + preset_name=preset_name, ) -def get_rest_task( - project_name: str, - task_id: str, -) -> Optional["TaskDict"]: +def get_built_in_anatomy_preset() -> AnatomyPresetDict: + """Get built-in anatomy preset. + + Returns: + AnatomyPresetDict: Built-in anatomy preset. + + """ con = get_server_api_connection() - return con.get_rest_task( - project_name=project_name, - task_id=task_id, - ) + return con.get_built_in_anatomy_preset() -def get_rest_product( - project_name: str, - product_id: str, -) -> Optional["ProductDict"]: +def get_build_in_anatomy_preset() -> AnatomyPresetDict: con = get_server_api_connection() - return con.get_rest_product( - project_name=project_name, - product_id=product_id, - ) + return con.get_build_in_anatomy_preset() -def get_rest_version( +def get_rest_project( project_name: str, - version_id: str, -) -> Optional["VersionDict"]: +) -> Optional[ProjectDict]: + """Query project by name. + + This call returns project with anatomy data. + + Args: + project_name (str): Name of project. + + Returns: + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. + + """ con = get_server_api_connection() - return con.get_rest_version( + return con.get_rest_project( project_name=project_name, - version_id=version_id, ) -def get_rest_representation( - project_name: str, - representation_id: str, -) -> Optional["RepresentationDict"]: +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> Generator[ProjectDict, None, None]: + """Query available project entities. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + Generator[ProjectDict, None, None]: Available projects. + + """ con = get_server_api_connection() - return con.get_rest_representation( - project_name=project_name, - representation_id=representation_id, + return con.get_rest_projects( + active=active, + library=library, ) def get_project_names( - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, -) -> List[str]: + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[str]: """Receive available project names. User must be logged in. Args: - active (Union[bool, None]): Filter active/inactive projects. Both + active (Optional[bool]): Filter active/inactive projects. Both are returned if 'None' is passed. - library (Union[bool, None]): Filter standard/library projects. Both + library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. Returns: @@ -3423,11 +3524,11 @@ def get_project_names( def get_projects( - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, + active: Optional[bool] = True, + library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: +) -> Generator[ProjectDict, None, None]: """Get projects. Args: @@ -3457,7 +3558,7 @@ def get_project( project_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["ProjectDict"]: +) -> Optional[ProjectDict]: """Get project. Args: @@ -3480,490 +3581,502 @@ def get_project( ) -def get_folders_hierarchy( +def create_project( project_name: str, - search_string: Optional[str] = None, - folder_types: Optional[Iterable[str]] = None, -) -> "ProjectHierarchyDict": - """Get project hierarchy. + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, +) -> ProjectDict: + """Create project using AYON settings. - All folders in project in hierarchy data structure. + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. - Example output: - { - "hierarchy": [ - { - "id": "...", - "name": "...", - "label": "...", - "status": "...", - "folderType": "...", - "hasTasks": False, - "taskNames": [], - "parents": [], - "parentId": None, - "children": [...children folders...] - }, - ... - ] - } + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. Args: - project_name (str): Project where to look for folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. Returns: - dict[str, Any]: Response data from server. + ProjectDict: Created project entity. """ con = get_server_api_connection() - return con.get_folders_hierarchy( + return con.create_project( project_name=project_name, - search_string=search_string, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, + ) + + +def update_project( + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes, +) -> None: + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + + """ + con = get_server_api_connection() + return con.update_project( + project_name=project_name, + library=library, folder_types=folder_types, + task_types=task_types, + link_types=link_types, + statuses=statuses, + tags=tags, + config=config, + attrib=attrib, + data=data, + active=active, + project_code=project_code, + **changes, ) -def get_folders_rest( +def delete_project( project_name: str, - include_attrib: bool = False, -) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. +): + """Delete project from server. - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. + This will completely remove project from server without any step back. - Example:: + Args: + project_name (str): Project name that will be removed. - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] + """ + con = get_server_api_connection() + return con.delete_project( + project_name=project_name, + ) + + +def get_project_root_overrides( + project_name: str, +) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + + """ + con = get_server_api_connection() + return con.get_project_root_overrides( + project_name=project_name, + ) + + +def get_project_roots_by_site( + project_name: str, +) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. Deprecated: - Use 'get_rest_folders' instead. Function was renamed to match - other rest functions, like 'get_rest_folder', - 'get_rest_project' etc. . - Will be removed in '1.0.7' or '1.1.0'. + Use 'get_project_root_overrides' instead. Function + deprecated since 1.0.6 Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + project_name (str): Name of project. Returns: - List[FlatFolderDict]: List of folder entities. + dict[str, dict[str, str]]: Root values by root name by site id. """ con = get_server_api_connection() - return con.get_folders_rest( + return con.get_project_roots_by_site( project_name=project_name, - include_attrib=include_attrib, ) -def get_folders( +def get_project_root_overrides_by_site_id( project_name: str, - folder_ids: Optional[Iterable[str]] = None, - folder_paths: Optional[Iterable[str]] = None, - folder_names: Optional[Iterable[str]] = None, - folder_types: Optional[Iterable[str]] = None, - parent_ids: Optional[Iterable[str]] = None, - folder_path_regex: Optional[str] = None, - has_products: Optional[bool] = None, - has_tasks: Optional[bool] = None, - has_children: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["FolderDict", None, None]: - """Query folders from server. - - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root overrides for site. - Notes: - Filter 'active' don't have direct filter in GraphQl. + If site id is not passed a site set in current api object is used + instead. Args: project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Folder ids to filter. - folder_paths (Optional[Iterable[str]]): Folder paths used - for filtering. - folder_names (Optional[Iterable[str]]): Folder names used - for filtering. - folder_types (Optional[Iterable[str]]): Folder types used - for filtering. - parent_ids (Optional[Iterable[str]]): Ids of folder parents. - Use 'None' if folder is direct child of project. - folder_path_regex (Optional[str]): Folder path regex used - for filtering. - has_products (Optional[bool]): Filter folders with/without - products. Ignored when None, default behavior. - has_tasks (Optional[bool]): Filter folders with/without - tasks. Ignored when None, default behavior. - has_children (Optional[bool]): Filter folders with/without - children. Ignored when None, default behavior. - statuses (Optional[Iterable[str]]): Folder statuses used - for filtering. - assignees_all (Optional[Iterable[str]]): Filter by assigness - on children tasks. Task must have all of passed assignees. - tags (Optional[Iterable[str]]): Folder tags used - for filtering. - active (Optional[bool]): Filter active/inactive folders. - Both are returned if is set to None. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + site_id (Optional[str]): Site id for which want to receive + site overrides. Returns: - Generator[FolderDict, None, None]: Queried folder entities. + dict[str, str]: Root values by root name or None if + site does not have overrides. """ con = get_server_api_connection() - return con.get_folders( + return con.get_project_root_overrides_by_site_id( project_name=project_name, - folder_ids=folder_ids, - folder_paths=folder_paths, - folder_names=folder_names, - folder_types=folder_types, - parent_ids=parent_ids, - folder_path_regex=folder_path_regex, - has_products=has_products, - has_tasks=has_tasks, - has_children=has_children, - statuses=statuses, - assignees_all=assignees_all, - tags=tags, - active=active, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + site_id=site_id, ) -def get_folder_by_id( +def get_project_roots_for_site( project_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by id. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + Deprecated: + Use 'get_project_root_overrides_by_site_id' instead. Function + deprecated since 1.0.6 Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + dict[str, str]: Root values by root name, root name is not + available if it does not have overrides. """ con = get_server_api_connection() - return con.get_folder_by_id( + return con.get_project_roots_for_site( project_name=project_name, - folder_id=folder_id, - fields=fields, - own_attributes=own_attributes, + site_id=site_id, ) -def get_folder_by_path( +def get_project_roots_by_site_id( project_name: str, - folder_path: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root values for a site. - Folder path is a path to folder with all parent names joined by slash. + If site id is not passed a site set in current api object is used + instead. If site id is not available, default roots are returned + for current platform. Args: - project_name (str): Name of project where to look for queried - entities. - folder_path (str): Folder path. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + root values. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + dict[str, str]: Root values. """ con = get_server_api_connection() - return con.get_folder_by_path( + return con.get_project_roots_by_site_id( project_name=project_name, - folder_path=folder_path, - fields=fields, - own_attributes=own_attributes, + site_id=site_id, ) -def get_folder_by_name( +def get_project_roots_by_platform( project_name: str, - folder_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. + platform_name: Optional[str] = None, +) -> dict[str, str]: + """Root values for a site. - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. + If platform name is not passed current platform name is used instead. + + This function does return root values without site overrides. It is + possible to use the function to receive default root values. Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of project. + platform_name (Optional[Literal["windows", "linux", "darwin"]]): + Platform name for which want to receive root values. Current + platform name is used if not passed. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + dict[str, str]: Root values. """ con = get_server_api_connection() - return con.get_folder_by_name( + return con.get_project_roots_by_platform( project_name=project_name, - folder_name=folder_name, - fields=fields, - own_attributes=own_attributes, + platform_name=platform_name, ) -def get_folder_ids_with_products( +def get_rest_folder( project_name: str, - folder_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """Find folders which have at least one product. + folder_id: str, +) -> Optional[FolderDict]: + con = get_server_api_connection() + return con.get_rest_folder( + project_name=project_name, + folder_id=folder_id, + ) - Folders that have at least one product should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. + +def get_rest_folders( + project_name: str, + include_attrib: bool = False, +) -> list[FlatFolderDict]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. Returns: - set[str]: Folder ids that have at least one product. + list[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.get_folder_ids_with_products( + return con.get_rest_folders( project_name=project_name, - folder_ids=folder_ids, + include_attrib=include_attrib, ) -def create_folder( +def get_folders_hierarchy( project_name: str, - name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, -) -> str: - """Create new folder. + search_string: Optional[str] = None, + folder_types: Optional[Iterable[str]] = None, +) -> ProjectHierarchyDict: + """Get project hierarchy. + + All folders in project in hierarchy data structure. + + Example output: + { + "hierarchy": [ + { + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] + }, + ... + ] + } Args: - project_name (str): Project name. - name (str): Folder name. - folder_type (Optional[str]): Folder type. - parent_id (Optional[str]): Parent folder id. Parent is project - if is ``None``. - label (Optional[str]): Label of folder. - attrib (Optional[dict[str, Any]]): Folder attributes. - data (Optional[dict[str, Any]]): Folder data. - tags (Optional[Iterable[str]]): Folder tags. - status (Optional[str]): Folder status. - active (Optional[bool]): Folder active state. - thumbnail_id (Optional[str]): Folder thumbnail id. - folder_id (Optional[str]): Folder id. If not passed new id is - generated. + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. Returns: - str: Entity id. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.create_folder( + return con.get_folders_hierarchy( project_name=project_name, - name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - folder_id=folder_id, + search_string=search_string, + folder_types=folder_types, ) -def update_folder( +def get_folders_rest( project_name: str, - folder_id: str, - name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, - label: Optional[str] = NOT_SET, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update folder entity on server. - - Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + include_attrib: bool = False, +) -> list[FlatFolderDict]: + """Get simplified flat list of all project folders. - Args: - project_name (str): Project name. - folder_id (str): Folder id. - name (Optional[str]): New name. - folder_type (Optional[str]): New folder type. - parent_id (Optional[Union[str, None]]): New parent folder id. - label (Optional[Union[str, None]]): New label. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. - """ - con = get_server_api_connection() - return con.update_folder( - project_name=project_name, - folder_id=folder_id, - name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - ) + Example:: + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] -def delete_folder( - project_name: str, - folder_id: str, - force: bool = False, -): - """Delete folder. + Deprecated: + Use 'get_rest_folders' instead. Function was renamed to match + other rest functions, like 'get_rest_folder', + 'get_rest_project' etc. . + Will be removed in '1.0.7' or '1.1.0'. Args: project_name (str): Project name. - folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. + + Returns: + list[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.delete_folder( + return con.get_folders_rest( project_name=project_name, - folder_id=folder_id, - force=force, + include_attrib=include_attrib, ) -def get_tasks( +def get_folders( project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, + folder_paths: Optional[Iterable[str]] = None, + folder_names: Optional[Iterable[str]] = None, + folder_types: Optional[Iterable[str]] = None, + parent_ids: Optional[Iterable[str]] = None, + folder_path_regex: Optional[str] = None, + has_products: Optional[bool] = None, + has_tasks: Optional[bool] = None, + has_children: Optional[bool] = None, statuses: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + active: Optional[bool] = True, + has_links: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["TaskDict", None, None]: - """Query task entities from server. +) -> Generator[FolderDict, None, None]: + """Query folders from server. + + Todos: + Folder name won't be unique identifier, so we should add + folder path filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. Args: project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + folder_path_regex (Optional[str]): Folder path regex used + for filtering. + has_products (Optional[bool]): Filter folders with/without + products. Ignored when None, default behavior. + has_tasks (Optional[bool]): Filter folders with/without + tasks. Ignored when None, default behavior. + has_children (Optional[bool]): Filter folders with/without + children. Ignored when None, default behavior. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. + assignees_all (Optional[Iterable[str]]): Filter by assigness + on children tasks. Task must have all of passed assignees. + tags (Optional[Iterable[str]]): Folder tags used + for filtering. + active (Optional[bool]): Filter active/inactive folders. Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -3971,308 +4084,227 @@ def get_tasks( not explicitly set on entity will have 'None' value. Returns: - Generator[TaskDict, None, None]: Queried task entities. + Generator[FolderDict, None, None]: Queried folder entities. """ con = get_server_api_connection() - return con.get_tasks( + return con.get_folders( project_name=project_name, - task_ids=task_ids, - task_names=task_names, - task_types=task_types, folder_ids=folder_ids, - assignees=assignees, - assignees_all=assignees_all, + folder_paths=folder_paths, + folder_names=folder_names, + folder_types=folder_types, + parent_ids=parent_ids, + folder_path_regex=folder_path_regex, + has_products=has_products, + has_tasks=has_tasks, + has_children=has_children, statuses=statuses, + assignees_all=assignees_all, tags=tags, active=active, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_task_by_name( +def get_folder_by_id( project_name: str, folder_id: str, - task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by name and folder id. +) -> Optional[FolderDict]: + """Query folder entity by id. Args: project_name (str): Name of project where to look for queried entities. folder_id (str): Folder id. - task_name (str): Task name fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_task_by_name( + return con.get_folder_by_id( project_name=project_name, folder_id=folder_id, - task_name=task_name, fields=fields, own_attributes=own_attributes, ) -def get_task_by_id( +def get_folder_by_path( project_name: str, - task_id: str, + folder_path: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by id. +) -> Optional[FolderDict]: + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. Args: project_name (str): Name of project where to look for queried entities. - task_id (str): Task id. + folder_path (str): Folder path. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_task_by_id( + return con.get_folder_by_path( project_name=project_name, - task_id=task_id, + folder_path=folder_path, fields=fields, own_attributes=own_attributes, ) -def get_tasks_by_folder_paths( +def get_folder_by_name( project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + folder_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Dict[str, List["TaskDict"]]: - """Query task entities from server by folder paths. +) -> Optional[FolderDict]: + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Dict[str, List[TaskDict]]: Task entities by - folder path. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_tasks_by_folder_paths( + return con.get_folder_by_name( project_name=project_name, - folder_paths=folder_paths, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, + folder_name=folder_name, fields=fields, own_attributes=own_attributes, ) -def get_tasks_by_folder_path( +def get_folder_ids_with_products( project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> List["TaskDict"]: - """Query task entities from server by folder path. - - Args: - project_name (str): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - """ - con = get_server_api_connection() - return con.get_tasks_by_folder_path( - project_name=project_name, - folder_path=folder_path, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, - ) - + folder_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """Find folders which have at least one product. -def get_task_by_folder_path( - project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. + Folders that have at least one product should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + set[str]: Folder ids that have at least one product. """ con = get_server_api_connection() - return con.get_task_by_folder_path( + return con.get_folder_ids_with_products( project_name=project_name, - folder_path=folder_path, - task_name=task_name, - fields=fields, - own_attributes=own_attributes, + folder_ids=folder_ids, ) -def create_task( +def create_folder( project_name: str, name: str, - task_type: str, - folder_id: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, + folder_id: Optional[str] = None, ) -> str: - """Create new task. + """Create new folder. Args: project_name (str): Project name. name (str): Folder name. - task_type (str): Task type. - folder_id (str): Parent folder id. + folder_type (Optional[str]): Folder type. + parent_id (Optional[str]): Parent folder id. Parent is project + if is ``None``. label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is + attrib (Optional[dict[str, Any]]): Folder attributes. + data (Optional[dict[str, Any]]): Folder data. + tags (Optional[Iterable[str]]): Folder tags. + status (Optional[str]): Folder status. + active (Optional[bool]): Folder active state. + thumbnail_id (Optional[str]): Folder thumbnail id. + folder_id (Optional[str]): Folder id. If not passed new id is generated. Returns: - str: Task id. + str: Entity id. """ con = get_server_api_connection() - return con.create_task( + return con.create_folder( project_name=project_name, name=name, - task_type=task_type, - folder_id=folder_id, + folder_type=folder_type, + parent_id=parent_id, label=label, - assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, thumbnail_id=thumbnail_id, - task_id=task_id, + folder_id=folder_id, ) -def update_task( +def update_folder( project_name: str, - task_id: str, + folder_id: str, name: Optional[str] = None, - task_type: Optional[str] = None, - folder_id: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, label: Optional[str] = NOT_SET, - assignees: Optional[List[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, -): - """Update task entity on server. +) -> None: + """Update folder entity on server. - Do not pass ``label`` amd ``thumbnail_id`` if you don't + Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't want to change their values. Value ``None`` would unset their value. @@ -4283,29 +4315,27 @@ def update_task( Args: project_name (str): Project name. - task_id (str): Task id. + folder_id (str): Folder id. name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. - assignees (Optional[str]): New assignees. + folder_type (Optional[str]): New folder type. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_task( + return con.update_folder( project_name=project_name, - task_id=task_id, - name=name, - task_type=task_type, folder_id=folder_id, + name=name, + folder_type=folder_type, + parent_id=parent_id, label=label, - assignees=assignees, attrib=attrib, data=data, tags=tags, @@ -4315,85 +4345,93 @@ def update_task( ) -def delete_task( +def delete_folder( project_name: str, - task_id: str, -): - """Delete task. + folder_id: str, + force: bool = False, +) -> None: + """Delete folder. Args: project_name (str): Project name. - task_id (str): Task id to delete. + folder_id (str): Folder id to delete. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. """ con = get_server_api_connection() - return con.delete_task( + return con.delete_folder( + project_name=project_name, + folder_id=folder_id, + force=force, + ) + + +def get_rest_task( + project_name: str, + task_id: str, +) -> Optional[TaskDict]: + con = get_server_api_connection() + return con.get_rest_task( project_name=project_name, task_id=task_id, ) -def get_products( +def get_tasks( project_name: str, - product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, folder_ids: Optional[Iterable[str]] = None, - product_types: Optional[Iterable[str]] = None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["ProductDict", None, None]: - """Query products from server. - - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. + own_attributes: bool = False, +) -> Generator[TaskDict, None, None]: + """Query task entities from server. Args: project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for + tags (Optional[Iterable[str]]): Task tags used for filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used - for filtering. - tags (Optional[Iterable[str]]): Product tags used - for filtering. - active (Optional[bool]): Filter active/inactive products. + active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Generator[ProductDict, None, None]: Queried product entities. + Generator[TaskDict, None, None]: Queried task entities. """ con = get_server_api_connection() - return con.get_products( + return con.get_tasks( project_name=project_name, - product_ids=product_ids, - product_names=product_names, + task_ids=task_ids, + task_names=task_names, + task_types=task_types, folder_ids=folder_ids, - product_types=product_types, - product_name_regex=product_name_regex, - product_path_regex=product_path_regex, - names_by_folder_ids=names_by_folder_ids, + assignees=assignees, + assignees_all=assignees_all, statuses=statuses, tags=tags, active=active, @@ -4402,202 +4440,290 @@ def get_products( ) -def get_product_by_id( +def get_task_by_name( project_name: str, - product_id: str, + folder_id: str, + task_name: str, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by id. + own_attributes: bool = False, +) -> Optional[TaskDict]: + """Query task entity by name and folder id. Args: project_name (str): Name of project where to look for queried entities. - product_id (str): Product id. + folder_id (str): Folder id. + task_name (str): Task name fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Optional[ProductDict]: Product entity data or None - if was not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_product_by_id( + return con.get_task_by_name( project_name=project_name, - product_id=product_id, + folder_id=folder_id, + task_name=task_name, fields=fields, own_attributes=own_attributes, ) -def get_product_by_name( +def get_task_by_id( project_name: str, - product_name: str, - folder_id: str, + task_id: str, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by name and folder id. + own_attributes: bool = False, +) -> Optional[TaskDict]: + """Query task entity by id. Args: project_name (str): Name of project where to look for queried entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). + task_id (str): Task id. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Optional[ProductDict]: Product entity data or None - if was not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_product_by_name( + return con.get_task_by_id( project_name=project_name, - product_name=product_name, - folder_id=folder_id, + task_id=task_id, fields=fields, own_attributes=own_attributes, ) -def get_product_types( +def get_tasks_by_folder_paths( + project_name: str, + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """Types of products. - - This is server wide information. Product types have 'name', 'icon' and - 'color'. + own_attributes: bool = False, +) -> dict[str, list[TaskDict]]: + """Query task entities from server by folder paths. Args: - fields (Optional[Iterable[str]]): Product types fields to query. + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - list[ProductTypeDict]: Product types information. + dict[str, list[TaskDict]]: Task entities by + folder path. """ con = get_server_api_connection() - return con.get_product_types( + return con.get_tasks_by_folder_paths( + project_name=project_name, + folder_paths=folder_paths, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, fields=fields, + own_attributes=own_attributes, ) -def get_project_product_types( +def get_tasks_by_folder_path( project_name: str, + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. - - Filter only product types available in a project. + own_attributes: bool = False, +) -> list[TaskDict]: + """Query task entities from server by folder path. Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - List[ProductTypeDict]: Product types information. + project_name (str): Name of project. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. """ con = get_server_api_connection() - return con.get_project_product_types( + return con.get_tasks_by_folder_path( project_name=project_name, + folder_path=folder_path, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, fields=fields, + own_attributes=own_attributes, ) -def get_product_type_names( - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """DEPRECATED Product type names. - - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. +def get_task_by_folder_path( + project_name: str, + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional[TaskDict]: + """Query task entity by folder path and task name. Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - set[str]: Product type names. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_product_type_names( + return con.get_task_by_folder_path( project_name=project_name, - product_ids=product_ids, + folder_path=folder_path, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def create_product( +def create_task( project_name: str, name: str, - product_type: str, + task_type: str, folder_id: str, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, status: Optional[str] = None, - active: "Union[bool, None]" = None, - product_id: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, ) -> str: - """Create new product. + """Create new task. Args: project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. + name (str): Folder name. + task_type (str): Task type. folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is + label (Optional[str]): Label of folder. + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is generated. Returns: - str: Product id. + str: Task id. """ con = get_server_api_connection() - return con.create_product( + return con.create_task( project_name=project_name, name=name, - product_type=product_type, + task_type=task_type, folder_id=folder_id, + label=label, + assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, - product_id=product_id, + thumbnail_id=thumbnail_id, + task_id=task_id, ) -def update_product( +def update_task( project_name: str, - product_id: str, + task_id: str, name: Optional[str] = None, + task_type: Optional[str] = None, folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, -): - """Update product entity on server. + thumbnail_id: Optional[str] = NOT_SET, +) -> None: + """Update task entity on server. + + Do not pass ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. Update of ``data`` will override existing value on folder entity. @@ -4606,238 +4732,160 @@ def update_product( Args: project_name (str): Project name. - product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. + task_id (str): Task id. + name (Optional[str]): New name. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. + label (Optional[Optional[str]]): New label. + assignees (Optional[str]): New assignees. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_product( + return con.update_task( project_name=project_name, - product_id=product_id, + task_id=task_id, name=name, + task_type=task_type, folder_id=folder_id, - product_type=product_type, + label=label, + assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, + thumbnail_id=thumbnail_id, ) -def delete_product( +def delete_task( project_name: str, - product_id: str, -): - """Delete product. + task_id: str, +) -> None: + """Delete task. Args: project_name (str): Project name. - product_id (str): Product id to delete. - - """ - con = get_server_api_connection() - return con.delete_product( - project_name=project_name, - product_id=product_id, - ) - - -def get_versions( - project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. - - Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Generator[VersionDict, None, None]: Queried version entities. - - """ - con = get_server_api_connection() - return con.get_versions( - project_name=project_name, - version_ids=version_ids, - product_ids=product_ids, - task_ids=task_ids, - versions=versions, - hero=hero, - standard=standard, - latest=latest, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_version_by_id( - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - con = get_server_api_connection() - return con.get_version_by_id( - project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_version_by_name( - project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by version and product id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + task_id (str): Task id to delete. """ con = get_server_api_connection() - return con.get_version_by_name( + return con.delete_task( + project_name=project_name, + task_id=task_id, + ) + + +def get_rest_product( + project_name: str, + product_id: str, +) -> Optional[ProductDict]: + con = get_server_api_connection() + return con.get_rest_product( project_name=project_name, - version=version, product_id=product_id, - fields=fields, - own_attributes=own_attributes, ) -def get_hero_version_by_id( +def get_products( project_name: str, - version_id: str, + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + product_types: Optional[Iterable[str]] = None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by id. +) -> Generator[ProductDict, None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. + project_name (str): Name of project. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + products. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Generator[ProductDict, None, None]: Queried product entities. """ con = get_server_api_connection() - return con.get_hero_version_by_id( + return con.get_products( project_name=project_name, - version_id=version_id, + product_ids=product_ids, + product_names=product_names, + folder_ids=folder_ids, + product_types=product_types, + product_name_regex=product_name_regex, + product_path_regex=product_path_regex, + names_by_folder_ids=names_by_folder_ids, + statuses=statuses, + tags=tags, + active=active, fields=fields, own_attributes=own_attributes, ) -def get_hero_version_by_product_id( +def get_product_by_id( project_name: str, product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by product id. - - Only one hero version is available on a product. +) -> Optional[ProductDict]: + """Query product entity by id. Args: project_name (str): Name of project where to look for queried entities. - product_id (int): Product id. + product_id (str): Product id. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + products. Returns: - Optional[VersionDict]: Version entity data or None + Optional[ProductDict]: Product entity data or None if was not found. """ con = get_server_api_connection() - return con.get_hero_version_by_product_id( + return con.get_product_by_id( project_name=project_name, product_id=product_id, fields=fields, @@ -4845,240 +4893,171 @@ def get_hero_version_by_product_id( ) -def get_hero_versions( +def get_product_by_name( project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + product_name: str, + folder_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. +) -> Optional[ProductDict]: + """Query product entity by name and folder id. Args: project_name (str): Name of project where to look for queried entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + products. Returns: - Optional[VersionDict]: Version entity data or None + Optional[ProductDict]: Product entity data or None if was not found. """ con = get_server_api_connection() - return con.get_hero_versions( + return con.get_product_by_name( project_name=project_name, - product_ids=product_ids, - version_ids=version_ids, - active=active, + product_name=product_name, + folder_id=folder_id, fields=fields, own_attributes=own_attributes, ) -def get_last_versions( - project_name: str, - product_ids: Iterable[str], - active: "Union[bool, None]" = True, +def get_product_types( fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. - - Args: - project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. - - """ - con = get_server_api_connection() - return con.get_last_versions( - project_name=project_name, - product_ids=product_ids, - active=active, - fields=fields, - own_attributes=own_attributes, - ) - +) -> list[ProductTypeDict]: + """Types of products. -def get_last_version_by_product_id( - project_name: str, - product_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product id. + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Optional[VersionDict]: Queried version entity or None. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_last_version_by_product_id( - project_name=project_name, - product_id=product_id, - active=active, + return con.get_product_types( fields=fields, - own_attributes=own_attributes, ) -def get_last_version_by_product_name( +def get_project_product_types( project_name: str, - product_name: str, - folder_id: str, - active: "Union[bool, None]" = True, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. +) -> list[ProductTypeDict]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Optional[VersionDict]: Queried version entity or None. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_last_version_by_product_name( + return con.get_project_product_types( project_name=project_name, - product_name=product_name, - folder_id=folder_id, - active=active, fields=fields, - own_attributes=own_attributes, ) -def version_is_latest( - project_name: str, - version_id: str, -) -> bool: - """Is version latest from a product. +def get_product_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """DEPRECATED Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. Returns: - bool: Version is latest or not. + set[str]: Product type names. """ con = get_server_api_connection() - return con.version_is_latest( + return con.get_product_type_names( project_name=project_name, - version_id=version_id, + product_ids=product_ids, ) -def create_version( +def create_product( project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, + product_id: Optional[str] = None, ) -> str: - """Create new version. + """Create new product. Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is + project_name (str): Project name. + name (str): Product name. + product_type (str): Product type. + folder_id (str): Parent folder id. + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is generated. Returns: - str: Version id. + str: Product id. """ con = get_server_api_connection() - return con.create_version( + return con.create_product( project_name=project_name, - version=version, - product_id=product_id, - task_id=task_id, - author=author, + name=name, + product_type=product_type, + folder_id=folder_id, attrib=attrib, data=data, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, - version_id=version_id, + product_id=product_id, ) -def update_version( +def update_product( project_name: str, - version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update version entity on server. - - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. +) -> None: + """Update product entity on server. Update of ``data`` will override existing value on folder entity. @@ -5087,462 +5066,490 @@ def update_version( Args: project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. - author (Optional[str]): New author username. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. """ con = get_server_api_connection() - return con.update_version( + return con.update_product( project_name=project_name, - version_id=version_id, - version=version, product_id=product_id, - task_id=task_id, - author=author, + name=name, + folder_id=folder_id, + product_type=product_type, attrib=attrib, data=data, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, ) -def delete_version( +def delete_product( project_name: str, - version_id: str, -): - """Delete version. + product_id: str, +) -> None: + """Delete product. Args: project_name (str): Project name. - version_id (str): Version id to delete. + product_id (str): Product id to delete. """ con = get_server_api_connection() - return con.delete_version( + return con.delete_product( + project_name=project_name, + product_id=product_id, + ) + + +def get_rest_version( + project_name: str, + version_id: str, +) -> Optional[VersionDict]: + con = get_server_api_connection() + return con.get_rest_version( project_name=project_name, version_id=version_id, ) -def get_representations( +def get_versions( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, + product_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[str] = None, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: - - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. +) -> Generator[VersionDict, None, None]: + """Get version entities based on passed filters from server. Args: project_name (str): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. statuses (Optional[Iterable[str]]): Representation statuses used for filtering. tags (Optional[Iterable[str]]): Representation tags used for filtering. active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + versions. Returns: - Generator[RepresentationDict, None, None]: Queried - representation entities. + Generator[VersionDict, None, None]: Queried version entities. """ con = get_server_api_connection() - return con.get_representations( + return con.get_versions( project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, version_ids=version_ids, - names_by_version_ids=names_by_version_ids, + product_ids=product_ids, + task_ids=task_ids, + versions=versions, + hero=hero, + standard=standard, + latest=latest, statuses=statuses, tags=tags, active=active, - has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_representation_by_id( +def get_version_by_id( project_name: str, - representation_id: str, + version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. +) -> Optional[VersionDict]: + """Query version entity by id. Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + versions. Returns: - Optional[RepresentationDict]: Queried representation - entity or None. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_representation_by_id( + return con.get_version_by_id( project_name=project_name, - representation_id=representation_id, + version_id=version_id, fields=fields, own_attributes=own_attributes, ) -def get_representation_by_name( +def get_version_by_name( + project_name: str, + version: int, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query version entity by version and product id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + con = get_server_api_connection() + return con.get_version_by_name( + project_name=project_name, + version=version, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_hero_version_by_id( project_name: str, - representation_name: str, version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. +) -> Optional[VersionDict]: + """Query hero version entity by id. Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + con = get_server_api_connection() + return con.get_hero_version_by_id( + project_name=project_name, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_hero_version_by_product_id( + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query hero version entity by product id. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + versions. Returns: - Optional[RepresentationDict]: Queried representation entity - or None. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_representation_by_name( + return con.get_hero_version_by_product_id( project_name=project_name, - representation_name=representation_name, - version_id=version_id, + product_id=product_id, fields=fields, own_attributes=own_attributes, ) -def get_representations_hierarchy( +def get_hero_versions( project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. - - Representation entity with parent entities up to project. + product_ids: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator[VersionDict, None, None]: + """Query hero versions by multiple filters. - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. + Only one hero version is available on a product. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_representations_hierarchy( + return con.get_hero_versions( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + product_ids=product_ids, + version_ids=version_ids, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_representation_hierarchy( +def get_last_versions( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. - - Representation parent entities up to project. + product_ids: Iterable[str], + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> dict[str, Optional[VersionDict]]: + """Query last version entities by product ids. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - RepresentationHierarchy: Representation hierarchy entities. + dict[str, Optional[VersionDict]]: Last versions by product id. """ con = get_server_api_connection() - return con.get_representation_hierarchy( + return con.get_last_versions( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + product_ids=product_ids, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_representations_parents( +def get_last_version_by_product_id( project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. + product_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query last version entity by product id. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_representations_parents( + return con.get_last_version_by_product_id( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + product_id=product_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_representation_parents( +def get_last_version_by_product_name( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. + product_name: str, + folder_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query last version entity by product name and folder id. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - RepresentationParents: Representation parent entities. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_representation_parents( + return con.get_last_version_by_product_name( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + product_name=product_name, + folder_id=folder_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_repre_ids_by_context_filters( +def version_is_latest( project_name: str, - context_filters: Optional[Dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, -) -> List[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. - - Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. - - Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) + version_id: str, +) -> bool: + """Is version latest from a product. + + Args: + project_name (str): Project where to look for representation. + version_id (str): Version id. + + Returns: + bool: Version is latest or not. """ con = get_server_api_connection() - return con.get_repre_ids_by_context_filters( + return con.version_is_latest( project_name=project_name, - context_filters=context_filters, - representation_names=representation_names, - version_ids=version_ids, + version_id=version_id, ) -def create_representation( +def create_version( project_name: str, - name: str, - version_id: str, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - representation_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, ) -> str: - """Create new representation. + """Create new version. Args: project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is + generated. Returns: - str: Representation id. + str: Version id. """ con = get_server_api_connection() - return con.create_representation( + return con.create_version( project_name=project_name, - name=name, - version_id=version_id, - files=files, + version=version, + product_id=product_id, + task_id=task_id, + author=author, attrib=attrib, data=data, - traits=traits, tags=tags, status=status, active=active, - representation_id=representation_id, + thumbnail_id=thumbnail_id, + version_id=version_id, ) -def update_representation( +def update_version( project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, -): - """Update representation entity on server. + thumbnail_id: Optional[str] = NOT_SET, +) -> None: + """Update version entity on server. + + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. Update of ``data`` will override existing value on folder entity. @@ -5551,515 +5558,642 @@ def update_representation( Args: project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[str]): New task id. + author (Optional[str]): New author username. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_representation( + return con.update_version( project_name=project_name, - representation_id=representation_id, - name=name, version_id=version_id, - files=files, + version=version, + product_id=product_id, + task_id=task_id, + author=author, attrib=attrib, data=data, - traits=traits, tags=tags, status=status, active=active, + thumbnail_id=thumbnail_id, ) -def delete_representation( +def delete_version( project_name: str, - representation_id: str, -): - """Delete representation. + version_id: str, +) -> None: + """Delete version. Args: project_name (str): Project name. - representation_id (str): Representation id to delete. + version_id (str): Version id to delete. """ con = get_server_api_connection() - return con.delete_representation( + return con.delete_version( + project_name=project_name, + version_id=version_id, + ) + + +def get_rest_representation( + project_name: str, + representation_id: str, +) -> Optional[RepresentationDict]: + con = get_server_api_connection() + return con.get_rest_representation( project_name=project_name, representation_id=representation_id, ) -def get_workfiles_info( +def get_representations( project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - paths: Optional[Iterable[str]] = None, - path_regex: Optional[str] = None, + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["WorkfileInfoDict", None, None]: - """Workfile info entities by passed filters. +) -> Generator[RepresentationDict, None, None]: + """Get representation entities based on passed filters from server. + + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used + tags (Optional[Iterable[str]]): Representation tags used for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + representations. Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. + Generator[RepresentationDict, None, None]: Queried + representation entities. """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.get_representations( project_name=project_name, - workfile_ids=workfile_ids, - task_ids=task_ids, - paths=paths, - path_regex=path_regex, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + names_by_version_ids=names_by_version_ids, statuses=statuses, tags=tags, + active=active, has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_workfile_info( - project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - con = get_server_api_connection() - return con.get_workfile_info( - project_name=project_name, - task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_workfile_info_by_id( +def get_representation_by_id( project_name: str, - workfile_id: str, + representation_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. +) -> Optional[RepresentationDict]: + """Query representation entity from server based on id filter. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + representations. Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + Optional[RepresentationDict]: Queried representation + entity or None. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.get_representation_by_id( project_name=project_name, - workfile_id=workfile_id, + representation_id=representation_id, fields=fields, own_attributes=own_attributes, - ) - - -def get_thumbnail_by_id( - project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. + ) - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + +def get_representation_by_name( + project_name: str, + representation_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[RepresentationDict]: + """Query representation entity by name and version id. Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Optional[RepresentationDict]: Queried representation entity + or None. """ con = get_server_api_connection() - return con.get_thumbnail_by_id( + return con.get_representation_by_name( project_name=project_name, - thumbnail_id=thumbnail_id, + representation_name=representation_name, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_thumbnail( +def get_representations_hierarchy( project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Get thumbnail from server. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. + Representation entity with parent entities up to project. - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, RepresentationHierarchy]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_thumbnail( + return con.get_representations_hierarchy( project_name=project_name, - entity_type=entity_type, - entity_id=entity_id, - thumbnail_id=thumbnail_id, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_folder_thumbnail( +def get_representation_hierarchy( project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + RepresentationHierarchy: Representation hierarchy entities. """ con = get_server_api_connection() - return con.get_folder_thumbnail( + return con.get_representation_hierarchy( project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_task_thumbnail( +def get_representations_parents( project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, RepresentationParents]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_task_thumbnail( + return con.get_representations_parents( project_name=project_name, - task_id=task_id, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_version_thumbnail( +def get_representation_parents( project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> Optional[RepresentationParents]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + RepresentationParents: Representation parent entities. """ con = get_server_api_connection() - return con.get_version_thumbnail( + return con.get_representation_parents( project_name=project_name, - version_id=version_id, - thumbnail_id=thumbnail_id, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_workfile_thumbnail( +def get_repre_ids_by_context_filters( project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. + context_filters: Optional[dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, +) -> list[str]: + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> from ayon_api import get_repre_ids_by_context_filters + >>> from ayon_api import get_representations + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) """ con = get_server_api_connection() - return con.get_workfile_thumbnail( + return con.get_repre_ids_by_context_filters( project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, + context_filters=context_filters, + representation_names=representation_names, + version_ids=version_ids, ) -def create_thumbnail( +def create_representation( project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, + name: str, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + representation_id: Optional[str] = None, ) -> str: - """Create new thumbnail on server from passed path. + """Create new representation. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. + project_name (str): Project name. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. + str: Representation id. """ con = get_server_api_connection() - return con.create_thumbnail( + return con.create_representation( project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, + name=name, + version_id=version_id, + files=files, + attrib=attrib, + data=data, + traits=traits, + tags=tags, + status=status, + active=active, + representation_id=representation_id, ) -def update_thumbnail( +def update_representation( project_name: str, - thumbnail_id: str, - src_filepath: str, -): - """Change thumbnail content by id. + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update representation entity on server. - Update can be also used to create new thumbnail. + Update of ``data`` will override existing value on folder entity. - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. - Raises: - ValueError: When thumbnail source cannot be processed. + Args: + project_name (str): Project name. + representation_id (str): Representation id. + name (Optional[str]): New name. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. """ con = get_server_api_connection() - return con.update_thumbnail( + return con.update_representation( project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, + representation_id=representation_id, + name=name, + version_id=version_id, + files=files, + attrib=attrib, + data=data, + traits=traits, + tags=tags, + status=status, + active=active, ) -def create_project( +def delete_representation( project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, -) -> "ProjectDict": - """Create project using AYON settings. + representation_id: str, +) -> None: + """Delete representation. - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. + Args: + project_name (str): Project name. + representation_id (str): Representation id to delete. - Entered project name must be unique and project must not exist yet. + """ + con = get_server_api_connection() + return con.delete_representation( + project_name=project_name, + representation_id=representation_id, + ) - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. +def get_workfiles_info( + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator[WorkfileInfoDict, None, None]: + """Workfile info entities by passed filters. - Raises: - ValueError: When project name already exists. + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. Returns: - ProjectDict: Created project entity. + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. """ con = get_server_api_connection() - return con.create_project( + return con.get_workfiles_info( project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, ) -def update_project( +def get_workfile_info( project_name: str, - library: Optional[bool] = None, - folder_types: Optional[List[Dict[str, Any]]] = None, - task_types: Optional[List[Dict[str, Any]]] = None, - link_types: Optional[List[Dict[str, Any]]] = None, - statuses: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[Dict[str, Any]]] = None, - config: Optional[Dict[str, Any]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes, -): - """Update project entity on server. + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """Workfile info entity by task id and workfile path. Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.update_project( + return con.get_workfile_info( project_name=project_name, - library=library, - folder_types=folder_types, - task_types=task_types, - link_types=link_types, - statuses=statuses, - tags=tags, - config=config, - attrib=attrib, - data=data, - active=active, - project_code=project_code, - **changes, + task_id=task_id, + path=path, + fields=fields, + own_attributes=own_attributes, ) -def delete_project( +def get_workfile_info_by_id( project_name: str, -): - """Delete project from server. - - This will completely remove project from server without any step back. + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """Workfile info entity by id. Args: - project_name (str): Project name that will be removed. + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.delete_project( + return con.get_workfile_info_by_id( project_name=project_name, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, ) @@ -6089,7 +6223,7 @@ def get_full_link_type_name( def get_link_types( project_name: str, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """All link types available on a project. Example output: @@ -6121,7 +6255,7 @@ def get_link_type( link_type_name: str, input_type: str, output_type: str, -) -> Optional[str]: +) -> Optional[dict[str, Any]]: """Get link type data. There is not dedicated REST endpoint to get single link type, @@ -6143,7 +6277,7 @@ def get_link_type( output_type (str): Output entity type of link. Returns: - Optional[str]: Link type information. + Optional[dict[str, Any]]: Link type information. """ con = get_server_api_connection() @@ -6160,8 +6294,8 @@ def create_link_type( link_type_name: str, input_type: str, output_type: str, - data: Optional[Dict[str, Any]] = None, -): + data: Optional[dict[str, Any]] = None, +) -> None: """Create or update link type on server. Warning: @@ -6193,7 +6327,7 @@ def delete_link_type( link_type_name: str, input_type: str, output_type: str, -): +) -> None: """Remove link type from project. Args: @@ -6220,8 +6354,8 @@ def make_sure_link_type_exists( link_type_name: str, input_type: str, output_type: str, - data: Optional[Dict[str, Any]] = None, -): + data: Optional[dict[str, Any]] = None, +) -> None: """Make sure link type exists on a project. Args: @@ -6250,7 +6384,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, -): +) -> CreateLinkData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -6272,7 +6406,7 @@ def create_link( Available from server version '1.0.0-rc.6'. Returns: - dict[str, str]: Information about link. + CreateLinkData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -6293,7 +6427,7 @@ def create_link( def delete_link( project_name: str, link_id: str, -): +) -> None: """Remove link by id. Args: @@ -6316,10 +6450,10 @@ def get_entities_links( entity_type: str, entity_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, link_names: Optional[Iterable[str]] = None, link_name_regex: Optional[str] = None, -) -> Dict[str, List[Dict[str, Any]]]: +) -> dict[str, list[dict[str, Any]]]: """Helper method to get links from server for entity types. .. highlight:: text @@ -6375,8 +6509,8 @@ def get_folders_links( project_name: str, folder_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: + link_direction: Optional[LinkDirection] = None, +) -> dict[str, list[dict[str, Any]]]: """Query folders links from server. Args: @@ -6404,8 +6538,8 @@ def get_folder_links( project_name: str, folder_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: """Query folder links from server. Args: @@ -6432,528 +6566,228 @@ def get_tasks_links( project_name: str, task_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: + link_direction: Optional[LinkDirection] = None, +) -> dict[str, list[dict[str, Any]]]: """Query tasks links from server. Args: project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. - - """ - con = get_server_api_connection() - return con.get_tasks_links( - project_name=project_name, - task_ids=task_ids, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_task_links( - project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query task links from server. - - Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of task. - - """ - con = get_server_api_connection() - return con.get_task_links( - project_name=project_name, - task_id=task_id, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_products_links( - project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query products links from server. - - Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. - - """ - con = get_server_api_connection() - return con.get_products_links( - project_name=project_name, - product_ids=product_ids, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_product_links( - project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query product links from server. - - Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of product. - - """ - con = get_server_api_connection() - return con.get_product_links( - project_name=project_name, - product_id=product_id, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_versions_links( - project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query versions links from server. - - Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. - - """ - con = get_server_api_connection() - return con.get_versions_links( - project_name=project_name, - version_ids=version_ids, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_version_links( - project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query version links from server. - - Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of version. - - """ - con = get_server_api_connection() - return con.get_version_links( - project_name=project_name, - version_id=version_id, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_representations_links( - project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query representations links from server. - - Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. - - """ - con = get_server_api_connection() - return con.get_representations_links( - project_name=project_name, - representation_ids=representation_ids, - link_types=link_types, - link_direction=link_direction, - ) - - -def get_representation_links( - project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query representation links from server. - - Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of representation. - - """ - con = get_server_api_connection() - return con.get_representation_links( - project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, - ) - - -def send_batch_operations( - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD operations to server. - - When multiple changes should be made on server side this is the best - way to go. It is possible to pass multiple operations to process on a - server side and do the changes in a transaction. - - Args: - project_name (str): On which project should be operations - processed. - operations (list[dict[str, Any]]): Operations to be processed. - can_fail (Optional[bool]): Server will try to process all - operations even if one of them fails. - raise_on_fail (Optional[bool]): Raise exception if an operation - fails. You can handle failed operations on your own - when set to 'False'. - - Raises: - ValueError: Operations can't be converted to json string. - FailedOperations: When output does not contain server operations - or 'raise_on_fail' is enabled and any operation fails. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - list[dict[str, Any]]: Operations result with process details. + dict[str, list[dict[str, Any]]]: Link info by task ids. """ con = get_server_api_connection() - return con.send_batch_operations( + return con.get_tasks_links( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, ) -def send_activities_batch_operations( +def get_task_links( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD activities operations to server. - - When multiple changes should be made on server side this is the best - way to go. It is possible to pass multiple operations to process on a - server side and do the changes in a transaction. + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: + """Query task links from server. Args: - project_name (str): On which project should be operations - processed. - operations (list[dict[str, Any]]): Operations to be processed. - can_fail (Optional[bool]): Server will try to process all - operations even if one of them fails. - raise_on_fail (Optional[bool]): Raise exception if an operation - fails. You can handle failed operations on your own - when set to 'False'. - - Raises: - ValueError: Operations can't be converted to json string. - FailedOperations: When output does not contain server operations - or 'raise_on_fail' is enabled and any operation fails. + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - list[dict[str, Any]]: Operations result with process details. + list[dict[str, Any]]: Link info of task. """ con = get_server_api_connection() - return con.send_activities_batch_operations( + return con.get_task_links( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + task_id=task_id, + link_types=link_types, + link_direction=link_direction, ) -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. +def get_products_links( + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - List[ActionManifestDict]: List of action manifests. + dict[str, list[dict[str, Any]]]: Link info by product ids. """ con = get_server_api_connection() - return con.get_actions( + return con.get_products_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, ) -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. +def get_product_links( + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: + """Query product links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. """ con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_product_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + product_id=product_id, + link_types=link_types, + link_direction=link_direction, ) -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. +def get_versions_links( + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionConfigResponse: Action configuration data. + dict[str, list[dict[str, Any]]]: Link info by version ids. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_versions_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. +def get_version_links( + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: + """Query version links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionConfigResponse: New action configuration data. + list[dict[str, Any]]: Link info of version. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.get_version_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def get_representations_links( + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. Args: - action_token (str): AYON launcher action token. + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.get_representations_links( + project_name=project_name, + representation_ids=representation_ids, + link_types=link_types, + link_direction=link_direction, ) -def abort_action( - action_token: str, - message: Optional[str] = None, -) -> None: - """Abort action using an action token. +def get_representation_links( + project_name: str, + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: + """Query representation links from server. Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, + return con.get_representation_links( + project_name=project_name, + representation_id=representation_id, + link_types=link_types, + link_direction=link_direction, ) @@ -6963,7 +6797,7 @@ def get_entity_lists( list_ids: Optional[Iterable[str]] = None, active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: +) -> Generator[dict[str, Any], None, None]: """Fetch entity lists from server. Args: @@ -6974,7 +6808,7 @@ def get_entity_lists( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities + Generator[dict[str, Any], None, None]: Entity list entities matching defined filters. """ @@ -6990,7 +6824,7 @@ def get_entity_lists( def get_entity_list_rest( project_name: str, list_id: str, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get entity list by id using REST API. Args: @@ -6998,7 +6832,7 @@ def get_entity_list_rest( list_id (str): Entity list id. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() @@ -7012,7 +6846,7 @@ def get_entity_list_by_id( project_name: str, list_id: str, fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get entity list by id using GraphQl. Args: @@ -7021,7 +6855,7 @@ def get_entity_list_by_id( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() @@ -7034,18 +6868,18 @@ def get_entity_list_by_id( def create_entity_list( project_name: str, - entity_type: "EntityListEntityType", + entity_type: EntityListEntityType, label: str, *, list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, + template: Optional[dict[str, Any]] = None, owner: Optional[str] = None, active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, + items: Optional[list[dict[str, Any]]] = None, list_id: Optional[str] = None, ) -> str: """Create entity list. @@ -7092,10 +6926,10 @@ def update_entity_list( list_id: str, *, label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -7149,7 +6983,7 @@ def delete_entity_list( def get_entity_list_attribute_definitions( project_name: str, list_id: str, -) -> List["EntityListAttributeDefinitionDict"]: +) -> list[EntityListAttributeDefinitionDict]: """Get attribute definitioins on entity list. Args: @@ -7157,7 +6991,7 @@ def get_entity_list_attribute_definitions( list_id (str): Entity list id. Returns: - List[EntityListAttributeDefinitionDict]: List of attribute + list[EntityListAttributeDefinitionDict]: List of attribute definitions. """ @@ -7171,14 +7005,14 @@ def get_entity_list_attribute_definitions( def set_entity_list_attribute_definitions( project_name: str, list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], + attribute_definitions: list[EntityListAttributeDefinitionDict], ) -> None: """Set attribute definitioins on entity list. Args: project_name (str): Project name. list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): + attribute_definitions (list[EntityListAttributeDefinitionDict]): List of attribute definitions. """ @@ -7196,9 +7030,9 @@ def create_entity_list_item( *, position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, item_id: Optional[str] = None, ) -> str: """Create entity list item. @@ -7233,15 +7067,15 @@ def create_entity_list_item( def update_entity_list_items( project_name: str, list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", + items: list[dict[str, Any]], + mode: EntityListItemMode, ) -> None: """Update items in entity list. Args: project_name (str): Project name where entity list live. list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. + items (list[dict[str, Any]]): Entity list items. mode (EntityListItemMode): Mode of items update. """ @@ -7262,9 +7096,9 @@ def update_entity_list_item( new_list_id: Optional[str], position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, ) -> None: """Update item in entity list. @@ -7316,3 +7150,237 @@ def delete_entity_list_item( list_id=list_id, item_id=item_id, ) + + +def get_thumbnail_by_id( + project_name: str, + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. + + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_thumbnail_by_id( + project_name=project_name, + thumbnail_id=thumbnail_id, + ) + + +def get_thumbnail( + project_name: str, + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Get thumbnail from server. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_thumbnail( + project_name=project_name, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, + ) + + +def get_folder_thumbnail( + project_name: str, + folder_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. + + Args: + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_folder_thumbnail( + project_name=project_name, + folder_id=folder_id, + thumbnail_id=thumbnail_id, + ) + + +def get_task_thumbnail( + project_name: str, + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_task_thumbnail( + project_name=project_name, + task_id=task_id, + ) + + +def get_version_thumbnail( + project_name: str, + version_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. + + Args: + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_version_thumbnail( + project_name=project_name, + version_id=version_id, + thumbnail_id=thumbnail_id, + ) + + +def get_workfile_thumbnail( + project_name: str, + workfile_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_workfile_thumbnail( + project_name=project_name, + workfile_id=workfile_id, + thumbnail_id=thumbnail_id, + ) + + +def create_thumbnail( + project_name: str, + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + con = get_server_api_connection() + return con.create_thumbnail( + project_name=project_name, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, + ) + + +def update_thumbnail( + project_name: str, + thumbnail_id: str, + src_filepath: str, +) -> None: + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + con = get_server_api_connection() + return con.update_thumbnail( + project_name=project_name, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, + ) diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py new file mode 100644 index 000000000..6e81904a8 --- /dev/null +++ b/ayon_api/_api_helpers/__init__.py @@ -0,0 +1,42 @@ +from .base import BaseServerAPI +from .installers import InstallersAPI +from .dependency_packages import DependencyPackagesAPI +from .secrets import SecretsAPI +from .bundles_addons import BundlesAddonsAPI +from .events import EventsAPI +from .attributes import AttributesAPI +from .projects import ProjectsAPI +from .folders import FoldersAPI +from .tasks import TasksAPI +from .products import ProductsAPI +from .versions import VersionsAPI +from .representations import RepresentationsAPI +from .workfiles import WorkfilesAPI +from .thumbnails import ThumbnailsAPI +from .activities import ActivitiesAPI +from .actions import ActionsAPI +from .links import LinksAPI +from .lists import ListsAPI + + +__all__ = ( + "BaseServerAPI", + "InstallersAPI", + "DependencyPackagesAPI", + "SecretsAPI", + "BundlesAddonsAPI", + "EventsAPI", + "AttributesAPI", + "ProjectsAPI", + "FoldersAPI", + "TasksAPI", + "ProductsAPI", + "VersionsAPI", + "RepresentationsAPI", + "WorkfilesAPI", + "ThumbnailsAPI", + "ActivitiesAPI", + "ActionsAPI", + "LinksAPI", + "ListsAPI", +) diff --git a/ayon_api/_actions.py b/ayon_api/_api_helpers/actions.py similarity index 76% rename from ayon_api/_actions.py rename to ayon_api/_api_helpers/actions.py index b575189dd..37ec2f519 100644 --- a/ayon_api/_actions.py +++ b/ayon_api/_api_helpers/actions.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import typing -from typing import Optional, Dict, List, Any +from typing import Optional, Any + +from ayon_api.utils import prepare_query_string -from .utils import prepare_query_string -from ._base import _BaseServerAPI +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( ActionEntityTypes, ActionManifestDict, ActionTriggerResponse, @@ -15,19 +18,19 @@ ) -class _ActionsAPI(_BaseServerAPI): +class ActionsAPI(BaseServerAPI): """Implementation of actions API for ServerAPI.""" def get_actions( self, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, - ) -> List["ActionManifestDict"]: + mode: Optional[ActionModeType] = None, + ) -> list[ActionManifestDict]: """Get actions for a context. Args: @@ -35,16 +38,16 @@ def get_actions( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. mode (Optional[ActionModeType]): Action modes. Returns: - List[ActionManifestDict]: List of action manifests. + list[ActionManifestDict]: list of action manifests. """ if variant is None: @@ -74,13 +77,13 @@ def trigger_action( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionTriggerResponse": + ) -> ActionTriggerResponse: """Trigger action. Args: @@ -91,11 +94,11 @@ def trigger_action( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. """ @@ -131,13 +134,13 @@ def get_action_config( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Get action configuration. Args: @@ -148,11 +151,11 @@ def get_action_config( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. Returns: @@ -177,32 +180,32 @@ def set_action_config( identifier: str, addon_name: str, addon_version: str, - value: Dict[str, Any], + value: dict[str, Any], project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_type: Optional[ActionEntityTypes] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Set action configuration. Args: identifier (str): Identifier of the action. addon_name (str): Name of the addon. addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action + value (Optional[dict[str, Any]]): Value of the action configuration. project_name (Optional[str]): Name of the project. None for global actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. Returns: @@ -222,7 +225,7 @@ def set_action_config( variant, ) - def take_action(self, action_token: str) -> "ActionTakeResponse": + def take_action(self, action_token: str) -> ActionTakeResponse: """Take action metadata using an action token. Args: @@ -262,14 +265,14 @@ def _send_config_request( identifier: str, addon_name: str, addon_version: str, - value: Optional[Dict[str, Any]], + value: Optional[dict[str, Any]], project_name: Optional[str], - entity_type: Optional["ActionEntityTypes"], - entity_ids: Optional[List[str]], - entity_subtypes: Optional[List[str]], - form_data: Optional[Dict[str, Any]], + entity_type: Optional[ActionEntityTypes], + entity_ids: Optional[list[str]], + entity_subtypes: Optional[list[str]], + form_data: Optional[dict[str, Any]], variant: Optional[str], - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Set and get action configuration.""" if variant is None: variant = self.get_default_settings_variant() diff --git a/ayon_api/_api_helpers/activities.py b/ayon_api/_api_helpers/activities.py new file mode 100644 index 000000000..f7ae3d4f7 --- /dev/null +++ b/ayon_api/_api_helpers/activities.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +import json +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.utils import ( + SortOrder, + prepare_list_filters, +) +from ayon_api.graphql_queries import activities_graphql_query + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + ActivityType, + ActivityReferenceType, + ) + + +class ActivitiesAPI(BaseServerAPI): + def get_activities( + self, + project_name: str, + activity_ids: Optional[Iterable[str]] = None, + activity_types: Optional[Iterable[ActivityType]] = None, + entity_ids: Optional[Iterable[str]] = None, + entity_names: Optional[Iterable[str]] = None, + entity_type: Optional[str] = None, + changed_after: Optional[str] = None, + changed_before: Optional[str] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + ) -> Generator[dict[str, Any], None, None]: + """Get activities from server with filtering options. + + Args: + project_name (str): Project on which activities happened. + activity_ids (Optional[Iterable[str]]): Activity ids. + activity_types (Optional[Iterable[ActivityType]]): Activity types. + entity_ids (Optional[Iterable[str]]): Entity ids. + entity_names (Optional[Iterable[str]]): Entity names. + entity_type (Optional[str]): Entity type. + changed_after (Optional[str]): Return only activities changed + after given iso datetime string. + changed_before (Optional[str]): Return only activities changed + before given iso datetime string. + reference_types (Optional[Iterable[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. + + """ + if not project_name: + return + filters = { + "projectName": project_name, + } + if reference_types is None: + reference_types = {"origin"} + + if not prepare_list_filters( + filters, + ("activityIds", activity_ids), + ("activityTypes", activity_types), + ("entityIds", entity_ids), + ("entityNames", entity_names), + ("referenceTypes", reference_types), + ): + return + + for filter_key, filter_value in ( + ("entityType", entity_type), + ("changedAfter", changed_after), + ("changedBefore", changed_before), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if not fields: + fields = self.get_default_fields_for_type("activity") + + query = activities_graphql_query(set(fields), order) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + if limit: + activities_field = query.get_field_by_path("activities") + activities_field.set_limit(limit) + + for parsed_data in query.continuous_query(self): + for activity in parsed_data["project"]["activities"]: + activity_data = activity.get("activityData") + if isinstance(activity_data, str): + activity["activityData"] = json.loads(activity_data) + yield activity + + def get_activity_by_id( + self, + project_name: str, + activity_id: str, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, + fields: Optional[Iterable[str]] = None, + ) -> Optional[dict[str, Any]]: + """Get activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Optional[dict[str, Any]]: Activity data or None if activity is not + found. + + """ + for activity in self.get_activities( + project_name=project_name, + activity_ids={activity_id}, + reference_types=reference_types, + fields=fields, + ): + return activity + return None + + def create_activity( + self, + project_name: str, + entity_id: str, + entity_type: str, + activity_type: ActivityType, + activity_id: Optional[str] = None, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + timestamp: Optional[str] = None, + data: Optional[dict[str, Any]] = None, + ) -> str: + """Create activity on a project. + + Args: + project_name (str): Project on which activity happened. + entity_id (str): Entity id. + entity_type (str): Entity type. + activity_type (ActivityType): Activity type. + activity_id (Optional[str]): Activity id. + body (Optional[str]): Activity body. + file_ids (Optional[list[str]]): List of file ids attached + to activity. + timestamp (Optional[str]): Activity timestamp. + data (Optional[dict[str, Any]]): Additional data. + + Returns: + str: Activity id. + + """ + post_data = { + "activityType": activity_type, + } + for key, value in ( + ("id", activity_id), + ("body", body), + ("files", file_ids), + ("timestamp", timestamp), + ("data", data), + ): + if value is not None: + post_data[key] = value + + response = self.post( + f"projects/{project_name}/{entity_type}/{entity_id}/activities", + **post_data + ) + response.raise_for_status() + return response.data["id"] + + def update_activity( + self, + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = None, + ) -> None: + """Update activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + body (str): Activity body. + file_ids (Optional[list[str]]): List of file ids attached + to activity. + append_file_ids (Optional[bool]): Append file ids to existing + list of file ids. + data (Optional[dict[str, Any]]): Update data in activity. + + """ + update_data = {} + major, minor, patch, _, _ = self.get_server_version_tuple() + new_patch_model = (major, minor, patch) > (1, 5, 6) + if body is None and not new_patch_model: + raise ValueError( + "Update without 'body' is supported" + " after server version 1.5.6." + ) + + if body is not None: + update_data["body"] = body + + if file_ids is not None: + update_data["files"] = file_ids + if new_patch_model: + update_data["appendFiles"] = append_file_ids + elif append_file_ids: + raise ValueError( + "Append file ids is supported after server version 1.5.6." + ) + + if data is not None: + if not new_patch_model: + raise ValueError( + "Update of data is supported after server version 1.5.6." + ) + update_data["data"] = data + + response = self.patch( + f"projects/{project_name}/activities/{activity_id}", + **update_data + ) + response.raise_for_status() + + def delete_activity(self, project_name: str, activity_id: str) -> None: + """Delete activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. + + """ + response = self.delete( + f"projects/{project_name}/activities/{activity_id}" + ) + response.raise_for_status() + + def send_activities_batch_operations( + self, + project_name: str, + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True + ) -> list[dict[str, Any]]: + """Post multiple CRUD activities operations to server. + + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. + + Args: + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. + + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. + + Returns: + list[dict[str, Any]]: Operations result with process details. + + """ + return self._send_batch_operations( + f"projects/{project_name}/operations/activities", + operations, + can_fail, + raise_on_fail, + ) diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py new file mode 100644 index 000000000..26db47484 --- /dev/null +++ b/ayon_api/_api_helpers/attributes.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import typing +from typing import Optional +import copy + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + AttributeSchemaDataDict, + AttributeSchemaDict, + AttributesSchemaDict, + AttributeScope, + ) + + +class AttributesAPI(BaseServerAPI): + _attributes_schema = None + _entity_type_attributes_cache = {} + + def get_attributes_schema( + self, use_cache: bool = True + ) -> AttributesSchemaDict: + if not use_cache: + self.reset_attributes_schema() + + if self._attributes_schema is None: + result = self.get("attributes") + result.raise_for_status() + self._attributes_schema = result.data + return copy.deepcopy(self._attributes_schema) + + def reset_attributes_schema(self) -> None: + self._attributes_schema = None + self._entity_type_attributes_cache = {} + + def set_attribute_config( + self, + attribute_name: str, + data: AttributeSchemaDataDict, + scope: list[AttributeScope], + position: Optional[int] = None, + builtin: bool = False, + ) -> None: + if position is None: + attributes = self.get("attributes").data["attributes"] + origin_attr = next( + ( + attr for attr in attributes + if attr["name"] == attribute_name + ), + None + ) + if origin_attr: + position = origin_attr["position"] + else: + position = len(attributes) + + response = self.put( + f"attributes/{attribute_name}", + data=data, + scope=scope, + position=position, + builtin=builtin + ) + if response.status_code != 204: + # TODO raise different exception + raise ValueError( + f"Attribute \"{attribute_name}\" was not created/updated." + f" {response.detail}" + ) + + self.reset_attributes_schema() + + def remove_attribute_config(self, attribute_name: str) -> None: + """Remove attribute from server. + + This can't be un-done, please use carefully. + + Args: + attribute_name (str): Name of attribute to remove. + + """ + response = self.delete(f"attributes/{attribute_name}") + response.raise_for_status( + f"Attribute \"{attribute_name}\" was not created/updated." + f" {response.detail}" + ) + + self.reset_attributes_schema() + + def get_attributes_for_type( + self, entity_type: AttributeScope + ) -> dict[str, AttributeSchemaDict]: + """Get attribute schemas available for an entity type. + + Example:: + + ``` + # Example attribute schema + { + # Common + "type": "integer", + "title": "Clip Out", + "description": null, + "example": 1, + "default": 1, + # These can be filled based on value of 'type' + "gt": null, + "ge": null, + "lt": null, + "le": null, + "minLength": null, + "maxLength": null, + "minItems": null, + "maxItems": null, + "regex": null, + "enum": null + } + ``` + + Args: + entity_type (str): Entity type for which should be attributes + received. + + Returns: + dict[str, dict[str, Any]]: Attribute schemas that are available + for entered entity type. + + """ + attributes = self._entity_type_attributes_cache.get(entity_type) + if attributes is None: + attributes_schema = self.get_attributes_schema() + attributes = {} + for attr in attributes_schema["attributes"]: + if entity_type not in attr["scope"]: + continue + attr_name = attr["name"] + attributes[attr_name] = attr["data"] + + self._entity_type_attributes_cache[entity_type] = attributes + + return copy.deepcopy(attributes) + + def get_attributes_fields_for_type( + self, entity_type: AttributeScope + ) -> set[str]: + """Prepare attribute fields for entity type. + + Returns: + set[str]: Attributes fields for entity type. + + """ + attributes = self.get_attributes_for_type(entity_type) + return { + f"attrib.{attr}" + for attr in attributes + } diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py new file mode 100644 index 000000000..f49b10a9b --- /dev/null +++ b/ayon_api/_api_helpers/base.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import logging +import typing +from typing import Optional, Any, Iterable + +import requests + +from ayon_api.utils import TransferProgress, RequestType + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + AnyEntityDict, + ServerVersion, + ProjectDict, + ) + +_PLACEHOLDER = object() + + +class BaseServerAPI: + @property + def log(self) -> logging.Logger: + raise NotImplementedError() + + def get_server_version(self) -> str: + raise NotImplementedError() + + def get_server_version_tuple(self) -> ServerVersion: + raise NotImplementedError() + + def get_base_url(self) -> str: + raise NotImplementedError() + + def get_rest_url(self) -> str: + raise NotImplementedError() + + def get(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def post(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def put(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def patch(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def delete(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_get(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_post(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_put(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_patch(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_delete(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def get_default_settings_variant(self) -> str: + raise NotImplementedError() + + def get_site_id(self) -> Optional[str]: + raise NotImplementedError() + + def get_default_fields_for_type(self, entity_type: str) -> set[str]: + raise NotImplementedError() + + def upload_file( + self, + endpoint: str, + filepath: str, + progress: Optional[TransferProgress] = None, + request_type: Optional[RequestType] = None, + **kwargs + ) -> requests.Response: + raise NotImplementedError() + + def download_file( + self, + endpoint: str, + filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + raise NotImplementedError() + + def get_rest_entity_by_id( + self, + project_name: str, + entity_type: str, + entity_id: str, + ) -> Optional[AnyEntityDict]: + raise NotImplementedError() + + def get_project( + self, + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[ProjectDict]: + raise NotImplementedError() + + def _prepare_fields( + self, + entity_type: str, + fields: set[str], + own_attributes: bool = False, + ): + raise NotImplementedError() + + def _convert_entity_data(self, entity: AnyEntityDict): + raise NotImplementedError() + + def _send_batch_operations( + self, + uri: str, + operations: list[dict[str, Any]], + can_fail: bool, + raise_on_fail: bool + ) -> list[dict[str, Any]]: + raise NotImplementedError() diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py new file mode 100644 index 000000000..1b70967ec --- /dev/null +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -0,0 +1,883 @@ +from __future__ import annotations + +import os +import typing +from typing import Optional, Any + +from ayon_api.utils import ( + RequestTypes, + prepare_query_string, + TransferProgress, +) + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + AddonsInfoDict, + BundlesInfoDict, + DevBundleAddonInfoDict, + ) + + +class BundlesAddonsAPI(BaseServerAPI): + def get_bundles(self) -> BundlesInfoDict: + """Server bundles with basic information. + + This is example output:: + + { + "bundles": [ + { + "name": "my_bundle", + "createdAt": "2023-06-12T15:37:02.420260", + "installerVersion": "1.0.0", + "addons": { + "core": "1.2.3" + }, + "dependencyPackages": { + "windows": "a_windows_package123.zip", + "linux": "a_linux_package123.zip", + "darwin": "a_mac_package123.zip" + }, + "isProduction": False, + "isStaging": False + } + ], + "productionBundle": "my_bundle", + "stagingBundle": "test_bundle" + } + + Returns: + dict[str, Any]: Server bundles with basic information. + + """ + response = self.get("bundles") + response.raise_for_status() + return response.data + + def create_bundle( + self, + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, + ) -> None: + """Create bundle on server. + + Bundle cannot be changed once is created. Only isProduction, isStaging + and dependency packages can change after creation. In case dev bundle + is created, it is possible to change anything, but it is not possible + to mark bundle as dev and production or staging at the same time. + + Development addon config can define custom path to client code. It is + used only for dev bundles. + + Example of 'dev_addons_config':: + + ```json + { + "core": { + "enabled": true, + "path": "/path/to/ayon-core/client" + } + } + ``` + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. + + """ + body = { + "name": name, + "installerVersion": installer_version, + "addons": addon_versions, + } + + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ): + if value is not None: + body[key] = value + + response = self.post("bundles", **body) + response.raise_for_status() + + def update_bundle( + self, + bundle_name: str, + addon_versions: Optional[dict[str, str]] = None, + installer_version: Optional[str] = None, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, + ) -> None: + """Update bundle on server. + + Dependency packages can be update only for single platform. Others + will be left untouched. Use 'None' value to unset dependency package + from bundle. + + Args: + bundle_name (str): Name of bundle. + addon_versions (Optional[dict[str, str]]): Addon versions, + possible only for dev bundles. + installer_version (Optional[str]): Installer version, possible + only for dev bundles. + dependency_packages (Optional[dict[str, str]]): Dependency pacakge + names that should be used with the bundle. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only for dev bundles. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only for dev bundles. + + """ + body = { + key: value + for key, value in ( + ("installerVersion", installer_version), + ("addons", addon_versions), + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ) + if value is not None + } + + response = self.patch( + f"bundles/{bundle_name}", + **body + ) + response.raise_for_status() + + def check_bundle_compatibility( + self, + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, + ) -> dict[str, Any]: + """Check bundle compatibility. + + Can be used as per-flight validation before creating bundle. + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. + + Returns: + dict[str, Any]: Server response, with 'success' and 'issues'. + + """ + body = { + "name": name, + "installerVersion": installer_version, + "addons": addon_versions, + } + + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ): + if value is not None: + body[key] = value + + response = self.post("bundles/check", **body) + response.raise_for_status() + return response.data + + def delete_bundle(self, bundle_name: str) -> None: + """Delete bundle from server. + + Args: + bundle_name (str): Name of bundle to delete. + + """ + response = self.delete(f"bundles/{bundle_name}") + response.raise_for_status() + + def get_addon_endpoint( + self, + addon_name: str, + addon_version: str, + *subpaths: str, + ) -> str: + """Calculate endpoint to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + + Returns: + str: Final url. + + """ + ending = "" + if subpaths: + ending = f"/{'/'.join(subpaths)}" + return f"addons/{addon_name}/{addon_version}{ending}" + + def get_addons_info(self, details: bool = True) -> AddonsInfoDict: + """Get information about addons available on server. + + Args: + details (Optional[bool]): Detailed data with information how + to get client code. + + """ + endpoint = "addons" + if details: + endpoint += "?details=1" + response = self.get(endpoint) + response.raise_for_status() + return response.data + + def get_addon_url( + self, + addon_name: str, + addon_version: str, + *subpaths: str, + use_rest: bool = True, + ) -> str: + """Calculate url to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. + + Returns: + str: Final url. + + """ + endpoint = self.get_addon_endpoint( + addon_name, addon_version, *subpaths + ) + url_base = self.get_base_url() if use_rest else self.get_rest_url() + return f"{url_base}/{endpoint}" + + def delete_addon( + self, + addon_name: str, + purge: Optional[bool] = None, + ) -> None: + """Delete addon from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. + + """ + if purge is not None: + purge = "true" if purge else "false" + query = prepare_query_string({"purge": purge}) + + response = self.delete(f"addons/{addon_name}{query}") + response.raise_for_status() + + def delete_addon_version( + self, + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, + ) -> None: + """Delete addon version from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. + + """ + if purge is not None: + purge = "true" if purge else "false" + query = prepare_query_string({"purge": purge}) + response = self.delete(f"addons/{addon_name}/{addon_version}{query}") + response.raise_for_status() + + def upload_addon_zip( + self, + src_filepath: str, + progress: Optional[TransferProgress] = None, + ): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output:: + + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + + Args: + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + Returns: + dict[str, Any]: Response data from server. + + """ + response = self.upload_file( + "addons/install", + src_filepath, + progress=progress, + request_type=RequestTypes.post, + ) + return response.json() + + def download_addon_private_file( + self, + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. + + """ + if not destination_filename: + destination_filename = filename + dst_filepath = os.path.join(destination_dir, destination_filename) + # Filename can contain "subfolders" + dst_dirpath = os.path.dirname(dst_filepath) + os.makedirs(dst_dirpath, exist_ok=True) + + endpoint = self.get_addon_endpoint( + addon_name, + addon_version, + "private", + filename + ) + url = f"{self.get_base_url()}/{endpoint}" + self.download_file( + url, dst_filepath, chunk_size=chunk_size, progress=progress + ) + return dst_filepath + + + def get_addon_settings_schema( + self, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None + ) -> dict[str, Any]: + """Sudio/Project settings schema of an addon. + + Project schema may look differently as some enums are based on project + values. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Schema for specific project or + default studio schemas. + + Returns: + dict[str, Any]: Schema of studio/project settings. + + """ + args = tuple() + if project_name: + args = (project_name, ) + + endpoint = self.get_addon_endpoint( + addon_name, addon_version, "schema", *args + ) + result = self.get(endpoint) + result.raise_for_status() + return result.data + + def get_addon_site_settings_schema( + self, addon_name: str, addon_version: str + ) -> dict[str, Any]: + """Site settings schema of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + + Returns: + dict[str, Any]: Schema of site settings. + + """ + result = self.get( + f"addons/{addon_name}/{addon_version}/siteSettings/schema" + ) + result.raise_for_status() + return result.data + + def get_addon_studio_settings( + self, + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + ) -> dict[str, Any]: + """Addon studio settings. + + Receive studio settings for specific version of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + + Returns: + dict[str, Any]: Addon settings. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + + result = self.get( + f"addons/{addon_name}/{addon_version}/settings{query}" + ) + result.raise_for_status() + return result.data + + def get_addon_project_settings( + self, + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True + ) -> dict[str, Any]: + """Addon project settings. + + Receive project settings for specific version of an addon. The settings + may be with site overrides when enabled. + + Site id is filled with current connection site id if not passed. To + make sure any site id is used set 'use_site' to 'False'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Name of project for which the settings are + received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. + + Returns: + dict[str, Any]: Addon settings. + + """ + if not use_site: + site_id = None + elif not site_id: + site_id = self.get_site_id() + + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site": site_id or None, + "variant": variant or None, + }) + result = self.get( + f"addons/{addon_name}/{addon_version}" + f"/settings/{project_name}{query}" + ) + result.raise_for_status() + return result.data + + def get_addon_settings( + self, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True + ) -> dict[str, Any]: + """Receive addon settings. + + Receive addon settings based on project name value. Some arguments may + be ignored if 'project_name' is set to 'None'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Name of project for which the + settings are received. A studio settings values are received + if is 'None'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using + site overrides set to 'False'. In that case won't be applied + any site overrides. + + Returns: + dict[str, Any]: Addon settings. + + """ + if project_name is None: + return self.get_addon_studio_settings( + addon_name, addon_version, variant + ) + return self.get_addon_project_settings( + addon_name, addon_version, project_name, variant, site_id, use_site + ) + + def get_addon_site_settings( + self, + addon_name: str, + addon_version: str, + site_id: Optional[str] = None, + ) -> dict[str, Any]: + """Site settings of an addon. + + If site id is not available an empty dictionary is returned. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + site_id (Optional[str]): Name of site for which should be settings + returned. using 'site_id' attribute if not passed. + + Returns: + dict[str, Any]: Site settings. + + """ + if site_id is None: + site_id = self.get_site_id() + + if not site_id: + return {} + + query = prepare_query_string({"site": site_id}) + result = self.get( + f"addons/{addon_name}/{addon_version}/siteSettings{query}" + ) + result.raise_for_status() + return result.data + + def get_bundle_settings( + self, + bundle_name: Optional[str] = None, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + ) -> dict[str, Any]: + """Get complete set of settings for given data. + + If project is not passed then studio settings are returned. If variant + is not passed 'default_settings_variant' is used. If bundle name is + not passed then current production/staging bundle is used, based on + variant value. + + Output contains addon settings and site settings in single dictionary. + + Todos: + - test how it behaves if there is not any bundle. + - test how it behaves if there is not any production/staging + bundle. + + Example output:: + + { + "addons": [ + { + "name": "addon-name", + "version": "addon-version", + "settings": {...}, + "siteSettings": {...} + } + ] + } + + Returns: + dict[str, Any]: All settings for single bundle. + + """ + if not use_site: + site_id = None + elif not site_id: + site_id = self.get_site_id() + + query = prepare_query_string({ + "project_name": project_name or None, + "bundle_name": bundle_name or None, + "variant": variant or self.get_default_settings_variant() or None, + "site_id": site_id, + }) + response = self.get(f"settings{query}") + response.raise_for_status() + return response.data + + def get_addons_studio_settings( + self, + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """All addons settings in one bulk. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server. + + """ + output = self.get_bundle_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) + if only_values: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + return output + + def get_addons_project_settings( + self, + project_name: str, + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """Project settings of all addons. + + Server returns information about used addon versions, so full output + looks like: + + ```json + { + "settings": {...}, + "addons": {...} + } + ``` + + The output can be limited to only values. To do so is 'only_values' + argument which is by default set to 'True'. In that case output + contains only value of 'settings' key. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + project_name (str): Name of project for which are settings + received. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server for passed + project. + + """ + if not project_name: + raise ValueError("Project name must be passed.") + + output = self.get_bundle_settings( + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) + if only_values: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + return output + + def get_addons_settings( + self, + bundle_name: Optional[str] = None, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """Universal function to receive all addon settings. + + Based on 'project_name' will receive studio settings or project + settings. In case project is not passed is 'site_id' ignored. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + project_name (Optional[str]): Name of project for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. + only_values (Optional[bool]): Only settings values will be + returned. By default, is set to 'True'. + + """ + if project_name is None: + return self.get_addons_studio_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values + ) + + return self.get_addons_project_settings( + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values + ) diff --git a/ayon_api/_api_helpers/dependency_packages.py b/ayon_api/_api_helpers/dependency_packages.py new file mode 100644 index 000000000..dc1f43e94 --- /dev/null +++ b/ayon_api/_api_helpers/dependency_packages.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +import os +import warnings +import platform +import typing +from typing import Optional, Any + +from ayon_api.utils import TransferProgress + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import DependencyPackagesDict + + +class DependencyPackagesAPI(BaseServerAPI): + def get_dependency_packages(self) -> DependencyPackagesDict: + """Information about dependency packages on server. + + To download dependency package, use 'download_dependency_package' + method and pass in 'filename'. + + Example data structure:: + + { + "packages": [ + { + "filename": str, + "platform": str, + "checksum": str, + "checksumAlgorithm": str, + "size": int, + "sources": list[dict[str, Any]], + "supportedAddons": dict[str, str], + "pythonModules": dict[str, str] + } + ] + } + + Returns: + DependencyPackagesDict: Information about dependency packages + known for server. + + """ + endpoint = self._get_dependency_package_route() + result = self.get(endpoint) + result.raise_for_status() + return result.data + + def create_dependency_package( + self, + filename: str, + python_modules: dict[str, str], + source_addons: dict[str, str], + installer_version: str, + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, + platform_name: Optional[str] = None, + ) -> None: + """Create dependency package on server. + + The package will be created on a server, it is also required to upload + the package archive file (using :meth:`upload_dependency_package`). + + Args: + filename (str): Filename of dependency package. + python_modules (dict[str, str]): Python modules in dependency + package:: + + {"": "", ...} + + source_addons (dict[str, str]): Name of addons for which is + dependency package created:: + + {"": "", ...} + + installer_version (str): Version of installer for which was + package created. + checksum (str): Checksum of archive file where dependencies are. + checksum_algorithm (str): Algorithm used to calculate checksum. + file_size (Optional[int]): Size of file. + sources (Optional[list[dict[str, Any]]]): Information about + sources from where it is possible to get file. + platform_name (Optional[str]): Name of platform for which is + dependency package targeted. Default value is + current platform. + + """ + post_body = { + "filename": filename, + "pythonModules": python_modules, + "sourceAddons": source_addons, + "installerVersion": installer_version, + "checksum": checksum, + "checksumAlgorithm": checksum_algorithm, + "size": file_size, + "platform": platform_name or platform.system().lower(), + } + if sources: + post_body["sources"] = sources + + route = self._get_dependency_package_route() + response = self.post(route, **post_body) + response.raise_for_status() + + def update_dependency_package( + self, filename: str, sources: list[dict[str, Any]] + ) -> None: + """Update dependency package metadata on server. + + Args: + filename (str): Filename of dependency package. + sources (list[dict[str, Any]]): Information about + sources from where it is possible to get file. Fully replaces + existing sources. + + """ + response = self.patch( + self._get_dependency_package_route(filename), + sources=sources + ) + response.raise_for_status() + + def delete_dependency_package( + self, filename: str, platform_name: Optional[str] = None + ) -> None: + """Remove dependency package for specific platform. + + Args: + filename (str): Filename of dependency package. + platform_name (Optional[str]): Deprecated. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'delete_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + + route = self._get_dependency_package_route(filename) + response = self.delete(route) + response.raise_for_status("Failed to delete dependency file") + + def download_dependency_package( + self, + src_filename: str, + dst_directory: str, + dst_filename: str, + platform_name: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> str: + """Download dependency package from server. + + This method requires to have authorized token available. The package + is only downloaded. + + Args: + src_filename (str): Filename of dependency pacakge. + For server version 0.2.0 and lower it is name of package + to download. + dst_directory (str): Where the file should be downloaded. + dst_filename (str): Name of destination filename. + platform_name (Optional[str]): Deprecated. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'download_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + route = self._get_dependency_package_route(src_filename) + package_filepath = os.path.join(dst_directory, dst_filename) + self.download_file( + route, + package_filepath, + chunk_size=chunk_size, + progress=progress + ) + return package_filepath + + def upload_dependency_package( + self, + src_filepath: str, + dst_filename: str, + platform_name: Optional[str] = None, + progress: Optional[TransferProgress] = None, + ) -> None: + """Upload dependency package to server. + + Args: + src_filepath (str): Path to a package file. + dst_filename (str): Dependency package filename or name of package + for server version 0.2.0 or lower. Must be unique. + platform_name (Optional[str]): Deprecated. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'upload_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + + route = self._get_dependency_package_route(dst_filename) + self.upload_file(route, src_filepath, progress=progress) + + def _get_dependency_package_route( + self, filename: Optional[str] = None + ) -> str: + endpoint = "desktop/dependencyPackages" + if filename: + return f"{endpoint}/{filename}" + return endpoint diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py new file mode 100644 index 000000000..19e12c6b8 --- /dev/null +++ b/ayon_api/_api_helpers/events.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Any, Iterable, Generator + +from ayon_api.utils import SortOrder, prepare_list_filters, RestApiResponse +from ayon_api.graphql_queries import events_graphql_query + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from typing import Union + + from ayon_api.typing import EventFilter, EventStatus, EnrollEventData + + +class EventsAPI(BaseServerAPI): + def get_event(self, event_id: str) -> Optional[dict[str, Any]]: + """Query full event data by id. + + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. + + Args: + event_id (str): Event id. + + Returns: + dict[str, Any]: Full event data. + + """ + response = self.get(f"events/{event_id}") + response.raise_for_status() + return response.data + + def get_events( + self, + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[EventStatus]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, + ) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. + + Notes: + Not all event happen on a project. + + Args: + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. + + Returns: + Generator[dict[str, Any]]: Available events matching filters. + + """ + if statuses is None and states is not None: + warnings.warn( + ( + "Used deprecated argument 'states' in 'get_events'." + " Use 'statuses' instead." + ), + DeprecationWarning + ) + statuses = states + + filters = {} + if not prepare_list_filters( + filters, + ("eventTopics", topics), + ("eventIds", event_ids), + ("projectNames", project_names), + ("eventStatuses", statuses), + ("eventUsers", users), + ): + return + + if include_logs is None: + include_logs = False + + for filter_key, filter_value in ( + ("includeLogsFilter", include_logs), + ("hasChildrenFilter", has_children), + ("newerThanFilter", newer_than), + ("olderThanFilter", older_than), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if not fields: + fields = self.get_default_fields_for_type("event") + + major, minor, patch, _, _ = self.get_server_version_tuple() + use_states = (major, minor, patch) <= (1, 5, 6) + + query = events_graphql_query(set(fields), order, use_states) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + if limit: + events_field = query.get_field_by_path("events") + events_field.set_limit(limit) + + for parsed_data in query.continuous_query(self): + for event in parsed_data["events"]: + yield event + + def update_event( + self, + event_id: str, + sender: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + status: Optional[EventStatus] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, + ) -> None: + """Update event data. + + Args: + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[EventStatus]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. + + """ + kwargs = { + key: value + for key, value in ( + ("sender", sender), + ("project", project_name), + ("user", username), + ("status", status), + ("description", description), + ("summary", summary), + ("payload", payload), + ("progress", progress), + ("retries", retries), + ) + if value is not None + } + + response = self.patch( + f"events/{event_id}", + **kwargs + ) + response.raise_for_status() + + def dispatch_event( + self, + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, + ) -> RestApiResponse: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + RestApiResponse: Response from server. + + """ + if summary is None: + summary = {} + if payload is None: + payload = {} + event_data = { + "topic": topic, + "sender": sender, + "hash": event_hash, + "project": project_name, + "user": username, + "description": description, + "summary": summary, + "payload": payload, + "finished": finished, + "store": store, + } + if depends_on: + event_data["dependsOn"] = depends_on + + if dependencies: + warnings.warn( + ( + "Used deprecated argument 'dependencies' in" + " 'dispatch_event'. Use 'depends_on' instead." + ), + DeprecationWarning + ) + + response = self.post("events", **event_data) + response.raise_for_status() + return response + + def create_event( + self, + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, + ) -> str: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + str: Event id. + + """ + result = self.dispatch_event( + topic, + sender, + event_hash, + project_name, + username, + depends_on, + description, + summary, + payload, + finished, + store, + dependencies, + ) + return result.data["id"] + + def delete_event(self, event_id: str) -> None: + """Delete event by id. + + Supported since AYON server 1.6.0. + + Args: + event_id (str): Event id. + + Returns: + RestApiResponse: Response from server. + + """ + response = self.delete(f"events/{event_id}") + response.raise_for_status() + + def enroll_event_job( + self, + source_topic: Union[str, list[str]], + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional[EventFilter] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, + ) -> Optional[EnrollEventData]: + """Enroll job based on events. + + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. + + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. + + Make sure the new event has updated status to '"finished"' status + when you're done with logic + + Target topic should not clash with other processes/services. + + Created target event have 'dependsOn' key where is id of source topic. + + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' + + Args: + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. + + Returns: + Optional[EnrollEventData]: None if there is no event matching + filters. Created event with 'target_topic'. + + """ + kwargs: dict[str, Any] = { + "sourceTopic": source_topic, + "targetTopic": target_topic, + "sender": sender, + } + major, minor, patch, _, _ = self.get_server_version_tuple() + if max_retries is not None: + kwargs["maxRetries"] = max_retries + if sequential is not None: + kwargs["sequential"] = sequential + if description is not None: + kwargs["description"] = description + if events_filter is not None: + kwargs["filter"] = events_filter + if ( + ignore_older_than is not None + and (major, minor, patch) > (1, 5, 1) + ): + kwargs["ignoreOlderThan"] = ignore_older_than + if ignore_sender_types is not None: + if (major, minor, patch) <= (1, 5, 4): + raise ValueError( + "Ignore sender types are not supported for" + f" your version of server {self.get_server_version()}." + ) + kwargs["ignoreSenderTypes"] = list(ignore_sender_types) + + response = self.post("enroll", **kwargs) + if response.status_code == 204: + return None + + if response.status_code == 503: + # Server is busy + self.log.info("Server is busy. Can't enroll event now.") + return None + + if response.status_code >= 400: + self.log.error(response.text) + return None + + return response.data diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py new file mode 100644 index 000000000..fbef4e485 --- /dev/null +++ b/ayon_api/_api_helpers/folders.py @@ -0,0 +1,638 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.exceptions import UnsupportedServerVersion +from ayon_api.utils import ( + prepare_query_string, + prepare_list_filters, + fill_own_attribs, + create_entity_id, + NOT_SET, +) +from ayon_api.graphql_queries import folders_graphql_query + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + FolderDict, + FlatFolderDict, + ProjectHierarchyDict, + ) + + +class FoldersAPI(BaseServerAPI): + def get_rest_folder( + self, project_name: str, folder_id: str + ) -> Optional[FolderDict]: + return self.get_rest_entity_by_id( + project_name, "folder", folder_id + ) + + def get_rest_folders( + self, project_name: str, include_attrib: bool = False + ) -> list[FlatFolderDict]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Args: + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. + + Returns: + list[FlatFolderDict]: List of folder entities. + + """ + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + raise UnsupportedServerVersion( + "Function 'get_folders_rest' is supported" + " for AYON server 1.0.8 and above." + ) + query = prepare_query_string({ + "attrib": "true" if include_attrib else "false" + }) + response = self.get( + f"projects/{project_name}/folders{query}" + ) + response.raise_for_status() + return response.data["folders"] + + def get_folders_hierarchy( + self, + project_name: str, + search_string: Optional[str] = None, + folder_types: Optional[Iterable[str]] = None + ) -> ProjectHierarchyDict: + """Get project hierarchy. + + All folders in project in hierarchy data structure. + + Example output: + { + "hierarchy": [ + { + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] + }, + ... + ] + } + + Args: + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. + + Returns: + dict[str, Any]: Response data from server. + + """ + if folder_types: + folder_types = ",".join(folder_types) + + query = prepare_query_string({ + "search": search_string or None, + "types": folder_types or None, + }) + response = self.get( + f"projects/{project_name}/hierarchy{query}" + ) + response.raise_for_status() + return response.data + + def get_folders_rest( + self, project_name: str, include_attrib: bool = False + ) -> list[FlatFolderDict]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Deprecated: + Use 'get_rest_folders' instead. Function was renamed to match + other rest functions, like 'get_rest_folder', + 'get_rest_project' etc. . + Will be removed in '1.0.7' or '1.1.0'. + + Args: + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. + + Returns: + list[FlatFolderDict]: List of folder entities. + + """ + warnings.warn( + ( + "DEPRECATION: Used deprecated 'get_folders_rest'," + " use 'get_rest_folders' instead." + ), + DeprecationWarning + ) + return self.get_rest_folders(project_name, include_attrib) + + def get_folders( + self, + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + folder_paths: Optional[Iterable[str]] = None, + folder_names: Optional[Iterable[str]] = None, + folder_types: Optional[Iterable[str]] = None, + parent_ids: Optional[Iterable[str]] = None, + folder_path_regex: Optional[str] = None, + has_products: Optional[bool] = None, + has_tasks: Optional[bool] = None, + has_children: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Generator[FolderDict, None, None]: + """Query folders from server. + + Todos: + Folder name won't be unique identifier, so we should add + folder path filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. + + Args: + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + folder_path_regex (Optional[str]): Folder path regex used + for filtering. + has_products (Optional[bool]): Filter folders with/without + products. Ignored when None, default behavior. + has_tasks (Optional[bool]): Filter folders with/without + tasks. Ignored when None, default behavior. + has_children (Optional[bool]): Filter folders with/without + children. Ignored when None, default behavior. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. + assignees_all (Optional[Iterable[str]]): Filter by assigness + on children tasks. Task must have all of passed assignees. + tags (Optional[Iterable[str]]): Folder tags used + for filtering. + active (Optional[bool]): Filter active/inactive folders. + Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[FolderDict, None, None]: Queried folder entities. + + """ + if not project_name: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("folderIds", folder_ids), + ("folderPaths", folder_paths), + ("folderNames", folder_names), + ("folderTypes", folder_types), + ("folderStatuses", statuses), + ("folderTags", tags), + ("folderAssigneesAll", assignees_all), + ): + return + + for filter_key, filter_value in ( + ("folderPathRegex", folder_path_regex), + ("folderHasProducts", has_products), + ("folderHasTasks", has_tasks), + ("folderHasLinks", has_links), + ("folderHasChildren", has_children), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if parent_ids is not None: + parent_ids = set(parent_ids) + if not parent_ids: + return + if None in parent_ids: + # Replace 'None' with '"root"' which is used during GraphQl + # query for parent ids filter for folders without folder + # parent + parent_ids.remove(None) + parent_ids.add("root") + + if project_name in parent_ids: + # Replace project name with '"root"' which is used during + # GraphQl query for parent ids filter for folders without + # folder parent + parent_ids.remove(project_name) + parent_ids.add("root") + + filters["parentFolderIds"] = list(parent_ids) + + if not fields: + fields = self.get_default_fields_for_type("folder") + else: + fields = set(fields) + self._prepare_fields("folder", fields) + + if active is not None: + fields.add("active") + + if own_attributes: + fields.add("ownAttrib") + + query = folders_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for folder in parsed_data["project"]["folders"]: + if active is not None and active is not folder["active"]: + continue + + self._convert_entity_data(folder) + + if own_attributes: + fill_own_attribs(folder) + yield folder + + def get_folder_by_id( + self, + project_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[FolderDict]: + """Query folder entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_path( + self, + project_name: str, + folder_path: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[FolderDict]: + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_paths=[folder_path], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_name( + self, + project_name: str, + folder_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[FolderDict]: + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_names=[folder_name], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_ids_with_products( + self, project_name: str, folder_ids: Optional[Iterable[str]] = None + ) -> set[str]: + """Find folders which have at least one product. + + Folders that have at least one product should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. + + Args: + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. + + Returns: + set[str]: Folder ids that have at least one product. + + """ + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return set() + + query = folders_graphql_query({"id"}) + query.set_variable_value("projectName", project_name) + query.set_variable_value("folderHasProducts", True) + if folder_ids: + query.set_variable_value("folderIds", list(folder_ids)) + + parsed_data = query.query(self) + folders = parsed_data["project"]["folders"] + return { + folder["id"] + for folder in folders + } + + def create_folder( + self, + project_name: str, + name: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + folder_id: Optional[str] = None, + ) -> str: + """Create new folder. + + Args: + project_name (str): Project name. + name (str): Folder name. + folder_type (Optional[str]): Folder type. + parent_id (Optional[str]): Parent folder id. Parent is project + if is ``None``. + label (Optional[str]): Label of folder. + attrib (Optional[dict[str, Any]]): Folder attributes. + data (Optional[dict[str, Any]]): Folder data. + tags (Optional[Iterable[str]]): Folder tags. + status (Optional[str]): Folder status. + active (Optional[bool]): Folder active state. + thumbnail_id (Optional[str]): Folder thumbnail id. + folder_id (Optional[str]): Folder id. If not passed new id is + generated. + + Returns: + str: Entity id. + + """ + if not folder_id: + folder_id = create_entity_id() + create_data = { + "id": folder_id, + "name": name, + } + for key, value in ( + ("folderType", folder_type), + ("parentId", parent_id), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/folders", + **create_data + ) + response.raise_for_status() + return folder_id + + def update_folder( + self, + project_name: str, + folder_id: str, + name: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, + label: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ) -> None: + """Update folder entity on server. + + Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + name (Optional[str]): New name. + folder_type (Optional[str]): New folder type. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("folderType", folder_type), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("label", label), + ("parentId", parent_id), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/folders/{folder_id}", + **update_data + ) + response.raise_for_status() + + def delete_folder( + self, project_name: str, folder_id: str, force: bool = False + ) -> None: + """Delete folder. + + Args: + project_name (str): Project name. + folder_id (str): Folder id to delete. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. + + """ + url = f"projects/{project_name}/folders/{folder_id}" + if force: + url += "?force=true" + response = self.delete(url) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/installers.py b/ayon_api/_api_helpers/installers.py new file mode 100644 index 000000000..be2bcaaec --- /dev/null +++ b/ayon_api/_api_helpers/installers.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import typing +from typing import Optional, Any + +import requests + +from ayon_api.utils import prepare_query_string, TransferProgress + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import InstallersInfoDict + + +class InstallersAPI(BaseServerAPI): + def get_installers( + self, + version: Optional[str] = None, + platform_name: Optional[str] = None, + ) -> InstallersInfoDict: + """Information about desktop application installers on server. + + Desktop application installers are helpers to download/update AYON + desktop application for artists. + + Args: + version (Optional[str]): Filter installers by version. + platform_name (Optional[str]): Filter installers by platform name. + + Returns: + InstallersInfoDict: Information about installers known for server. + + """ + query = prepare_query_string({ + "version": version or None, + "platform": platform_name or None, + }) + response = self.get(f"desktop/installers{query}") + response.raise_for_status() + return response.data + + def create_installer( + self, + filename: str, + version: str, + python_version: str, + platform_name: str, + python_modules: dict[str, str], + runtime_python_modules: dict[str, str], + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, + ) -> None: + """Create new installer information on server. + + This step will create only metadata. Make sure to upload installer + to the server using 'upload_installer' method. + + Runtime python modules are modules that are required to run AYON + desktop application, but are not added to PYTHONPATH for any + subprocess. + + Args: + filename (str): Installer filename. + version (str): Version of installer. + python_version (str): Version of Python. + platform_name (str): Name of platform. + python_modules (dict[str, str]): Python modules that are available + in installer. + runtime_python_modules (dict[str, str]): Runtime python modules + that are available in installer. + checksum (str): Installer file checksum. + checksum_algorithm (str): Type of checksum used to create checksum. + file_size (int): File size. + sources (Optional[list[dict[str, Any]]]): List of sources that + can be used to download file. + + """ + body = { + "filename": filename, + "version": version, + "pythonVersion": python_version, + "platform": platform_name, + "pythonModules": python_modules, + "runtimePythonModules": runtime_python_modules, + "checksum": checksum, + "checksumAlgorithm": checksum_algorithm, + "size": file_size, + } + if sources: + body["sources"] = sources + + response = self.post("desktop/installers", **body) + response.raise_for_status() + + def update_installer( + self, filename: str, sources: list[dict[str, Any]] + ) -> None: + """Update installer information on server. + + Args: + filename (str): Installer filename. + sources (list[dict[str, Any]]): List of sources that + can be used to download file. Fully replaces existing sources. + + """ + response = self.patch( + f"desktop/installers/{filename}", + sources=sources + ) + response.raise_for_status() + + def delete_installer(self, filename: str) -> None: + """Delete installer from server. + + Args: + filename (str): Installer filename. + + """ + response = self.delete(f"desktop/installers/{filename}") + response.raise_for_status() + + def download_installer( + self, + filename: str, + dst_filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None + ) -> TransferProgress: + """Download installer file from server. + + Args: + filename (str): Installer filename. + dst_filepath (str): Destination filepath. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + TransferProgress: Progress object. + + """ + return self.download_file( + f"desktop/installers/{filename}", + dst_filepath, + chunk_size=chunk_size, + progress=progress + ) + + def upload_installer( + self, + src_filepath: str, + dst_filename: str, + progress: Optional[TransferProgress] = None, + ) -> requests.Response: + """Upload installer file to server. + + Args: + src_filepath (str): Source filepath. + dst_filename (str): Destination filename. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Response object. + + """ + return self.upload_file( + f"desktop/installers/{dst_filename}", + src_filepath, + progress=progress + ) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py new file mode 100644 index 000000000..6c23b504c --- /dev/null +++ b/ayon_api/_api_helpers/links.py @@ -0,0 +1,661 @@ +from __future__ import annotations + +import collections +import typing +from typing import Optional, Any, Iterable + +from ayon_api.graphql_queries import ( + folders_graphql_query, + tasks_graphql_query, + products_graphql_query, + versions_graphql_query, + representations_graphql_query, +) + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from typing import TypedDict + from ayon_api.typing import LinkDirection + + class CreateLinkData(TypedDict): + id: str + + +class LinksAPI(BaseServerAPI): + def get_full_link_type_name( + self, link_type_name: str, input_type: str, output_type: str + ) -> str: + """Calculate full link type name used for query from server. + + Args: + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + str: Full name of link type used for query from server. + + """ + return "|".join([link_type_name, input_type, output_type]) + + def get_link_types(self, project_name: str) -> list[dict[str, Any]]: + """All link types available on a project. + + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] + + Args: + project_name (str): Name of project where to look for link types. + + Returns: + list[dict[str, Any]]: Link types available on project. + + """ + response = self.get(f"projects/{project_name}/links/types") + response.raise_for_status() + return response.data["types"] + + def get_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + ) -> Optional[dict[str, Any]]: + """Get link type data. + + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. + + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + + Args: + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + Optional[dict[str, Any]]: Link type information. + + """ + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + for link_type in self.get_link_types(project_name): + if link_type["name"] == full_type_name: + return link_type + return None + + def create_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, + ) -> None: + """Create or update link type on server. + + Warning: + Because PUT is used for creation it is also used for update. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. + + Raises: + HTTPRequestError: Server error happened. + + """ + if data is None: + data = {} + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.put( + f"projects/{project_name}/links/types/{full_type_name}", + **data + ) + response.raise_for_status() + + def delete_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + ) -> None: + """Remove link type from project. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Raises: + HTTPRequestError: Server error happened. + + """ + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.delete( + f"projects/{project_name}/links/types/{full_type_name}" + ) + response.raise_for_status() + + def make_sure_link_type_exists( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, + ) -> None: + """Make sure link type exists on a project. + + Args: + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. + + """ + link_type = self.get_link_type( + project_name, link_type_name, input_type, output_type) + if ( + link_type + and (data is None or data == link_type["data"]) + ): + return + self.create_link_type( + project_name, link_type_name, input_type, output_type, data + ) + + def create_link( + self, + project_name: str, + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, + ) -> CreateLinkData: + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } + + Args: + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Input entity id. + input_type (str): Entity type of input entity. + output_id (str): Output entity id. + output_type (str): Entity type of output entity. + link_name (Optional[str]): Name of link. + Available from server version '1.0.0-rc.6'. + + Returns: + CreateLinkData: Information about link. + + Raises: + HTTPRequestError: Server error happened. + + """ + full_link_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type) + + kwargs = { + "input": input_id, + "output": output_id, + "linkType": full_link_type_name, + } + if link_name: + kwargs["name"] = link_name + + response = self.post( + f"projects/{project_name}/links", **kwargs + ) + response.raise_for_status() + return response.data + + def delete_link(self, project_name: str, link_id: str) -> None: + """Remove link by id. + + Args: + project_name (str): Project where link exists. + link_id (str): Id of link. + + Raises: + HTTPRequestError: Server error happened. + + """ + response = self.delete( + f"projects/{project_name}/links/{link_id}" + ) + response.raise_for_status() + + def get_entities_links( + self, + project_name: str, + entity_type: str, + entity_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + link_names: Optional[Iterable[str]] = None, + link_name_regex: Optional[str] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Helper method to get links from server for entity types. + + .. highlight:: text + .. code-block:: text + + Example output: + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], + ... + } + + Args: + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "product", + "version", "representations"]): Entity type. + entity_ids (Optional[Iterable[str]]): Ids of entities for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by entity ids. + + """ + if entity_type == "folder": + query_func = folders_graphql_query + id_filter_key = "folderIds" + project_sub_key = "folders" + elif entity_type == "task": + query_func = tasks_graphql_query + id_filter_key = "taskIds" + project_sub_key = "tasks" + elif entity_type == "product": + query_func = products_graphql_query + id_filter_key = "productIds" + project_sub_key = "products" + elif entity_type == "version": + query_func = versions_graphql_query + id_filter_key = "versionIds" + project_sub_key = "versions" + elif entity_type == "representation": + query_func = representations_graphql_query + id_filter_key = "representationIds" + project_sub_key = "representations" + else: + raise ValueError("Unknown type \"{}\". Expected {}".format( + entity_type, + ", ".join( + ("folder", "task", "product", "version", "representation") + ) + )) + + output = collections.defaultdict(list) + filters = { + "projectName": project_name + } + if entity_ids is not None: + entity_ids = set(entity_ids) + if not entity_ids: + return output + filters[id_filter_key] = list(entity_ids) + + if not self._prepare_link_filters( + filters, link_types, link_direction, link_names, link_name_regex + ): + return output + + link_fields = {"id", "links"} + query = query_func(link_fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for entity in parsed_data["project"][project_sub_key]: + entity_id = entity["id"] + output[entity_id].extend(entity["links"]) + return output + + def get_folders_links( + self, + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. + + Args: + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by folder ids. + + """ + return self.get_entities_links( + project_name, "folder", folder_ids, link_types, link_direction + ) + + def get_folder_links( + self, + project_name: str, + folder_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> list[dict[str, Any]]: + """Query folder links from server. + + Args: + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of folder. + + """ + return self.get_folders_links( + project_name, [folder_id], link_types, link_direction + )[folder_id] + + def get_tasks_links( + self, + project_name: str, + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. + + Args: + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by task ids. + + """ + return self.get_entities_links( + project_name, "task", task_ids, link_types, link_direction + ) + + def get_task_links( + self, + project_name: str, + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> list[dict[str, Any]]: + """Query task links from server. + + Args: + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of task. + + """ + return self.get_tasks_links( + project_name, [task_id], link_types, link_direction + )[task_id] + + def get_products_links( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. + + Args: + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by product ids. + + """ + return self.get_entities_links( + project_name, "product", product_ids, link_types, link_direction + ) + + def get_product_links( + self, + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> list[dict[str, Any]]: + """Query product links from server. + + Args: + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. + + """ + return self.get_products_links( + project_name, [product_id], link_types, link_direction + )[product_id] + + def get_versions_links( + self, + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. + + Args: + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by version ids. + + """ + return self.get_entities_links( + project_name, "version", version_ids, link_types, link_direction + ) + + def get_version_links( + self, + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> list[dict[str, Any]]: + """Query version links from server. + + Args: + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of version. + + """ + return self.get_versions_links( + project_name, [version_id], link_types, link_direction + )[version_id] + + def get_representations_links( + self, + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. + + Args: + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by representation ids. + + """ + return self.get_entities_links( + project_name, + "representation", + representation_ids, + link_types, + link_direction + ) + + def get_representation_links( + self, + project_name: str, + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None + ) -> list[dict[str, Any]]: + """Query representation links from server. + + Args: + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of representation. + + """ + return self.get_representations_links( + project_name, [representation_id], link_types, link_direction + )[representation_id] + + def _prepare_link_filters( + self, + filters: dict[str, Any], + link_types: Optional[Iterable[str], None], + link_direction: Optional[LinkDirection], + link_names: Optional[Iterable[str]], + link_name_regex: Optional[str], + ) -> bool: + """Add links filters for GraphQl queries. + + Args: + filters (dict[str, Any]): Object where filters will be added. + link_types (Optional[Iterable[str]]): Link types filters. + link_direction (Optional[Literal["in", "out"]]): Direction of + link "in", "out" or 'None' for both. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. + + Returns: + bool: Links are valid, and query from server can happen. + + """ + if link_types is not None: + link_types = set(link_types) + if not link_types: + return False + filters["linkTypes"] = list(link_types) + + if link_names is not None: + link_names = set(link_names) + if not link_names: + return False + filters["linkNames"] = list(link_names) + + if link_direction is not None: + if link_direction not in ("in", "out"): + return False + filters["linkDirection"] = link_direction + + if link_name_regex is not None: + filters["linkNameRegex"] = link_name_regex + return True diff --git a/ayon_api/_lists.py b/ayon_api/_api_helpers/lists.py similarity index 86% rename from ayon_api/_lists.py rename to ayon_api/_api_helpers/lists.py index 480c1e613..b6bd79265 100644 --- a/ayon_api/_lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -1,20 +1,23 @@ +from __future__ import annotations + import json import typing -from typing import Optional, Iterable, Any, Dict, List, Generator +from typing import Optional, Iterable, Any, Generator + +from ayon_api.utils import create_entity_id +from ayon_api.graphql_queries import entity_lists_graphql_query -from ._base import _BaseServerAPI -from .utils import create_entity_id -from .graphql_queries import entity_lists_graphql_query +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( EntityListEntityType, EntityListAttributeDefinitionDict, EntityListItemMode, ) -class _ListsAPI(_BaseServerAPI): +class ListsAPI(BaseServerAPI): def get_entity_lists( self, project_name: str, @@ -22,7 +25,7 @@ def get_entity_lists( list_ids: Optional[Iterable[str]] = None, active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: + ) -> Generator[dict[str, Any], None, None]: """Fetch entity lists from server. Args: @@ -33,7 +36,7 @@ def get_entity_lists( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities + Generator[dict[str, Any], None, None]: Entity list entities matching defined filters. """ @@ -44,7 +47,7 @@ def get_entity_lists( if active is not None: fields.add("active") - filters: Dict[str, Any] = {"projectName": project_name} + filters: dict[str, Any] = {"projectName": project_name} if list_ids is not None: if not list_ids: return @@ -69,7 +72,7 @@ def get_entity_lists( def get_entity_list_rest( self, project_name: str, list_id: str - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get entity list by id using REST API. Args: @@ -77,7 +80,7 @@ def get_entity_list_rest( list_id (str): Entity list id. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ response = self.get(f"projects/{project_name}/lists/{list_id}") @@ -89,7 +92,7 @@ def get_entity_list_by_id( project_name: str, list_id: str, fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get entity list by id using GraphQl. Args: @@ -98,7 +101,7 @@ def get_entity_list_by_id( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ for entity_list in self.get_entity_lists( @@ -110,18 +113,18 @@ def get_entity_list_by_id( def create_entity_list( self, project_name: str, - entity_type: "EntityListEntityType", + entity_type: EntityListEntityType, label: str, *, list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, + template: Optional[dict[str, Any]] = None, owner: Optional[str] = None, active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, + items: Optional[list[dict[str, Any]]] = None, list_id: Optional[str] = None, ) -> str: """Create entity list. @@ -180,10 +183,10 @@ def update_entity_list( list_id: str, *, label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -234,7 +237,7 @@ def delete_entity_list(self, project_name: str, list_id: str) -> None: def get_entity_list_attribute_definitions( self, project_name: str, list_id: str - ) -> List["EntityListAttributeDefinitionDict"]: + ) -> list[EntityListAttributeDefinitionDict]: """Get attribute definitioins on entity list. Args: @@ -242,7 +245,7 @@ def get_entity_list_attribute_definitions( list_id (str): Entity list id. Returns: - List[EntityListAttributeDefinitionDict]: List of attribute + list[EntityListAttributeDefinitionDict]: List of attribute definitions. """ @@ -256,14 +259,14 @@ def set_entity_list_attribute_definitions( self, project_name: str, list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], + attribute_definitions: list[EntityListAttributeDefinitionDict], ) -> None: """Set attribute definitioins on entity list. Args: project_name (str): Project name. list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): + attribute_definitions (list[EntityListAttributeDefinitionDict]): List of attribute definitions. """ @@ -280,9 +283,9 @@ def create_entity_list_item( *, position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, item_id: Optional[str] = None, ) -> str: """Create entity list item. @@ -328,15 +331,15 @@ def update_entity_list_items( self, project_name: str, list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", + items: list[dict[str, Any]], + mode: EntityListItemMode, ) -> None: """Update items in entity list. Args: project_name (str): Project name where entity list live. list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. + items (list[dict[str, Any]]): Entity list items. mode (EntityListItemMode): Mode of items update. """ @@ -356,9 +359,9 @@ def update_entity_list_item( new_list_id: Optional[str], position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, ) -> None: """Update item in entity list. diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py new file mode 100644 index 000000000..13fdb9b80 --- /dev/null +++ b/ayon_api/_api_helpers/products.py @@ -0,0 +1,504 @@ +from __future__ import annotations + +import collections +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.utils import ( + prepare_list_filters, + create_entity_id, +) +from ayon_api.graphql_queries import ( + products_graphql_query, + product_types_query, +) + +from .base import BaseServerAPI, _PLACEHOLDER + +if typing.TYPE_CHECKING: + from ayon_api.typing import ProductDict, ProductTypeDict + + +class ProductsAPI(BaseServerAPI): + def get_rest_product( + self, project_name: str, product_id: str + ) -> Optional[ProductDict]: + return self.get_rest_entity_by_id( + project_name, "product", product_id + ) + + def get_products( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]]=None, + folder_ids: Optional[Iterable[str]]=None, + product_types: Optional[Iterable[str]]=None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Generator[ProductDict, None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. + + Args: + project_name (str): Name of project. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Generator[ProductDict, None, None]: Queried product entities. + + """ + if not project_name: + return + + # Prepare these filters before 'name_by_filter_ids' filter + filter_product_names = None + if product_names is not None: + filter_product_names = set(product_names) + if not filter_product_names: + return + + filter_folder_ids = None + if folder_ids is not None: + filter_folder_ids = set(folder_ids) + if not filter_folder_ids: + return + + # This will disable 'folder_ids' and 'product_names' filters + # - maybe could be enhanced in future? + if names_by_folder_ids is not None: + filter_product_names = set() + filter_folder_ids = set() + + for folder_id, names in names_by_folder_ids.items(): + if folder_id and names: + filter_folder_ids.add(folder_id) + filter_product_names |= set(names) + + if not filter_product_names or not filter_folder_ids: + return + + # Convert fields and add minimum required fields + if fields: + fields = set(fields) | {"id"} + self._prepare_fields("product", fields) + else: + fields = self.get_default_fields_for_type("product") + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for products. The" + " argument will be removed from function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered + if names_by_folder_ids: + fields.add("name") + fields.add("folderId") + + # Prepare filters for query + filters = { + "projectName": project_name + } + + if filter_folder_ids: + filters["folderIds"] = list(filter_folder_ids) + + if filter_product_names: + filters["productNames"] = list(filter_product_names) + + if not prepare_list_filters( + filters, + ("productIds", product_ids), + ("productTypes", product_types), + ("productStatuses", statuses), + ("productTags", tags), + ): + return + + for filter_key, filter_value in ( + ("productNameRegex", product_name_regex), + ("productPathRegex", product_path_regex), + ): + if filter_value: + filters[filter_key] = filter_value + + query = products_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + parsed_data = query.query(self) + + products = parsed_data.get("project", {}).get("products", []) + # Filter products by 'names_by_folder_ids' + if names_by_folder_ids: + products_by_folder_id = collections.defaultdict(list) + for product in products: + filtered_product = self._filter_product( + project_name, product, active + ) + if filtered_product is not None: + folder_id = filtered_product["folderId"] + products_by_folder_id[folder_id].append(filtered_product) + + for folder_id, names in names_by_folder_ids.items(): + for folder_product in products_by_folder_id[folder_id]: + if folder_product["name"] in names: + yield folder_product + + else: + for product in products: + filtered_product = self._filter_product( + project_name, product, active + ) + if filtered_product is not None: + yield filtered_product + + def get_product_by_id( + self, + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[ProductDict]: + """Query product entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_id (str): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Optional[ProductDict]: Product entity data or None + if was not found. + + """ + products = self.get_products( + project_name, + product_ids=[product_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for product in products: + return product + return None + + def get_product_by_name( + self, + project_name: str, + product_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[ProductDict]: + """Query product entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Optional[ProductDict]: Product entity data or None + if was not found. + + """ + products = self.get_products( + project_name, + product_names=[product_name], + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for product in products: + return product + return None + + def get_product_types( + self, fields: Optional[Iterable[str]] = None + ) -> list[ProductTypeDict]: + """Types of products. + + This is server wide information. Product types have 'name', 'icon' and + 'color'. + + Args: + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + list[ProductTypeDict]: Product types information. + + """ + if not fields: + fields = self.get_default_fields_for_type("productType") + + query = product_types_query(fields) + + parsed_data = query.query(self) + + return parsed_data.get("productTypes", []) + + def get_project_product_types( + self, project_name: str, fields: Optional[Iterable[str]] = None + ) -> list[ProductTypeDict]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. + + Args: + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + list[ProductTypeDict]: Product types information. + + """ + warnings.warn( + "Used deprecated function 'get_project_product_types'." + " Use 'get_project' with 'productTypes' in 'fields' instead.", + DeprecationWarning, + stacklevel=2, + ) + if fields is None: + fields = {"productTypes"} + else: + fields = { + f"productTypes.{key}" + for key in fields + } + + project = self.get_project(project_name, fields=fields) + return project["productTypes"] + + def get_product_type_names( + self, + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, + ) -> set[str]: + """DEPRECATED Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. + + Returns: + set[str]: Product type names. + + """ + warnings.warn( + "Used deprecated function 'get_product_type_names'." + " Use 'get_product_types' or 'get_products' instead.", + DeprecationWarning, + stacklevel=2, + ) + if project_name: + if not product_ids: + return set() + products = self.get_products( + project_name, + product_ids=product_ids, + fields=["productType"], + active=None, + ) + return { + product["productType"] + for product in products + } + + return { + product_info["name"] + for product_info in self.get_product_types(project_name) + } + + def create_product( + self, + project_name: str, + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] =None, + status: Optional[str] = None, + active: Optional[bool] = None, + product_id: Optional[str] = None, + ) -> str: + """Create new product. + + Args: + project_name (str): Project name. + name (str): Product name. + product_type (str): Product type. + folder_id (str): Parent folder id. + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is + generated. + + Returns: + str: Product id. + + """ + if not product_id: + product_id = create_entity_id() + create_data = { + "id": product_id, + "name": name, + "productType": product_type, + "folderId": folder_id, + } + for key, value in ( + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/products", + **create_data + ) + response.raise_for_status() + return product_id + + def update_product( + self, + project_name: str, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ) -> None: + """Update product entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("productType", product_type), + ("folderId", folder_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/products/{product_id}", + **update_data + ) + response.raise_for_status() + + def delete_product(self, project_name: str, product_id: str) -> None: + """Delete product. + + Args: + project_name (str): Project name. + product_id (str): Product id to delete. + + """ + response = self.delete( + f"projects/{project_name}/products/{product_id}" + ) + response.raise_for_status() + + def _filter_product( + self, + project_name: str, + product: ProductDict, + active: Optional[bool], + ) -> Optional[ProductDict]: + if active is not None and product["active"] is not active: + return None + + self._convert_entity_data(product) + + return product diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py new file mode 100644 index 000000000..09fe66d98 --- /dev/null +++ b/ayon_api/_api_helpers/projects.py @@ -0,0 +1,737 @@ +from __future__ import annotations + +import json +import platform +import warnings +import typing +from typing import Optional, Generator, Iterable, Any + +from ayon_api.constants import PROJECT_NAME_REGEX +from ayon_api.utils import prepare_query_string, fill_own_attribs +from ayon_api.graphql_queries import projects_graphql_query + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ProjectDict, AnatomyPresetDict + + +class ProjectsAPI(BaseServerAPI): + def get_project_anatomy_presets(self) -> list[AnatomyPresetDict]: + """Anatomy presets available on server. + + Content has basic information about presets. Example output:: + + [ + { + "name": "netflix_VFX", + "primary": false, + "version": "1.0.0" + }, + { + ... + }, + ... + ] + + Returns: + list[dict[str, str]]: Anatomy presets available on server. + + """ + result = self.get("anatomy/presets") + result.raise_for_status() + return result.data.get("presets") or [] + + def get_default_anatomy_preset_name(self) -> str: + """Name of default anatomy preset. + + Primary preset is used as default preset. But when primary preset is + not set a built-in is used instead. Built-in preset is named '_'. + + Returns: + str: Name of preset that can be used by + 'get_project_anatomy_preset'. + + """ + for preset in self.get_project_anatomy_presets(): + if preset.get("primary"): + return preset["name"] + return "_" + + def get_project_anatomy_preset( + self, preset_name: Optional[str] = None + ) -> AnatomyPresetDict: + """Anatomy preset values by name. + + Get anatomy preset values by preset name. Primary preset is returned + if preset name is set to 'None'. + + Args: + preset_name (Optional[str]): Preset name. + + Returns: + AnatomyPresetDict: Anatomy preset values. + + """ + if preset_name is None: + preset_name = "__primary__" + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + preset_name = self.get_default_anatomy_preset_name() + + result = self.get(f"anatomy/presets/{preset_name}") + result.raise_for_status() + return result.data + + def get_built_in_anatomy_preset(self) -> AnatomyPresetDict: + """Get built-in anatomy preset. + + Returns: + AnatomyPresetDict: Built-in anatomy preset. + + """ + preset_name = "__builtin__" + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + preset_name = "_" + return self.get_project_anatomy_preset(preset_name) + + def get_build_in_anatomy_preset(self) -> AnatomyPresetDict: + warnings.warn( + ( + "Used deprecated 'get_build_in_anatomy_preset' use" + " 'get_built_in_anatomy_preset' instead." + ), + DeprecationWarning + ) + return self.get_built_in_anatomy_preset() + + def get_rest_project( + self, project_name: str + ) -> Optional[ProjectDict]: + """Query project by name. + + This call returns project with anatomy data. + + Args: + project_name (str): Name of project. + + Returns: + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. + + """ + if not project_name: + return None + + response = self.get(f"projects/{project_name}") + # TODO ignore only error about not existing project + if response.status != 200: + return None + project = response.data + self._fill_project_entity_data(project) + return project + + def get_rest_projects( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + ) -> Generator[ProjectDict, None, None]: + """Query available project entities. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + Generator[ProjectDict, None, None]: Available projects. + + """ + for project_name in self.get_project_names(active, library): + project = self.get_rest_project(project_name) + if project: + yield project + + def get_project_names( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + ) -> list[str]: + """Receive available project names. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + list[str]: List of available project names. + + """ + if active is not None: + active = "true" if active else "false" + + if library is not None: + library = "true" if library else "false" + + query = prepare_query_string({"active": active, "library": library}) + + response = self.get(f"projects{query}") + response.raise_for_status() + data = response.data + project_names = [] + if data: + for project in data["projects"]: + project_names.append(project["name"]) + return project_names + + def get_projects( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Generator[ProjectDict, None, None]: + """Get projects. + + Args: + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[ProjectDict, None, None]: Queried projects. + + """ + if fields is not None: + fields = set(fields) + + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + projects_by_name = {} + if graphql_fields: + projects = list(self._get_graphql_projects( + active, + library, + fields=graphql_fields, + own_attributes=own_attributes, + )) + if not use_rest: + yield from projects + return + projects_by_name = {p["name"]: p for p in projects} + + for project in self.get_rest_projects(active, library): + name = project["name"] + graphql_p = projects_by_name.get(name) + if graphql_p: + project["productTypes"] = graphql_p["productTypes"] + yield project + + def get_project( + self, + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[ProjectDict]: + """Get project. + + Args: + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. + + """ + if fields is not None: + fields = set(fields) + + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + graphql_project = None + if graphql_fields: + graphql_project = next(self._get_graphql_projects( + None, + None, + fields=graphql_fields, + own_attributes=own_attributes, + ), None) + if not graphql_project or not use_rest: + return graphql_project + + project = self.get_rest_project(project_name) + if own_attributes: + fill_own_attribs(project) + if graphql_project: + project["productTypes"] = graphql_project["productTypes"] + return project + + def create_project( + self, + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, + ) -> ProjectDict: + """Create project using AYON settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. + + Returns: + ProjectDict: Created project entity. + + """ + if self.get_project(project_name): + raise ValueError( + f"Project with name \"{project_name}\" already exists" + ) + + if not PROJECT_NAME_REGEX.match(project_name): + raise ValueError( + f"Project name \"{project_name}\" contain invalid characters" + ) + + preset = self.get_project_anatomy_preset(preset_name) + + result = self.post( + "projects", + name=project_name, + code=project_code, + anatomy=preset, + library=library_project + ) + + if result.status != 201: + details = f"Unknown details ({result.status})" + if result.data: + details = result.data.get("detail") or details + raise ValueError( + f"Failed to create project \"{project_name}\": {details}" + ) + + return self.get_project(project_name) + + def update_project( + self, + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes + ) -> None: + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + + """ + changes.update({ + key: value + for key, value in ( + ("library", library), + ("folderTypes", folder_types), + ("taskTypes", task_types), + ("linkTypes", link_types), + ("statuses", statuses), + ("tags", tags), + ("config", config), + ("attrib", attrib), + ("data", data), + ("active", active), + ("code", project_code), + ) + if value is not None + }) + response = self.patch( + f"projects/{project_name}", + **changes + ) + response.raise_for_status() + + def delete_project(self, project_name: str): + """Delete project from server. + + This will completely remove project from server without any step back. + + Args: + project_name (str): Project name that will be removed. + + """ + if not self.get_project(project_name): + raise ValueError( + f"Project with name \"{project_name}\" was not found" + ) + + result = self.delete(f"projects/{project_name}") + if result.status_code != 204: + detail = result.data["detail"] + raise ValueError( + f"Failed to delete project \"{project_name}\". {detail}" + ) + + def get_project_root_overrides( + self, project_name: str + ) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + + """ + result = self.get(f"projects/{project_name}/roots") + result.raise_for_status() + return result.data + + def get_project_roots_by_site( + self, project_name: str + ) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Deprecated: + Use 'get_project_root_overrides' instead. Function + deprecated since 1.0.6 + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + + """ + warnings.warn( + ( + "Method 'get_project_roots_by_site' is deprecated." + " Please use 'get_project_root_overrides' instead." + ), + DeprecationWarning + ) + return self.get_project_root_overrides(project_name) + + def get_project_root_overrides_by_site_id( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. + + Returns: + dict[str, str]: Root values by root name or None if + site does not have overrides. + + """ + if site_id is None: + site_id = self.get_site_id() + + if site_id is None: + return {} + roots = self.get_project_root_overrides(project_name) + return roots.get(site_id, {}) + + def get_project_roots_for_site( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + + Deprecated: + Use 'get_project_root_overrides_by_site_id' instead. Function + deprecated since 1.0.6 + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. + + Returns: + dict[str, str]: Root values by root name, root name is not + available if it does not have overrides. + + """ + warnings.warn( + ( + "Method 'get_project_roots_for_site' is deprecated." + " Please use 'get_project_root_overrides_by_site_id' instead." + ), + DeprecationWarning + ) + return self.get_project_root_overrides_by_site_id(project_name) + + def get_project_roots_by_site_id( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root values for a site. + + If site id is not passed a site set in current api object is used + instead. If site id is not available, default roots are returned + for current platform. + + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + root values. + + Returns: + dict[str, str]: Root values. + + """ + if site_id is None: + site_id = self.get_site_id() + + return self._get_project_roots_values(project_name, site_id=site_id) + + def get_project_roots_by_platform( + self, project_name: str, platform_name: Optional[str] = None + ) -> dict[str, str]: + """Root values for a site. + + If platform name is not passed current platform name is used instead. + + This function does return root values without site overrides. It is + possible to use the function to receive default root values. + + Args: + project_name (str): Name of project. + platform_name (Optional[Literal["windows", "linux", "darwin"]]): + Platform name for which want to receive root values. Current + platform name is used if not passed. + + Returns: + dict[str, str]: Root values. + + """ + return self._get_project_roots_values( + project_name, platform_name=platform_name + ) + + def _get_project_graphql_fields( + self, fields: Optional[set[str]] + ) -> tuple[set[str], bool]: + """Fetch of project must be done using REST endpoint. + + Returns: + set[str]: GraphQl fields. + + """ + if fields is None: + return set(), True + + has_product_types = False + graphql_fields = set() + for field in fields: + # Product types are available only in GraphQl + if field.startswith("productTypes"): + has_product_types = True + graphql_fields.add(field) + + if not has_product_types: + return set(), True + + inters = fields & {"name", "code", "active", "library"} + remainders = fields - (inters | graphql_fields) + if remainders: + graphql_fields.add("name") + return graphql_fields, True + graphql_fields |= inters + return graphql_fields, False + + def _fill_project_entity_data(self, project: dict[str, Any]) -> None: + # Add fake scope to statuses if not available + if "statuses" in project: + for status in project["statuses"]: + scope = status.get("scope") + if scope is None: + status["scope"] = [ + "folder", + "task", + "product", + "version", + "representation", + "workfile" + ] + + # Convert 'data' from string to dict if needed + if "data" in project: + project_data = project["data"] + if isinstance(project_data, str): + project_data = json.loads(project_data) + project["data"] = project_data + + # Fill 'bundle' from data if is not filled + if "bundle" not in project: + bundle_data = project["data"].get("bundle", {}) + prod_bundle = bundle_data.get("production") + staging_bundle = bundle_data.get("staging") + project["bundle"] = { + "production": prod_bundle, + "staging": staging_bundle, + } + + # Convert 'config' from string to dict if needed + config = project.get("config") + if isinstance(config, str): + project["config"] = json.loads(config) + + # Unifiy 'linkTypes' data structure from REST and GraphQL + if "linkTypes" in project: + for link_type in project["linkTypes"]: + if "data" in link_type: + link_data = link_type.pop("data") + link_type.update(link_data) + if "style" not in link_type: + link_type["style"] = None + if "color" not in link_type: + link_type["color"] = None + + def _get_graphql_projects( + self, + active: Optional[bool], + library: Optional[bool], + fields: set[str], + own_attributes: bool, + project_name: Optional[str] = None + ) -> Generator[ProjectDict, None, None]: + if active is not None: + fields.add("active") + + if library is not None: + fields.add("library") + + self._prepare_fields("project", fields, own_attributes) + + query = projects_graphql_query(fields) + if project_name is not None: + query.set_variable_value("projectName", project_name) + + for parsed_data in query.continuous_query(self): + for project in parsed_data["projects"]: + if active is not None and active is not project["active"]: + continue + if own_attributes: + fill_own_attribs(project) + self._fill_project_entity_data(project) + yield project + + def _get_project_roots_values( + self, + project_name: str, + site_id: Optional[str] = None, + platform_name: Optional[str] = None, + ) -> dict[str, str]: + """Root values for site or platform. + + Helper function that treats 'siteRoots' endpoint. The endpoint + requires to pass exactly one query value of site id + or platform name. + + When using platform name, it does return default project roots without + any site overrides. + + Output should contain all project roots with all filled values. If + value does not have override on a site, it should be filled with + project default value. + + Args: + project_name (str): Project name. + site_id (Optional[str]): Site id for which want to receive + site overrides. + platform_name (Optional[str]): Platform for which want to receive + roots. + + Returns: + dict[str, str]: Root values. + + """ + query_data = {} + if site_id is not None: + query_data["site_id"] = site_id + else: + if platform_name is None: + platform_name = platform.system() + query_data["platform"] = platform_name.lower() + + query = prepare_query_string(query_data) + response = self.get( + f"projects/{project_name}/siteRoots{query}" + ) + response.raise_for_status() + return response.data diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py new file mode 100644 index 000000000..2eb9814cb --- /dev/null +++ b/ayon_api/_api_helpers/representations.py @@ -0,0 +1,747 @@ +from __future__ import annotations + +import json +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.constants import REPRESENTATION_FILES_FIELDS +from ayon_api.utils import ( + RepresentationHierarchy, + RepresentationParents, + PatternType, + create_entity_id, +) +from ayon_api.graphql_queries import ( + representations_graphql_query, + representations_hierarchy_qraphql_query, +) + +from .base import BaseServerAPI, _PLACEHOLDER + +if typing.TYPE_CHECKING: + from ayon_api.typing import RepresentationDict + + +class RepresentationsAPI(BaseServerAPI): + def get_rest_representation( + self, project_name: str, representation_id: str + ) -> Optional[RepresentationDict]: + return self.get_rest_entity_by_id( + project_name, "representation", representation_id + ) + + def get_representations( + self, + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator[RepresentationDict, None, None]: + """Get representation entities based on passed filters from server. + + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. + + Args: + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Generator[RepresentationDict, None, None]: Queried + representation entities. + + """ + if not fields: + fields = self.get_default_fields_for_type("representation") + else: + fields = set(fields) + self._prepare_fields("representation", fields) + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for representations. " + "The argument will be removed form function signature in " + "future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + if "files" in fields: + fields.discard("files") + fields |= REPRESENTATION_FILES_FIELDS + + filters = { + "projectName": project_name + } + + if representation_ids is not None: + representation_ids = set(representation_ids) + if not representation_ids: + return + filters["representationIds"] = list(representation_ids) + + version_ids_filter = None + representation_names_filter = None + if names_by_version_ids is not None: + version_ids_filter = set() + representation_names_filter = set() + for version_id, names in names_by_version_ids.items(): + version_ids_filter.add(version_id) + representation_names_filter |= set(names) + + if not version_ids_filter or not representation_names_filter: + return + + else: + if representation_names is not None: + representation_names_filter = set(representation_names) + if not representation_names_filter: + return + + if version_ids is not None: + version_ids_filter = set(version_ids) + if not version_ids_filter: + return + + if version_ids_filter: + filters["versionIds"] = list(version_ids_filter) + + if representation_names_filter: + filters["representationNames"] = list(representation_names_filter) + + if statuses is not None: + statuses = set(statuses) + if not statuses: + return + filters["representationStatuses"] = list(statuses) + + if tags is not None: + tags = set(tags) + if not tags: + return + filters["representationTags"] = list(tags) + + if has_links is not None: + filters["representationHasLinks"] = has_links.upper() + + query = representations_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for repre in parsed_data["project"]["representations"]: + if active is not None and active is not repre["active"]: + continue + + self._convert_entity_data(repre) + + self._representation_conversion(repre) + + yield repre + + def get_representation_by_id( + self, + project_name: str, + representation_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[RepresentationDict]: + """Query representation entity from server based on id filter. + + Args: + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[RepresentationDict]: Queried representation + entity or None. + + """ + representations = self.get_representations( + project_name, + representation_ids=[representation_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representation_by_name( + self, + project_name: str, + representation_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[RepresentationDict]: + """Query representation entity by name and version id. + + Args: + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[RepresentationDict]: Queried representation entity + or None. + + """ + representations = self.get_representations( + project_name, + representation_names=[representation_name], + version_ids=[version_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representations_hierarchy( + self, + project_name: str, + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, + ) -> dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. + + Representation entity with parent entities up to project. + + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. + + Returns: + dict[str, RepresentationHierarchy]: Parent entities by + representation id. + + """ + if not representation_ids: + return {} + + if project_fields is not None: + project_fields = set(project_fields) + self._prepare_fields("project", project_fields) + + project = {} + if project_fields is None: + project = self.get_project(project_name) + + elif project_fields: + # Keep project as empty dictionary if does not have + # filled any fields + project = self.get_project( + project_name, fields=project_fields + ) + + repre_ids = set(representation_ids) + output = { + repre_id: RepresentationHierarchy( + project, None, None, None, None, None + ) + for repre_id in representation_ids + } + + if folder_fields is None: + folder_fields = self.get_default_fields_for_type("folder") + else: + folder_fields = set(folder_fields) + + if task_fields is None: + task_fields = self.get_default_fields_for_type("task") + else: + task_fields = set(task_fields) + + if product_fields is None: + product_fields = self.get_default_fields_for_type("product") + else: + product_fields = set(product_fields) + + if version_fields is None: + version_fields = self.get_default_fields_for_type("version") + else: + version_fields = set(version_fields) + + if representation_fields is None: + representation_fields = self.get_default_fields_for_type( + "representation" + ) + else: + representation_fields = set(representation_fields) + + for (entity_type, fields) in ( + ("folder", folder_fields), + ("task", task_fields), + ("product", product_fields), + ("version", version_fields), + ("representation", representation_fields), + ): + self._prepare_fields(entity_type, fields) + + representation_fields.add("id") + + query = representations_hierarchy_qraphql_query( + folder_fields, + task_fields, + product_fields, + version_fields, + representation_fields, + ) + query.set_variable_value("projectName", project_name) + query.set_variable_value("representationIds", list(repre_ids)) + + parsed_data = query.query(self) + for repre in parsed_data["project"]["representations"]: + repre_id = repre["id"] + version = repre.pop("version", {}) + product = version.pop("product", {}) + task = version.pop("task", None) + folder = product.pop("folder", {}) + self._convert_entity_data(repre) + self._representation_conversion(repre) + self._convert_entity_data(version) + self._convert_entity_data(product) + self._convert_entity_data(folder) + if task: + self._convert_entity_data(task) + + output[repre_id] = RepresentationHierarchy( + project, folder, task, product, version, repre + ) + + return output + + def get_representation_hierarchy( + self, + project_name: str, + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, + ) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. + + Returns: + RepresentationHierarchy: Representation hierarchy entities. + + """ + if not representation_id: + return None + + parents_by_repre_id = self.get_representations_hierarchy( + project_name, + [representation_id], + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, + ) + return parents_by_repre_id[representation_id] + + def get_representations_parents( + self, + project_name: str, + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + ) -> dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + + Returns: + dict[str, RepresentationParents]: Parent entities by + representation id. + + """ + hierarchy_by_repre_id = self.get_representations_hierarchy( + project_name, + representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=set(), + product_fields=product_fields, + version_fields=version_fields, + representation_fields={"id"}, + ) + return { + repre_id: RepresentationParents( + hierarchy.version, + hierarchy.product, + hierarchy.folder, + hierarchy.project, + ) + for repre_id, hierarchy in hierarchy_by_repre_id.items() + } + + def get_representation_parents( + self, + project_name: str, + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + ) -> Optional[RepresentationParents]: + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + + Returns: + RepresentationParents: Representation parent entities. + + """ + if not representation_id: + return None + + parents_by_repre_id = self.get_representations_parents( + project_name, + [representation_id], + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, + ) + return parents_by_repre_id[representation_id] + + def get_repre_ids_by_context_filters( + self, + project_name: str, + context_filters: Optional[dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + ) -> list[str]: + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. + + Args: + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. + + Returns: + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> from ayon_api import get_repre_ids_by_context_filters + >>> from ayon_api import get_representations + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) + + """ + if not isinstance(context_filters, dict): + raise TypeError( + f"Expected 'dict' got {str(type(context_filters))}" + ) + + filter_body = {} + if representation_names is not None: + if not representation_names: + return [] + filter_body["names"] = list(set(representation_names)) + + if version_ids is not None: + if not version_ids: + return [] + filter_body["versionIds"] = list(set(version_ids)) + + body_context_filters = [] + for key, filters in context_filters.items(): + if not isinstance(filters, (set, list, tuple)): + raise TypeError( + "Expected 'set', 'list', 'tuple' got {}".format( + str(type(filters)))) + + new_filters = set() + for filter_value in filters: + if isinstance(filter_value, PatternType): + filter_value = filter_value.pattern + new_filters.add(filter_value) + + body_context_filters.append({ + "key": key, + "values": list(new_filters) + }) + + response = self.post( + f"projects/{project_name}/repreContextFilter", + context=body_context_filters, + **filter_body + ) + response.raise_for_status() + return response.data["ids"] + + def create_representation( + self, + project_name: str, + name: str, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]]=None, + status: Optional[str] = None, + active: Optional[bool] = None, + representation_id: Optional[str] = None, + ) -> str: + """Create new representation. + + Args: + project_name (str): Project name. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. + + Returns: + str: Representation id. + + """ + if not representation_id: + representation_id = create_entity_id() + create_data = { + "id": representation_id, + "name": name, + "versionId": version_id, + } + for key, value in ( + ("files", files), + ("attrib", attrib), + ("data", data), + ("traits", traits), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/representations", + **create_data + ) + response.raise_for_status() + return representation_id + + def update_representation( + self, + project_name: str, + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ) -> None: + """Update representation entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + representation_id (str): Representation id. + name (Optional[str]): New name. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("versionId", version_id), + ("files", files), + ("attrib", attrib), + ("data", data), + ("traits", traits), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/representations/{representation_id}", + **update_data + ) + response.raise_for_status() + + def delete_representation( + self, project_name: str, representation_id: str + ) -> None: + """Delete representation. + + Args: + project_name (str): Project name. + representation_id (str): Representation id to delete. + + """ + response = self.delete( + f"projects/{project_name}/representations/{representation_id}" + ) + response.raise_for_status() + + def _representation_conversion( + self, representation: RepresentationDict + ) -> None: + if "context" in representation: + orig_context = representation["context"] + context = {} + if orig_context and orig_context != "null": + context = json.loads(orig_context) + representation["context"] = context + + repre_files = representation.get("files") + if not repre_files: + return + + for repre_file in repre_files: + repre_file_size = repre_file.get("size") + if repre_file_size is not None: + repre_file["size"] = int(repre_file["size"]) diff --git a/ayon_api/_api_helpers/secrets.py b/ayon_api/_api_helpers/secrets.py new file mode 100644 index 000000000..188d696c9 --- /dev/null +++ b/ayon_api/_api_helpers/secrets.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import typing + +from .base import BaseServerAPI +if typing.TYPE_CHECKING: + from ayon_api.typing import SecretDict + + +class SecretsAPI(BaseServerAPI): + def get_secrets(self) -> list[SecretDict]: + """Get all secrets. + + Example output:: + + [ + { + "name": "secret_1", + "value": "secret_value_1", + }, + { + "name": "secret_2", + "value": "secret_value_2", + } + ] + + Returns: + list[SecretDict]: List of secret entities. + + """ + response = self.get("secrets") + response.raise_for_status() + return response.data + + def get_secret(self, secret_name: str) -> SecretDict: + """Get secret by name. + + Example output:: + + { + "name": "secret_name", + "value": "secret_value", + } + + Args: + secret_name (str): Name of secret. + + Returns: + dict[str, str]: Secret entity data. + + """ + response = self.get(f"secrets/{secret_name}") + response.raise_for_status() + return response.data + + def save_secret(self, secret_name: str, secret_value: str) -> None: + """Save secret. + + This endpoint can create and update secret. + + Args: + secret_name (str): Name of secret. + secret_value (str): Value of secret. + + """ + response = self.put( + f"secrets/{secret_name}", + name=secret_name, + value=secret_value, + ) + response.raise_for_status() + + def delete_secret(self, secret_name: str) -> None: + """Delete secret by name. + + Args: + secret_name (str): Name of secret to delete. + + """ + response = self.delete(f"secrets/{secret_name}") + response.raise_for_status() diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py new file mode 100644 index 000000000..aa984032b --- /dev/null +++ b/ayon_api/_api_helpers/tasks.py @@ -0,0 +1,515 @@ +from __future__ import annotations + +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.utils import ( + prepare_list_filters, + fill_own_attribs, + create_entity_id, + NOT_SET, +) +from ayon_api.graphql_queries import ( + tasks_graphql_query, + tasks_by_folder_paths_graphql_query, +) + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import TaskDict + + +class TasksAPI(BaseServerAPI): + def get_rest_task( + self, project_name: str, task_id: str + ) -> Optional[TaskDict]: + return self.get_rest_entity_by_id(project_name, "task", task_id) + + def get_tasks( + self, + project_name: str, + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Generator[TaskDict, None, None]: + """Query task entities from server. + + Args: + project_name (str): Name of project. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[TaskDict, None, None]: Queried task entities. + + """ + if not project_name: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("taskIds", task_ids), + ("taskNames", task_names), + ("taskTypes", task_types), + ("folderIds", folder_ids), + ("taskAssigneesAny", assignees), + ("taskAssigneesAll", assignees_all), + ("taskStatuses", statuses), + ("taskTags", tags), + ): + return + + if not fields: + fields = self.get_default_fields_for_type("task") + else: + fields = set(fields) + self._prepare_fields("task", fields, own_attributes) + + if active is not None: + fields.add("active") + + query = tasks_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for task in parsed_data["project"]["tasks"]: + if active is not None and active is not task["active"]: + continue + + self._convert_entity_data(task) + + if own_attributes: + fill_own_attribs(task) + yield task + + def get_task_by_name( + self, + project_name: str, + folder_id: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional[TaskDict]: + """Query task entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + task_name (str): Task name + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks( + project_name, + folder_ids=[folder_id], + task_names=[task_name], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + def get_task_by_id( + self, + project_name: str, + task_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Optional[TaskDict]: + """Query task entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + task_id (str): Task id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks( + project_name, + task_ids=[task_id], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + def get_tasks_by_folder_paths( + self, + project_name: str, + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> dict[str, list[TaskDict]]: + """Query task entities from server by folder paths. + + Args: + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + dict[str, list[TaskDict]]: Task entities by + folder path. + + """ + folder_paths = set(folder_paths) + if not project_name or not folder_paths: + return {} + + filters = { + "projectName": project_name, + "folderPaths": list(folder_paths), + } + if not prepare_list_filters( + filters, + ("taskNames", task_names), + ("taskTypes", task_types), + ("taskAssigneesAny", assignees), + ("taskAssigneesAll", assignees_all), + ("taskStatuses", statuses), + ("taskTags", tags), + ): + return {} + + if not fields: + fields = self.get_default_fields_for_type("task") + else: + fields = set(fields) + self._prepare_fields("task", fields, own_attributes) + + if active is not None: + fields.add("active") + + query = tasks_by_folder_paths_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + output = { + folder_path: [] + for folder_path in folder_paths + } + for parsed_data in query.continuous_query(self): + for folder in parsed_data["project"]["folders"]: + folder_path = folder["path"] + for task in folder["tasks"]: + if active is not None and active is not task["active"]: + continue + + self._convert_entity_data(task) + + if own_attributes: + fill_own_attribs(task) + output[folder_path].append(task) + return output + + def get_tasks_by_folder_path( + self, + project_name: str, + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> list[TaskDict]: + """Query task entities from server by folder path. + + Args: + project_name (str): Name of project. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + """ + return self.get_tasks_by_folder_paths( + project_name, + [folder_path], + task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes + )[folder_path] + + def get_task_by_folder_path( + self, + project_name: str, + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Optional[TaskDict]: + """Query task entity by folder path and task name. + + Args: + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks_by_folder_path( + project_name, + folder_path, + active=None, + task_names=[task_name], + fields=fields, + own_attributes=own_attributes, + ): + return task + return None + + def create_task( + self, + project_name: str, + name: str, + task_type: str, + folder_id: str, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, + ) -> str: + """Create new task. + + Args: + project_name (str): Project name. + name (str): Folder name. + task_type (str): Task type. + folder_id (str): Parent folder id. + label (Optional[str]): Label of folder. + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is + generated. + + Returns: + str: Task id. + + """ + if not task_id: + task_id = create_entity_id() + create_data = { + "id": task_id, + "name": name, + "taskType": task_type, + "folderId": folder_id, + } + for key, value in ( + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("assignees", assignees), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/tasks", + **create_data + ) + response.raise_for_status() + return task_id + + def update_task( + self, + project_name: str, + task_id: str, + name: Optional[str] = None, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ) -> None: + """Update task entity on server. + + Do not pass ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + task_id (str): Task id. + name (Optional[str]): New name. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. + label (Optional[Optional[str]]): New label. + assignees (Optional[str]): New assignees. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("taskType", task_type), + ("folderId", folder_id), + ("assignees", assignees), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("label", label), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/tasks/{task_id}", + **update_data + ) + response.raise_for_status() + + def delete_task(self, project_name: str, task_id: str) -> None: + """Delete task. + + Args: + project_name (str): Project name. + task_id (str): Task id to delete. + + """ + response = self.delete( + f"projects/{project_name}/tasks/{task_id}" + ) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py new file mode 100644 index 000000000..e4b1c56d1 --- /dev/null +++ b/ayon_api/_api_helpers/thumbnails.py @@ -0,0 +1,307 @@ +from __future__ import annotations + +import os +import warnings +from typing import Optional + +from ayon_api.utils import ( + get_media_mime_type, + ThumbnailContent, + RequestTypes, + RestApiResponse, +) + +from .base import BaseServerAPI + + +class ThumbnailsAPI(BaseServerAPI): + def get_thumbnail_by_id( + self, project_name: str, thumbnail_id: str + ) -> ThumbnailContent: + """Get thumbnail from server by id. + + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + response = self.raw_get( + f"projects/{project_name}/thumbnails/{thumbnail_id}" + ) + return self._prepare_thumbnail_content(project_name, response) + + def get_thumbnail( + self, + project_name: str, + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Get thumbnail from server. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + + if entity_type in ( + "folder", + "task", + "version", + "workfile", + ): + entity_type += "s" + + response = self.raw_get( + f"projects/{project_name}/{entity_type}/{entity_id}/thumbnail" + ) + return self._prepare_thumbnail_content(project_name, response) + + def get_folder_thumbnail( + self, + project_name: str, + folder_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. + + Args: + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_folder_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "folder", folder_id + ) + + def get_task_thumbnail( + self, + project_name: str, + task_id: str, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + return self.get_thumbnail(project_name, "task", task_id) + + def get_version_thumbnail( + self, + project_name: str, + version_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. + + Args: + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_version_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "version", version_id + ) + + def get_workfile_thumbnail( + self, + project_name: str, + workfile_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_workfile_thumbnail' got 'thumbnail_id'" + " which is deprecated and will be removed in future" + " version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "workfile", workfile_id + ) + + def create_thumbnail( + self, + project_name: str, + src_filepath: str, + thumbnail_id: Optional[str] = None, + ) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + if thumbnail_id: + self.update_thumbnail( + project_name, + thumbnail_id, + src_filepath + ) + return thumbnail_id + + mime_type = get_media_mime_type(src_filepath) + response = self.upload_file( + f"projects/{project_name}/thumbnails", + src_filepath, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + return response.json()["id"] + + def update_thumbnail( + self, project_name: str, thumbnail_id: str, src_filepath: str + ) -> None: + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + mime_type = get_media_mime_type(src_filepath) + response = self.upload_file( + f"projects/{project_name}/thumbnails/{thumbnail_id}", + src_filepath, + request_type=RequestTypes.put, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + + def _prepare_thumbnail_content( + self, + project_name: str, + response: RestApiResponse, + ) -> ThumbnailContent: + content = None + content_type = response.content_type + + # It is expected the response contains thumbnail id otherwise the + # content cannot be cached and filepath returned + thumbnail_id = response.headers.get("X-Thumbnail-Id") + if thumbnail_id is not None: + content = response.content + + return ThumbnailContent( + project_name, thumbnail_id, content, content_type + ) diff --git a/ayon_api/_api_helpers/versions.py b/ayon_api/_api_helpers/versions.py new file mode 100644 index 000000000..fe8a02469 --- /dev/null +++ b/ayon_api/_api_helpers/versions.py @@ -0,0 +1,640 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ayon_api.utils import ( + NOT_SET, + create_entity_id, + prepare_list_filters, +) +from ayon_api.graphql import GraphQlQuery +from ayon_api.graphql_queries import versions_graphql_query + +from .base import BaseServerAPI, _PLACEHOLDER + +if typing.TYPE_CHECKING: + from ayon_api.typing import VersionDict + + +class VersionsAPI(BaseServerAPI): + def get_rest_version( + self, project_name: str, version_id: str + ) -> Optional[VersionDict]: + return self.get_rest_entity_by_id(project_name, "version", version_id) + + def get_versions( + self, + project_name: str, + version_ids: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Generator[VersionDict, None, None]: + """Get version entities based on passed filters from server. + + Args: + project_name (str): Name of project where to look for versions. + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Generator[VersionDict, None, None]: Queried version entities. + + """ + if not fields: + fields = self.get_default_fields_for_type("version") + else: + fields = set(fields) + self._prepare_fields("version", fields) + + # Make sure fields have minimum required fields + fields |= {"id", "version"} + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for versions. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + if not hero and not standard: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("taskIds", task_ids), + ("versionIds", version_ids), + ("productIds", product_ids), + ("taskIds", task_ids), + ("versions", versions), + ("versionStatuses", statuses), + ("versionTags", tags), + ): + return + + queries = [] + # Add filters based on 'hero' and 'standard' + # NOTE: There is not a filter to "ignore" hero versions or to get + # latest and hero version + # - if latest and hero versions should be returned it must be done in + # 2 graphql queries + if standard and not latest: + # This query all versions standard + hero + # - hero must be filtered out if is not enabled during loop + query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + queries.append(query) + else: + if hero: + # Add hero query if hero is enabled + hero_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + hero_query.set_variable_value(attr, filter_value) + + hero_query.set_variable_value("heroOnly", True) + queries.append(hero_query) + + if standard: + standard_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + standard_query.set_variable_value(attr, filter_value) + + if latest: + standard_query.set_variable_value("latestOnly", True) + queries.append(standard_query) + + for query in queries: + for parsed_data in query.continuous_query(self): + for version in parsed_data["project"]["versions"]: + if active is not None and version["active"] is not active: + continue + + if not hero and version["version"] < 0: + continue + + self._convert_entity_data(version) + + yield version + + def get_version_by_id( + self, + project_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[VersionDict]: + """Query version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_versions( + project_name, + version_ids={version_id}, + active=None, + hero=True, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_version_by_name( + self, + project_name: str, + version: int, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[VersionDict]: + """Query version entity by version and product id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_versions( + project_name, + product_ids={product_id}, + versions={version}, + active=None, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_id( + self, + project_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[VersionDict]: + """Query hero version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_hero_versions( + project_name, + version_ids=[version_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_product_id( + self, + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional[VersionDict]: + """Query hero version entity by product id. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_hero_versions( + project_name, + product_ids=[product_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_versions( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator[VersionDict, None, None]: + """Query hero versions by multiple filters. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + return self.get_versions( + project_name, + version_ids=version_ids, + product_ids=product_ids, + hero=True, + standard=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def get_last_versions( + self, + project_name: str, + product_ids: Iterable[str], + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> dict[str, Optional[VersionDict]]: + """Query last version entities by product ids. + + Args: + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + dict[str, Optional[VersionDict]]: Last versions by product id. + + """ + if fields: + fields = set(fields) + fields.add("productId") + product_ids = set(product_ids) + versions = self.get_versions( + project_name, + product_ids=product_ids, + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + output = { + version["productId"]: version + for version in versions + } + for product_id in product_ids: + output.setdefault(product_id, None) + return output + + def get_last_version_by_product_id( + self, + project_name: str, + product_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[VersionDict]: + """Query last version entity by product id. + + Args: + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Queried version entity or None. + + """ + versions = self.get_versions( + project_name, + product_ids=[product_id], + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_last_version_by_product_name( + self, + project_name: str, + product_name: str, + folder_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[VersionDict]: + """Query last version entity by product name and folder id. + + Args: + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[VersionDict]: Queried version entity or None. + + """ + if not folder_id: + return None + + product = self.get_product_by_name( + project_name, product_name, folder_id, fields={"id"} + ) + if not product: + return None + return self.get_last_version_by_product_id( + project_name, + product["id"], + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def version_is_latest(self, project_name: str, version_id: str) -> bool: + """Is version latest from a product. + + Args: + project_name (str): Project where to look for representation. + version_id (str): Version id. + + Returns: + bool: Version is latest or not. + + """ + query = GraphQlQuery("VersionIsLatest") + project_name_var = query.add_variable( + "projectName", "String!", project_name + ) + version_id_var = query.add_variable( + "versionId", "String!", version_id + ) + project_query = query.add_field("project") + project_query.set_filter("name", project_name_var) + version_query = project_query.add_field("version") + version_query.set_filter("id", version_id_var) + product_query = version_query.add_field("product") + latest_version_query = product_query.add_field("latestVersion") + latest_version_query.add_field("id") + + parsed_data = query.query(self) + latest_version = ( + parsed_data["project"]["version"]["product"]["latestVersion"] + ) + return latest_version["id"] == version_id + + def create_version( + self, + project_name: str, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, + ) -> str: + """Create new version. + + Args: + project_name (str): Project name. + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is + generated. + + Returns: + str: Version id. + + """ + if not version_id: + version_id = create_entity_id() + create_data = { + "id": version_id, + "version": version, + "productId": product_id, + } + for key, value in ( + ("taskId", task_id), + ("author", author), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/versions", + **create_data + ) + response.raise_for_status() + return version_id + + def update_version( + self, + project_name: str, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ) -> None: + """Update version entity on server. + + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[str]): New task id. + author (Optional[str]): New author username. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("version", version), + ("productId", product_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("author", author), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("taskId", task_id), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/versions/{version_id}", + **update_data + ) + response.raise_for_status() + + def delete_version(self, project_name: str, version_id: str) -> None: + """Delete version. + + Args: + project_name (str): Project name. + version_id (str): Version id to delete. + + """ + response = self.delete( + f"projects/{project_name}/versions/{version_id}" + ) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py new file mode 100644 index 000000000..e27aab3c1 --- /dev/null +++ b/ayon_api/_api_helpers/workfiles.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Iterable, Generator + +from ayon_api.graphql_queries import workfiles_info_graphql_query + +from .base import BaseServerAPI, _PLACEHOLDER + +if typing.TYPE_CHECKING: + from ayon_api.typing import WorkfileInfoDict + + +class WorkfilesAPI(BaseServerAPI): + def get_workfiles_info( + self, + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] =None, + paths: Optional[Iterable[str]] =None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str]=None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator[WorkfileInfoDict, None, None]: + """Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ + filters = {"projectName": project_name} + if task_ids is not None: + task_ids = set(task_ids) + if not task_ids: + return + filters["taskIds"] = list(task_ids) + + if paths is not None: + paths = set(paths) + if not paths: + return + filters["paths"] = list(paths) + + if path_regex is not None: + filters["workfilePathRegex"] = path_regex + + if workfile_ids is not None: + workfile_ids = set(workfile_ids) + if not workfile_ids: + return + filters["workfileIds"] = list(workfile_ids) + + if statuses is not None: + statuses = set(statuses) + if not statuses: + return + filters["workfileStatuses"] = list(statuses) + + if tags is not None: + tags = set(tags) + if not tags: + return + filters["workfileTags"] = list(tags) + + if has_links is not None: + filters["workfilehasLinks"] = has_links.upper() + + if not fields: + fields = self.get_default_fields_for_type("workfile") + else: + fields = set(fields) + self._prepare_fields("workfile", fields) + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for workfiles. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + query = workfiles_info_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for workfile_info in parsed_data["project"]["workfiles"]: + self._convert_entity_data(workfile_info) + yield workfile_info + + def get_workfile_info( + self, + project_name: str, + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[WorkfileInfoDict]: + """Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if not task_id or not path: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + task_ids=[task_id], + paths=[path], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None + + def get_workfile_info_by_id( + self, + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[WorkfileInfoDict]: + """Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if not workfile_id: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + workfile_ids=[workfile_id], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None diff --git a/ayon_api/_base.py b/ayon_api/_base.py deleted file mode 100644 index b3863a2c4..000000000 --- a/ayon_api/_base.py +++ /dev/null @@ -1,46 +0,0 @@ -import typing -from typing import Set - -if typing.TYPE_CHECKING: - from .typing import AnyEntityDict - - -class _BaseServerAPI: - def get(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def post(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def put(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def patch(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def delete(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def raw_get(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def raw_post(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def raw_put(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def raw_patch(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def raw_delete(self, entrypoint: str, **kwargs): - raise NotImplementedError() - - def get_default_settings_variant(self) -> str: - raise NotImplementedError() - - def get_default_fields_for_type(self, entity_type: str) -> Set[str]: - raise NotImplementedError() - - def _convert_entity_data(self, entity: "AnyEntityDict"): - raise NotImplementedError() diff --git a/ayon_api/constants.py b/ayon_api/constants.py index d47e4fc59..6dada2de5 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -1,3 +1,5 @@ +import re + # Environments where server url and api key are stored for global connection SERVER_URL_ENV_KEY = "AYON_SERVER_URL" SERVER_API_ENV_KEY = "AYON_API_KEY" @@ -11,6 +13,12 @@ # Backwards compatibility SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY +# This should be collected from server schema +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + f"^[{PROJECT_NAME_ALLOWED_SYMBOLS}]+$" +) + # --- User --- DEFAULT_USER_FIELDS = { "accessGroups", diff --git a/ayon_api/exceptions.py b/ayon_api/exceptions.py index 32c786834..6a44e9d9c 100644 --- a/ayon_api/exceptions.py +++ b/ayon_api/exceptions.py @@ -1,5 +1,18 @@ import copy +try: + # This should be used if 'requests' have it available + from requests.exceptions import JSONDecodeError +except ImportError: + # Older versions of 'requests' don't have custom exception for json + # decode error + try: + from simplejson import JSONDecodeError + except ImportError: + from json import JSONDecodeError + +RequestsJSONDecodeError = JSONDecodeError + class UrlError(Exception): """Url cannot be parsed as url. diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4fecb75da..f016c0b1f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -3,40 +3,22 @@ Provides access to server API. """ +from __future__ import annotations + import os import re import io import json import time import logging -import collections import platform -import copy import uuid -import warnings -import itertools from contextlib import contextmanager import typing -from typing import Optional, Iterable, Tuple, Generator, Dict, List, Set, Any - -try: - from http import HTTPStatus -except ImportError: - HTTPStatus = None +from typing import Optional, Iterable, Generator, Any import requests -try: - # This should be used if 'requests' have it available - from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError -except ImportError: - # Older versions of 'requests' don't have custom exception for json - # decode error - try: - from simplejson import JSONDecodeError as RequestsJSONDecodeError - except ImportError: - from json import JSONDecodeError as RequestsJSONDecodeError - from .constants import ( SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, @@ -57,95 +39,61 @@ DEFAULT_USER_FIELDS, DEFAULT_ENTITY_LIST_FIELDS, ) -from .graphql import GraphQlQuery, INTROSPECTION_QUERY -from .graphql_queries import ( - projects_graphql_query, - product_types_query, - folders_graphql_query, - tasks_graphql_query, - tasks_by_folder_paths_graphql_query, - products_graphql_query, - versions_graphql_query, - representations_graphql_query, - representations_hierarchy_qraphql_query, - workfiles_info_graphql_query, - events_graphql_query, - users_graphql_query, - activities_graphql_query, -) +from .graphql import INTROSPECTION_QUERY +from .graphql_queries import users_graphql_query from .exceptions import ( FailedOperations, UnauthorizedError, AuthenticationError, ServerNotReached, - ServerError, - HTTPRequestError, - UnsupportedServerVersion, ) from .utils import ( - RepresentationParents, - RepresentationHierarchy, + RequestType, + RequestTypes, + RestApiResponse, prepare_query_string, logout_from_server, create_entity_id, entity_data_json_default, failed_json_default, TransferProgress, - ThumbnailContent, get_default_timeout, get_default_settings_variant, get_default_site_id, NOT_SET, get_media_mime_type, - SortOrder, get_machine_name, + fill_own_attribs, +) +from ._api_helpers import ( + InstallersAPI, + DependencyPackagesAPI, + SecretsAPI, + BundlesAddonsAPI, + EventsAPI, + AttributesAPI, + ProjectsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + RepresentationsAPI, + WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, + LinksAPI, + ListsAPI, ) -from ._actions import _ActionsAPI -from ._lists import _ListsAPI if typing.TYPE_CHECKING: from typing import Union from .typing import ( - ActivityType, - ActivityReferenceType, - LinkDirection, - EventFilter, - AttributeScope, - AttributeSchemaDataDict, - AttributeSchemaDict, - AttributesSchemaDict, - AddonsInfoDict, - InstallersInfoDict, - DependencyPackagesDict, - DevBundleAddonInfoDict, - BundlesInfoDict, - AnatomyPresetDict, - SecretDict, - + ServerVersion, AnyEntityDict, - ProjectDict, - FolderDict, - TaskDict, - ProductDict, - VersionDict, - RepresentationDict, - WorkfileInfoDict, - FlatFolderDict, - - ProjectHierarchyDict, - ProductTypeDict, StreamType, ) -PatternType = type(re.compile("")) -JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) -# This should be collected from server schema -PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" -PROJECT_NAME_REGEX = re.compile( - "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) -) -_PLACEHOLDER = object() - VERSION_REGEX = re.compile( r"(?P0|[1-9]\d*)" r"\.(?P0|[1-9]\d*)" @@ -155,155 +103,6 @@ ) -def _convert_list_filter_value(value): - if value is None: - return None - - if isinstance(value, PatternType): - return [value.pattern] - - if isinstance(value, (int, float, str, bool)): - return [value] - return list(set(value)) - - -def _prepare_list_filters(output, *args, **kwargs): - for key, value in itertools.chain(args, kwargs.items()): - value = _convert_list_filter_value(value) - if value is None: - continue - if not value: - return False - output[key] = value - return True - - -def _get_description(response): - if HTTPStatus is None: - return str(response.orig_response) - return HTTPStatus(response.status).description - - -class RequestType: - def __init__(self, name: str): - self.name: str = name - - def __hash__(self): - return self.name.__hash__() - - -class RequestTypes: - get = RequestType("GET") - post = RequestType("POST") - put = RequestType("PUT") - patch = RequestType("PATCH") - delete = RequestType("DELETE") - - -class RestApiResponse(object): - """API Response.""" - - def __init__(self, response, data=None): - if response is None: - status_code = 500 - else: - status_code = response.status_code - self._response = response - self.status = status_code - self._data = data - - @property - def text(self): - if self._response is None: - return self.detail - return self._response.text - - @property - def orig_response(self): - return self._response - - @property - def headers(self): - if self._response is None: - return {} - return self._response.headers - - @property - def data(self): - if self._data is None: - try: - self._data = self.orig_response.json() - except RequestsJSONDecodeError: - self._data = {} - return self._data - - @property - def content(self): - if self._response is None: - return b"" - return self._response.content - - @property - def content_type(self) -> Optional[str]: - return self.headers.get("Content-Type") - - @property - def detail(self): - detail = self.get("detail") - if detail: - return detail - return _get_description(self) - - @property - def status_code(self) -> int: - return self.status - - @property - def ok(self) -> bool: - if self._response is not None: - return self._response.ok - return False - - def raise_for_status(self, message=None): - if self._response is None: - if self._data and self._data.get("detail"): - raise ServerError(self._data["detail"]) - raise ValueError("Response is not available.") - - if self.status_code == 401: - raise UnauthorizedError("Missing or invalid authentication token") - try: - self._response.raise_for_status() - except requests.exceptions.HTTPError as exc: - if message is None: - message = str(exc) - raise HTTPRequestError(message, exc.response) - - def __enter__(self, *args, **kwargs): - return self._response.__enter__(*args, **kwargs) - - def __contains__(self, key): - return key in self.data - - def __repr__(self): - return f"<{self.__class__.__name__} [{self.status}]>" - - def __len__(self): - return int(200 <= self.status < 400) - - def __bool__(self): - return 200 <= self.status < 400 - - def __getitem__(self, key): - return self.data[key] - - def get(self, key, default=None): - data = self.data - if isinstance(data, dict): - return self.data.get(key, default) - return default - - class GraphQlResponse: """GraphQl response.""" @@ -323,25 +122,6 @@ def __repr__(self): return f"<{self.__class__.__name__}>" -def fill_own_attribs(entity): - if not entity or not entity.get("attrib"): - return - - attributes = entity.get("ownAttrib") - if attributes is None: - return - attributes = set(attributes) - - own_attrib = {} - entity["ownAttrib"] = own_attrib - - for key, value in entity["attrib"].items(): - if key not in attributes: - own_attrib[key] = None - else: - own_attrib[key] = copy.deepcopy(value) - - class _AsUserStack: """Handle stack of users used over server api connection in service mode. @@ -426,7 +206,26 @@ def as_user(self, username): self._last_user = new_last_user -class ServerAPI(_ListsAPI, _ActionsAPI): +class ServerAPI( + InstallersAPI, + DependencyPackagesAPI, + SecretsAPI, + BundlesAddonsAPI, + EventsAPI, + AttributesAPI, + ProjectsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + RepresentationsAPI, + WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, + LinksAPI, + ListsAPI, +): """Base handler of connection to server. Requires url to server which is used as base for api and graphql calls. @@ -1043,7 +842,7 @@ def _update_session_headers(self): elif key in self._session.headers: self._session.headers.pop(key) - def get_info(self) -> Dict[str, Any]: + def get_info(self) -> dict[str, Any]: """Get information about current used api key. By default, the 'info' contains only 'uptime' and 'version'. With @@ -1074,7 +873,7 @@ def get_server_version(self) -> str: self._server_version = self.get_info()["version"] return self._server_version - def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: + def get_server_version_tuple(self) -> "ServerVersion": """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -1082,8 +881,7 @@ def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: This function only returns first three numbers of version. Returns: - Tuple[int, int, int, Union[str, None], Union[str, None]]: Server - version. + ServerVersion: Server version. """ if self._server_version_tuple is None: @@ -1099,7 +897,7 @@ def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: return self._server_version_tuple server_version = property(get_server_version) - server_version_tuple: Tuple[int, int, int, str, str] = property( + server_version_tuple: "ServerVersion" = property( get_server_version_tuple ) @@ -1113,7 +911,7 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations - def _get_user_info(self) -> Optional[Dict[str, Any]]: + def _get_user_info(self) -> Optional[dict[str, Any]]: if self._access_token is None: return None @@ -1142,7 +940,7 @@ def get_users( usernames: Optional[Iterable[str]] = None, emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: + ) -> Generator[dict[str, Any], None, None]: """Get Users. Only administrators and managers can fetch all users. For other users @@ -1217,7 +1015,7 @@ def get_user_by_name( username: str, project_name: Optional[str] = None, fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -1247,7 +1045,7 @@ def get_user_by_name( def get_user( self, username: Optional[str] = None - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. @@ -1256,7 +1054,7 @@ def get_user( username (Optional[str]): Username. Returns: - Optional[Dict[str, Any]]: User info or None if user is not + Optional[dict[str, Any]]: User info or None if user is not found. """ @@ -1279,7 +1077,7 @@ def get_user( def get_headers( self, content_type: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: if content_type is None: content_type = "application/json" @@ -1454,22 +1252,7 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response - content_type = response.headers.get("Content-Type") - if content_type == "application/json": - try: - new_response = RestApiResponse(response) - except JSONDecodeError: - new_response = RestApiResponse( - None, - { - "detail": "The response is not a JSON: {}".format( - response.text) - } - ) - - else: - new_response = RestApiResponse(response) - + new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response @@ -1533,693 +1316,273 @@ def get(self, entrypoint: str, **kwargs): def delete(self, entrypoint: str, **kwargs): return self.raw_delete(entrypoint, params=kwargs) - def get_event(self, event_id: str) -> Optional[Dict[str, Any]]: - """Query full event data by id. + def _endpoint_to_url( + self, + endpoint: str, + use_rest: Optional[bool] = True + ) -> str: + """Cleanup endpoint and return full url to AYON server. - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. + If endpoint already starts with server url only slashes are removed. Args: - event_id (str): Event id. + endpoint (str): Endpoint to be cleaned. + use_rest (Optional[bool]): Use only base server url if set to + False, otherwise REST endpoint is used. Returns: - dict[str, Any]: Full event data. + str: Full url to AYON server. """ - response = self.get(f"events/{event_id}") - response.raise_for_status() - return response.data + endpoint = endpoint.lstrip("/").rstrip("/") + if endpoint.startswith(self._base_url): + return endpoint + base_url = self._rest_url if use_rest else self._graphql_url + return f"{base_url}/{endpoint}" - def get_events( - self, - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: - """Get events from server with filtering options. + def _download_file_to_stream( + self, url: str, stream, chunk_size, progress + ): + kwargs = {"stream": True} + if self._session is None: + kwargs["headers"] = self.get_headers() + get_func = self._base_functions_mapping[RequestTypes.get] + else: + get_func = self._session_functions_mapping[RequestTypes.get] - Notes: - Not all event happen on a project. + with get_func(url, **kwargs) as response: + response.raise_for_status() + progress.set_content_size(response.headers["Content-length"]) + for chunk in response.iter_content(chunk_size=chunk_size): + stream.write(chunk) + progress.add_transferred_chunk(len(chunk)) - Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. - fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending - or descending order. It is recommended to set 'limit' - when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. + def download_file_to_stream( + self, + endpoint: str, + stream: "StreamType", + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download file from AYON server to IOStream. - Returns: - Generator[dict[str, Any]]: Available events matching filters. + Endpoint can be full url (must start with 'base_url' of api object). - """ - if statuses is None and states is not None: - warnings.warn( - ( - "Used deprecated argument 'states' in 'get_events'." - " Use 'statuses' instead." - ), - DeprecationWarning - ) - statuses = states + Progress object can be used to track download. Can be used when + download happens in thread and other thread want to catch changes over + time. - filters = {} - if not _prepare_list_filters( - filters, - ("eventTopics", topics), - ("eventIds", event_ids), - ("projectNames", project_names), - ("eventStatuses", statuses), - ("eventUsers", users), - ): - return + Todos: + Use retries and timeout. + Return RestApiResponse. - if include_logs is None: - include_logs = False + Args: + endpoint (str): Endpoint or URL to file that should be downloaded. + stream (StreamType): Stream where output will + be stored. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. - for filter_key, filter_value in ( - ("includeLogsFilter", include_logs), - ("hasChildrenFilter", has_children), - ("newerThanFilter", newer_than), - ("olderThanFilter", older_than), - ): - if filter_value is not None: - filters[filter_key] = filter_value + """ + if not chunk_size: + chunk_size = self.default_download_chunk_size - if not fields: - fields = self.get_default_fields_for_type("event") + url = self._endpoint_to_url(endpoint) - major, minor, patch, _, _ = self.server_version_tuple - use_states = (major, minor, patch) <= (1, 5, 6) + if progress is None: + progress = TransferProgress() - query = events_graphql_query(set(fields), order, use_states) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) + progress.set_source_url(url) + progress.set_started() - if limit: - events_field = query.get_field_by_path("events") - events_field.set_limit(limit) + try: + self._download_file_to_stream( + url, stream, chunk_size, progress + ) - for parsed_data in query.continuous_query(self): - for event in parsed_data["events"]: - yield event + except Exception as exc: + progress.set_failed(str(exc)) + raise - def update_event( - self, - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, - ): - """Update event data. + finally: + progress.set_transfer_done() + return progress - Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. + def download_file( + self, + endpoint: str, + filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download file from AYON server. - """ - kwargs = { - key: value - for key, value in ( - ("sender", sender), - ("project", project_name), - ("user", username), - ("status", status), - ("description", description), - ("summary", summary), - ("payload", payload), - ("progress", progress), - ("retries", retries), - ) - if value is not None - } + Endpoint can be full url (must start with 'base_url' of api object). - response = self.patch( - f"events/{event_id}", - **kwargs - ) - response.raise_for_status() + Progress object can be used to track download. Can be used when + download happens in thread and other thread want to catch changes over + time. - def dispatch_event( - self, - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[List[str]] = None, - ): - """Dispatch event to server. + Todos: + Use retries and timeout. + Return RestApiResponse. Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. - - Returns: - RestApiResponse: Response from server. + endpoint (str): Endpoint or URL to file that should be downloaded. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. """ - if summary is None: - summary = {} - if payload is None: - payload = {} - event_data = { - "topic": topic, - "sender": sender, - "hash": event_hash, - "project": project_name, - "user": username, - "description": description, - "summary": summary, - "payload": payload, - "finished": finished, - "store": store, - } - if depends_on: - event_data["dependsOn"] = depends_on - - if dependencies: - warnings.warn( - ( - "Used deprecated argument 'dependencies' in" - " 'dispatch_event'. Use 'depends_on' instead." - ), - DeprecationWarning - ) - - response = self.post("events", **event_data) - response.raise_for_status() - return response + # Create dummy object so the function does not have to check + # 'progress' variable everywhere + if progress is None: + progress = TransferProgress() - def delete_event(self, event_id: str): - """Delete event by id. + progress.set_destination_url(filepath) - Supported since AYON server 1.6.0. + dst_directory = os.path.dirname(filepath) + os.makedirs(dst_directory, exist_ok=True) - Args: - event_id (str): Event id. + try: + with open(filepath, "wb") as stream: + self.download_file_to_stream( + endpoint, stream, chunk_size, progress + ) - Returns: - RestApiResponse: Response from server. + except Exception as exc: + progress.set_failed(str(exc)) + raise - """ - response = self.delete(f"events/{event_id}") - response.raise_for_status() - return response + return progress - def enroll_event_job( - self, - source_topic: "Union[str, List[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, - ): - """Enroll job based on events. - - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. - - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. - - Make sure the new event has updated status to '"finished"' status - when you're done with logic - - Target topic should not clash with other processes/services. - - Created target event have 'dependsOn' key where is id of source topic. - - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' + @staticmethod + def _upload_chunks_iter( + file_stream: "StreamType", + progress: TransferProgress, + chunk_size: int, + ) -> Generator[bytes, None, None]: + """Generator that yields chunks of file. Args: - source_topic (Union[str, List[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[List[str]]): Ignore events triggered - by given sender types. + file_stream (StreamType): Byte stream. + progress (TransferProgress): Object to track upload progress. + chunk_size (int): Size of chunks that are uploaded at once. - Returns: - Union[None, dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. + Yields: + bytes: Chunk of file. """ - kwargs = { - "sourceTopic": source_topic, - "targetTopic": target_topic, - "sender": sender, - } - major, minor, patch, _, _ = self.server_version_tuple - if max_retries is not None: - kwargs["maxRetries"] = max_retries - if sequential is not None: - kwargs["sequential"] = sequential - if description is not None: - kwargs["description"] = description - if events_filter is not None: - kwargs["filter"] = events_filter - if ( - ignore_older_than is not None - and (major, minor, patch) > (1, 5, 1) - ): - kwargs["ignoreOlderThan"] = ignore_older_than - if ignore_sender_types is not None: - if (major, minor, patch) <= (1, 5, 4): - raise ValueError( - "Ignore sender types are not supported for" - f" your version of server {self.server_version}." - ) - kwargs["ignoreSenderTypes"] = list(ignore_sender_types) + # Get size of file + file_stream.seek(0, io.SEEK_END) + size = file_stream.tell() + file_stream.seek(0) + # Set content size to progress object + progress.set_content_size(size) - response = self.post("enroll", **kwargs) - if response.status_code == 204: - return None - - if response.status_code == 503: - # Server is busy - self.log.info("Server is busy. Can't enroll event now.") - return None - - if response.status_code >= 400: - self.log.error(response.text) - return None - - return response.data - - def get_activities( - self, - project_name: str, - activity_ids: Optional[Iterable[str]] = None, - activity_types: Optional[Iterable["ActivityType"]] = None, - entity_ids: Optional[Iterable[str]] = None, - entity_names: Optional[Iterable[str]] = None, - entity_type: Optional[str] = None, - changed_after: Optional[str] = None, - changed_before: Optional[str] = None, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - ) -> Generator[Dict[str, Any], None, None]: - """Get activities from server with filtering options. - - Args: - project_name (str): Project on which activities happened. - activity_ids (Optional[Iterable[str]]): Activity ids. - activity_types (Optional[Iterable[ActivityType]]): Activity types. - entity_ids (Optional[Iterable[str]]): Entity ids. - entity_names (Optional[Iterable[str]]): Entity names. - entity_type (Optional[str]): Entity type. - changed_after (Optional[str]): Return only activities changed - after given iso datetime string. - changed_before (Optional[str]): Return only activities changed - before given iso datetime string. - reference_types (Optional[Iterable[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. - - Returns: - Generator[dict[str, Any]]: Available activities matching filters. - - """ - if not project_name: - return - filters = { - "projectName": project_name, - } - if reference_types is None: - reference_types = {"origin"} - - if not _prepare_list_filters( - filters, - ("activityIds", activity_ids), - ("activityTypes", activity_types), - ("entityIds", entity_ids), - ("entityNames", entity_names), - ("referenceTypes", reference_types), - ): - return - - for filter_key, filter_value in ( - ("entityType", entity_type), - ("changedAfter", changed_after), - ("changedBefore", changed_before), - ): - if filter_value is not None: - filters[filter_key] = filter_value - - if not fields: - fields = self.get_default_fields_for_type("activity") - - query = activities_graphql_query(set(fields), order) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - if limit: - activities_field = query.get_field_by_path("activities") - activities_field.set_limit(limit) - - for parsed_data in query.continuous_query(self): - for activity in parsed_data["project"]["activities"]: - activity_data = activity.get("activityData") - if isinstance(activity_data, str): - activity["activityData"] = json.loads(activity_data) - yield activity - - def get_activity_by_id( - self, - project_name: str, - activity_id: str, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: - """Get activity by id. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - - Returns: - Optional[Dict[str, Any]]: Activity data or None if activity is not - found. - - """ - for activity in self.get_activities( - project_name=project_name, - activity_ids={activity_id}, - reference_types=reference_types, - fields=fields, - ): - return activity - return None + while True: + chunk = file_stream.read(chunk_size) + if not chunk: + break + progress.add_transferred_chunk(len(chunk)) + yield chunk - def create_activity( + def _upload_file( self, - project_name: str, - entity_id: str, - entity_type: str, - activity_type: "ActivityType", - activity_id: Optional[str] = None, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - timestamp: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> str: - """Create activity on a project. + url: str, + stream: "StreamType", + progress: TransferProgress, + request_type: Optional[RequestType] = None, + chunk_size: Optional[int] = None, + **kwargs + ) -> requests.Response: + """Upload file to server. Args: - project_name (str): Project on which activity happened. - entity_id (str): Entity id. - entity_type (str): Entity type. - activity_type (ActivityType): Activity type. - activity_id (Optional[str]): Activity id. - body (Optional[str]): Activity body. - file_ids (Optional[List[str]]): List of file ids attached - to activity. - timestamp (Optional[str]): Activity timestamp. - data (Optional[Dict[str, Any]]): Additional data. + url (str): Url where file will be uploaded. + stream (StreamType): File stream. + progress (TransferProgress): Object that gives ability to track + progress. + request_type (Optional[RequestType]): Type of request that will + be used. Default is PUT. + chunk_size (Optional[int]): Size of chunks that are uploaded + at once. + **kwargs (Any): Additional arguments that will be passed + to request function. Returns: - str: Activity id. - - """ - post_data = { - "activityType": activity_type, - } - for key, value in ( - ("id", activity_id), - ("body", body), - ("files", file_ids), - ("timestamp", timestamp), - ("data", data), - ): - if value is not None: - post_data[key] = value - - response = self.post( - f"projects/{project_name}/{entity_type}/{entity_id}/activities", - **post_data - ) - response.raise_for_status() - return response.data["id"] - - def update_activity( - self, - project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[Dict[str, Any]] = None, - ): - """Update activity by id. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - body (str): Activity body. - file_ids (Optional[List[str]]): List of file ids attached - to activity. - append_file_ids (Optional[bool]): Append file ids to existing - list of file ids. - data (Optional[Dict[str, Any]]): Update data in activity. + requests.Response: Server response. """ - update_data = {} - major, minor, patch, _, _ = self.server_version_tuple - new_patch_model = (major, minor, patch) > (1, 5, 6) - if body is None and not new_patch_model: - raise ValueError( - "Update without 'body' is supported" - " after server version 1.5.6." - ) - - if body is not None: - update_data["body"] = body + if request_type is None: + request_type = RequestTypes.put - if file_ids is not None: - update_data["files"] = file_ids - if new_patch_model: - update_data["appendFiles"] = append_file_ids - elif append_file_ids: - raise ValueError( - "Append file ids is supported after server version 1.5.6." - ) + if self._session is None: + headers = kwargs.setdefault("headers", {}) + for key, value in self.get_headers().items(): + if key not in headers: + headers[key] = value + post_func = self._base_functions_mapping[request_type] + else: + post_func = self._session_functions_mapping[request_type] - if data is not None: - if not new_patch_model: - raise ValueError( - "Update of data is supported after server version 1.5.6." - ) - update_data["data"] = data + if not chunk_size: + chunk_size = self.default_upload_chunk_size - response = self.patch( - f"projects/{project_name}/activities/{activity_id}", - **update_data + response = post_func( + url, + data=self._upload_chunks_iter(stream, progress, chunk_size), + **kwargs ) - response.raise_for_status() - - def delete_activity(self, project_name: str, activity_id: str): - """Delete activity by id. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id to remove. - """ - response = self.delete( - f"projects/{project_name}/activities/{activity_id}" - ) response.raise_for_status() + return response - def _endpoint_to_url( - self, - endpoint: str, - use_rest: Optional[bool] = True - ) -> str: - """Cleanup endpoint and return full url to AYON server. - - If endpoint already starts with server url only slashes are removed. - - Args: - endpoint (str): Endpoint to be cleaned. - use_rest (Optional[bool]): Use only base server url if set to - False, otherwise REST endpoint is used. - - Returns: - str: Full url to AYON server. - - """ - endpoint = endpoint.lstrip("/").rstrip("/") - if endpoint.startswith(self._base_url): - return endpoint - base_url = self._rest_url if use_rest else self._graphql_url - return f"{base_url}/{endpoint}" - - def _download_file_to_stream( - self, url: str, stream, chunk_size, progress - ): - kwargs = {"stream": True} - if self._session is None: - kwargs["headers"] = self.get_headers() - get_func = self._base_functions_mapping[RequestTypes.get] - else: - get_func = self._session_functions_mapping[RequestTypes.get] - - with get_func(url, **kwargs) as response: - response.raise_for_status() - progress.set_content_size(response.headers["Content-length"]) - for chunk in response.iter_content(chunk_size=chunk_size): - stream.write(chunk) - progress.add_transferred_chunk(len(chunk)) - - def download_file_to_stream( + def upload_file_from_stream( self, endpoint: str, stream: "StreamType", - chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, - ) -> TransferProgress: - """Download file from AYON server to IOStream. - - Endpoint can be full url (must start with 'base_url' of api object). - - Progress object can be used to track download. Can be used when - download happens in thread and other thread want to catch changes over - time. + request_type: Optional[RequestType] = None, + **kwargs + ) -> requests.Response: + """Upload file to server from bytes. Todos: Use retries and timeout. Return RestApiResponse. Args: - endpoint (str): Endpoint or URL to file that should be downloaded. - stream (StreamType): Stream where output will - be stored. - chunk_size (Optional[int]): Size of chunks that are received - in single loop. + endpoint (str): Endpoint or url where file will be uploaded. + stream (StreamType): File content stream. progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + to track upload progress. + request_type (Optional[RequestType]): Type of request that will + be used to upload file. + **kwargs (Any): Additional arguments that will be passed + to request function. - """ - if not chunk_size: - chunk_size = self.default_download_chunk_size + Returns: + requests.Response: Response object + """ url = self._endpoint_to_url(endpoint) + # Create dummy object so the function does not have to check + # 'progress' variable everywhere if progress is None: progress = TransferProgress() - progress.set_source_url(url) + progress.set_destination_url(url) progress.set_started() try: - self._download_file_to_stream( - url, stream, chunk_size, progress + return self._upload_file( + url, stream, progress, request_type, **kwargs ) except Exception as exc: @@ -2228,199 +1591,16 @@ def download_file_to_stream( finally: progress.set_transfer_done() - return progress - def download_file( + def upload_file( self, endpoint: str, filepath: str, - chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, - ) -> TransferProgress: - """Download file from AYON server. - - Endpoint can be full url (must start with 'base_url' of api object). - - Progress object can be used to track download. Can be used when - download happens in thread and other thread want to catch changes over - time. - - Todos: - Use retries and timeout. - Return RestApiResponse. - - Args: - endpoint (str): Endpoint or URL to file that should be downloaded. - filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received - in single loop. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - """ - # Create dummy object so the function does not have to check - # 'progress' variable everywhere - if progress is None: - progress = TransferProgress() - - progress.set_destination_url(filepath) - - dst_directory = os.path.dirname(filepath) - os.makedirs(dst_directory, exist_ok=True) - - try: - with open(filepath, "wb") as stream: - self.download_file_to_stream( - endpoint, stream, chunk_size, progress - ) - - except Exception as exc: - progress.set_failed(str(exc)) - raise - - return progress - - @staticmethod - def _upload_chunks_iter( - file_stream: "StreamType", - progress: TransferProgress, - chunk_size: int, - ) -> Generator[bytes, None, None]: - """Generator that yields chunks of file. - - Args: - file_stream (StreamType): Byte stream. - progress (TransferProgress): Object to track upload progress. - chunk_size (int): Size of chunks that are uploaded at once. - - Yields: - bytes: Chunk of file. - - """ - # Get size of file - file_stream.seek(0, io.SEEK_END) - size = file_stream.tell() - file_stream.seek(0) - # Set content size to progress object - progress.set_content_size(size) - - while True: - chunk = file_stream.read(chunk_size) - if not chunk: - break - progress.add_transferred_chunk(len(chunk)) - yield chunk - - def _upload_file( - self, - url: str, - stream: "StreamType", - progress: TransferProgress, - request_type: Optional[RequestType] = None, - chunk_size: Optional[int] = None, - **kwargs - ) -> requests.Response: - """Upload file to server. - - Args: - url (str): Url where file will be uploaded. - stream (StreamType): File stream. - progress (TransferProgress): Object that gives ability to track - progress. - request_type (Optional[RequestType]): Type of request that will - be used. Default is PUT. - chunk_size (Optional[int]): Size of chunks that are uploaded - at once. - **kwargs (Any): Additional arguments that will be passed - to request function. - - Returns: - requests.Response: Server response. - - """ - if request_type is None: - request_type = RequestTypes.put - - if self._session is None: - headers = kwargs.setdefault("headers", {}) - for key, value in self.get_headers().items(): - if key not in headers: - headers[key] = value - post_func = self._base_functions_mapping[request_type] - else: - post_func = self._session_functions_mapping[request_type] - - if not chunk_size: - chunk_size = self.default_upload_chunk_size - - response = post_func( - url, - data=self._upload_chunks_iter(stream, progress, chunk_size), - **kwargs - ) - - response.raise_for_status() - return response - - def upload_file_from_stream( - self, - endpoint: str, - stream: "StreamType", - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, - **kwargs - ) -> requests.Response: - """Upload file to server from bytes. - - Todos: - Use retries and timeout. - Return RestApiResponse. - - Args: - endpoint (str): Endpoint or url where file will be uploaded. - stream (StreamType): File content stream. - progress (Optional[TransferProgress]): Object that gives ability - to track upload progress. - request_type (Optional[RequestType]): Type of request that will - be used to upload file. - **kwargs (Any): Additional arguments that will be passed - to request function. - - Returns: - requests.Response: Response object - - """ - url = self._endpoint_to_url(endpoint) - - # Create dummy object so the function does not have to check - # 'progress' variable everywhere - if progress is None: - progress = TransferProgress() - - progress.set_destination_url(url) - progress.set_started() - - try: - return self._upload_file( - url, stream, progress, request_type, **kwargs - ) - - except Exception as exc: - progress.set_failed(str(exc)) - raise - - finally: - progress.set_transfer_done() - - def upload_file( - self, - endpoint: str, - filepath: str, - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, - **kwargs - ) -> requests.Response: - """Upload file to server. + request_type: Optional[RequestType] = None, + **kwargs + ) -> requests.Response: + """Upload file to server. Todos: Use retries and timeout. @@ -2429,6464 +1609,270 @@ def upload_file( Args: endpoint (str): Endpoint or url where file will be uploaded. filepath (str): Source filepath. - progress (Optional[TransferProgress]): Object that gives ability - to track upload progress. - request_type (Optional[RequestType]): Type of request that will - be used to upload file. - **kwargs (Any): Additional arguments that will be passed - to request function. - - Returns: - requests.Response: Response object - - """ - if progress is None: - progress = TransferProgress() - - progress.set_source_url(filepath) - - with open(filepath, "rb") as stream: - return self.upload_file_from_stream( - endpoint, stream, progress, request_type, **kwargs - ) - - def upload_reviewable( - self, - project_name: str, - version_id: str, - filepath: str, - label: Optional[str] = None, - content_type: Optional[str] = None, - filename: Optional[str] = None, - progress: Optional[TransferProgress] = None, - headers: Optional[Dict[str, Any]] = None, - **kwargs - ) -> requests.Response: - """Upload reviewable file to server. - - Args: - project_name (str): Project name. - version_id (str): Version id. - filepath (str): Reviewable file path to upload. - label (Optional[str]): Reviewable label. Filled automatically - server side with filename. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): User as original filename. Filename from - 'filepath' is used when not filled. - progress (Optional[TransferProgress]): Progress. - headers (Optional[Dict[str, Any]]): Headers. - - Returns: - requests.Response: Server response. - - """ - if not content_type: - content_type = get_media_mime_type(filepath) - - if not content_type: - raise ValueError( - f"Could not determine MIME type of file '{filepath}'" - ) - - if headers is None: - headers = self.get_headers(content_type) - else: - # Make sure content-type is filled with file content type - content_type_key = next( - ( - key - for key in headers - if key.lower() == "content-type" - ), - "Content-Type" - ) - headers[content_type_key] = content_type - - # Fill original filename if not explicitly defined - if not filename: - filename = os.path.basename(filepath) - headers["x-file-name"] = filename - - query = prepare_query_string({"label": label or None}) - endpoint = ( - f"/projects/{project_name}" - f"/versions/{version_id}/reviewables{query}" - ) - return self.upload_file( - endpoint, - filepath, - progress=progress, - headers=headers, - request_type=RequestTypes.post, - **kwargs - ) - - def trigger_server_restart(self): - """Trigger server restart. - - Restart may be required when a change of specific value happened on - server. - - """ - result = self.post("system/restart") - if result.status_code != 204: - # TODO add better exception - raise ValueError("Failed to restart server") - - def query_graphql( - self, - query: str, - variables: Optional[Dict[str, Any]] = None, - ) -> GraphQlResponse: - """Execute GraphQl query. - - Args: - query (str): GraphQl query string. - variables (Optional[dict[str, Any]): Variables that can be - used in query. - - Returns: - GraphQlResponse: Response from server. - - """ - data = {"query": query, "variables": variables or {}} - response = self._do_rest_request( - RequestTypes.post, - self._graphql_url, - json=data - ) - response.raise_for_status() - return GraphQlResponse(response) - - def get_graphql_schema(self) -> Dict[str, Any]: - return self.query_graphql(INTROSPECTION_QUERY).data["data"] - - def get_server_schema(self) -> Optional[Dict[str, Any]]: - """Get server schema with info, url paths, components etc. - - Todos: - Cache schema - How to find out it is outdated? - - Returns: - dict[str, Any]: Full server schema. - - """ - url = f"{self._base_url}/openapi.json" - response = self._do_rest_request(RequestTypes.get, url) - if response: - return response.data - return None - - def get_schemas(self) -> Dict[str, Any]: - """Get components schema. - - Name of components does not match entity type names e.g. 'project' is - under 'ProjectModel'. We should find out some mapping. Also, there - are properties which don't have information about reference to object - e.g. 'config' has just object definition without reference schema. - - Returns: - dict[str, Any]: Component schemas. - - """ - server_schema = self.get_server_schema() - return server_schema["components"]["schemas"] - - def get_attributes_schema( - self, use_cache: bool = True - ) -> "AttributesSchemaDict": - if not use_cache: - self.reset_attributes_schema() - - if self._attributes_schema is None: - result = self.get("attributes") - result.raise_for_status() - self._attributes_schema = result.data - return copy.deepcopy(self._attributes_schema) - - def reset_attributes_schema(self): - self._attributes_schema = None - self._entity_type_attributes_cache = {} - - def set_attribute_config( - self, - attribute_name: str, - data: "AttributeSchemaDataDict", - scope: List["AttributeScope"], - position: Optional[int] = None, - builtin: bool = False, - ): - if position is None: - attributes = self.get("attributes").data["attributes"] - origin_attr = next( - ( - attr for attr in attributes - if attr["name"] == attribute_name - ), - None - ) - if origin_attr: - position = origin_attr["position"] - else: - position = len(attributes) - - response = self.put( - f"attributes/{attribute_name}", - data=data, - scope=scope, - position=position, - builtin=builtin - ) - if response.status_code != 204: - # TODO raise different exception - raise ValueError( - f"Attribute \"{attribute_name}\" was not created/updated." - f" {response.detail}" - ) - - self.reset_attributes_schema() - - def remove_attribute_config(self, attribute_name: str): - """Remove attribute from server. - - This can't be un-done, please use carefully. - - Args: - attribute_name (str): Name of attribute to remove. - - """ - response = self.delete(f"attributes/{attribute_name}") - response.raise_for_status( - f"Attribute \"{attribute_name}\" was not created/updated." - f" {response.detail}" - ) - - self.reset_attributes_schema() - - def get_attributes_for_type( - self, entity_type: "AttributeScope" - ) -> Dict[str, "AttributeSchemaDict"]: - """Get attribute schemas available for an entity type. - - Example:: - - ``` - # Example attribute schema - { - # Common - "type": "integer", - "title": "Clip Out", - "description": null, - "example": 1, - "default": 1, - # These can be filled based on value of 'type' - "gt": null, - "ge": null, - "lt": null, - "le": null, - "minLength": null, - "maxLength": null, - "minItems": null, - "maxItems": null, - "regex": null, - "enum": null - } - ``` - - Args: - entity_type (str): Entity type for which should be attributes - received. - - Returns: - dict[str, dict[str, Any]]: Attribute schemas that are available - for entered entity type. - - """ - attributes = self._entity_type_attributes_cache.get(entity_type) - if attributes is None: - attributes_schema = self.get_attributes_schema() - attributes = {} - for attr in attributes_schema["attributes"]: - if entity_type not in attr["scope"]: - continue - attr_name = attr["name"] - attributes[attr_name] = attr["data"] - - self._entity_type_attributes_cache[entity_type] = attributes - - return copy.deepcopy(attributes) - - def get_attributes_fields_for_type( - self, entity_type: "AttributeScope" - ) -> Set[str]: - """Prepare attribute fields for entity type. - - Returns: - set[str]: Attributes fields for entity type. - - """ - attributes = self.get_attributes_for_type(entity_type) - return { - f"attrib.{attr}" - for attr in attributes - } - - def get_default_fields_for_type(self, entity_type: str) -> Set[str]: - """Default fields for entity type. - - Returns most of commonly used fields from server. - - Args: - entity_type (str): Name of entity type. - - Returns: - set[str]: Fields that should be queried from server. - - """ - # Event does not have attributes - if entity_type == "event": - return set(DEFAULT_EVENT_FIELDS) - - if entity_type == "activity": - return set(DEFAULT_ACTIVITY_FIELDS) - - if entity_type == "project": - entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - if (maj_v, min_v, patch_v) > (1, 10, 0): - entity_type_defaults.add("productTypes") - - elif entity_type == "folder": - entity_type_defaults = set(DEFAULT_FOLDER_FIELDS) - - elif entity_type == "task": - entity_type_defaults = set(DEFAULT_TASK_FIELDS) - - elif entity_type == "product": - entity_type_defaults = set(DEFAULT_PRODUCT_FIELDS) - - elif entity_type == "version": - entity_type_defaults = set(DEFAULT_VERSION_FIELDS) - - elif entity_type == "representation": - entity_type_defaults = ( - DEFAULT_REPRESENTATION_FIELDS - | REPRESENTATION_FILES_FIELDS - ) - - if not self.graphql_allows_traits_in_representations: - entity_type_defaults.discard("traits") - - elif entity_type == "productType": - entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) - - elif entity_type == "workfile": - entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS) - - elif entity_type == "user": - entity_type_defaults = set(DEFAULT_USER_FIELDS) - - elif entity_type == "entityList": - entity_type_defaults = set(DEFAULT_ENTITY_LIST_FIELDS) - - else: - raise ValueError(f"Unknown entity type \"{entity_type}\"") - return ( - entity_type_defaults - | self.get_attributes_fields_for_type(entity_type) - ) - - def get_addons_info(self, details: bool = True) -> "AddonsInfoDict": - """Get information about addons available on server. - - Args: - details (Optional[bool]): Detailed data with information how - to get client code. - - """ - endpoint = "addons" - if details: - endpoint += "?details=1" - response = self.get(endpoint) - response.raise_for_status() - return response.data - - def get_addon_endpoint( - self, - addon_name: str, - addon_version: str, - *subpaths: str, - ) -> str: - """Calculate endpoint to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - - Returns: - str: Final url. - - """ - ending = "" - if subpaths: - ending = "/{}".format("/".join(subpaths)) - return f"addons/{addon_name}/{addon_version}{ending}" - - def get_addon_url( - self, - addon_name: str, - addon_version: str, - *subpaths: str, - use_rest: bool = True, - ) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. - - Returns: - str: Final url. - - """ - endpoint = self.get_addon_endpoint( - addon_name, addon_version, *subpaths - ) - url_base = self._base_url if use_rest else self._rest_url - return f"{url_base}/{endpoint}" - - def download_addon_private_file( - self, - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - if not destination_filename: - destination_filename = filename - dst_filepath = os.path.join(destination_dir, destination_filename) - # Filename can contain "subfolders" - dst_dirpath = os.path.dirname(dst_filepath) - os.makedirs(dst_dirpath, exist_ok=True) - - endpoint = self.get_addon_endpoint( - addon_name, - addon_version, - "private", - filename - ) - url = f"{self._base_url}/{endpoint}" - self.download_file( - url, dst_filepath, chunk_size=chunk_size, progress=progress - ) - return dst_filepath - - def get_installers( - self, - version: Optional[str] = None, - platform_name: Optional[str] = None, - ) -> "InstallersInfoDict": - """Information about desktop application installers on server. - - Desktop application installers are helpers to download/update AYON - desktop application for artists. - - Args: - version (Optional[str]): Filter installers by version. - platform_name (Optional[str]): Filter installers by platform name. - - Returns: - InstallersInfoDict: Information about installers known for server. - - """ - query = prepare_query_string({ - "version": version or None, - "platform": platform_name or None, - }) - response = self.get(f"desktop/installers{query}") - response.raise_for_status() - return response.data - - def create_installer( - self, - filename: str, - version: str, - python_version: str, - platform_name: str, - python_modules: Dict[str, str], - runtime_python_modules: Dict[str, str], - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, - ): - """Create new installer information on server. - - This step will create only metadata. Make sure to upload installer - to the server using 'upload_installer' method. - - Runtime python modules are modules that are required to run AYON - desktop application, but are not added to PYTHONPATH for any - subprocess. - - Args: - filename (str): Installer filename. - version (str): Version of installer. - python_version (str): Version of Python. - platform_name (str): Name of platform. - python_modules (dict[str, str]): Python modules that are available - in installer. - runtime_python_modules (dict[str, str]): Runtime python modules - that are available in installer. - checksum (str): Installer file checksum. - checksum_algorithm (str): Type of checksum used to create checksum. - file_size (int): File size. - sources (Optional[list[dict[str, Any]]]): List of sources that - can be used to download file. - - """ - body = { - "filename": filename, - "version": version, - "pythonVersion": python_version, - "platform": platform_name, - "pythonModules": python_modules, - "runtimePythonModules": runtime_python_modules, - "checksum": checksum, - "checksumAlgorithm": checksum_algorithm, - "size": file_size, - } - if sources: - body["sources"] = sources - - response = self.post("desktop/installers", **body) - response.raise_for_status() - - def update_installer(self, filename: str, sources: List[Dict[str, Any]]): - """Update installer information on server. - - Args: - filename (str): Installer filename. - sources (list[dict[str, Any]]): List of sources that - can be used to download file. Fully replaces existing sources. - - """ - response = self.patch( - f"desktop/installers/{filename}", - sources=sources - ) - response.raise_for_status() - - def delete_installer(self, filename: str): - """Delete installer from server. - - Args: - filename (str): Installer filename. - - """ - response = self.delete(f"desktop/installers/{filename}") - response.raise_for_status() - - def download_installer( - self, - filename: str, - dst_filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None - ): - """Download installer file from server. - - Args: - filename (str): Installer filename. - dst_filepath (str): Destination filepath. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - """ - self.download_file( - f"desktop/installers/{filename}", - dst_filepath, - chunk_size=chunk_size, - progress=progress - ) - - def upload_installer( - self, - src_filepath: str, - dst_filename: str, - progress: Optional[TransferProgress] = None, - ): - """Upload installer file to server. - - Args: - src_filepath (str): Source filepath. - dst_filename (str): Destination filename. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - requests.Response: Response object. - - """ - return self.upload_file( - f"desktop/installers/{dst_filename}", - src_filepath, - progress=progress - ) - - def _get_dependency_package_route( - self, filename: Optional[str] = None - ) -> str: - endpoint = "desktop/dependencyPackages" - if filename: - return f"{endpoint}/{filename}" - return endpoint - - def get_dependency_packages(self) -> "DependencyPackagesDict": - """Information about dependency packages on server. - - To download dependency package, use 'download_dependency_package' - method and pass in 'filename'. - - Example data structure:: - - { - "packages": [ - { - "filename": str, - "platform": str, - "checksum": str, - "checksumAlgorithm": str, - "size": int, - "sources": list[dict[str, Any]], - "supportedAddons": dict[str, str], - "pythonModules": dict[str, str] - } - ] - } - - Returns: - DependencyPackagesDict: Information about dependency packages - known for server. - - """ - endpoint = self._get_dependency_package_route() - result = self.get(endpoint) - result.raise_for_status() - return result.data - - def create_dependency_package( - self, - filename: str, - python_modules: Dict[str, str], - source_addons: Dict[str, str], - installer_version: str, - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, - platform_name: Optional[str] = None, - ): - """Create dependency package on server. - - The package will be created on a server, it is also required to upload - the package archive file (using :meth:`upload_dependency_package`). - - Args: - filename (str): Filename of dependency package. - python_modules (dict[str, str]): Python modules in dependency - package:: - - {"": "", ...} - - source_addons (dict[str, str]): Name of addons for which is - dependency package created:: - - {"": "", ...} - - installer_version (str): Version of installer for which was - package created. - checksum (str): Checksum of archive file where dependencies are. - checksum_algorithm (str): Algorithm used to calculate checksum. - file_size (Optional[int]): Size of file. - sources (Optional[list[dict[str, Any]]]): Information about - sources from where it is possible to get file. - platform_name (Optional[str]): Name of platform for which is - dependency package targeted. Default value is - current platform. - - """ - post_body = { - "filename": filename, - "pythonModules": python_modules, - "sourceAddons": source_addons, - "installerVersion": installer_version, - "checksum": checksum, - "checksumAlgorithm": checksum_algorithm, - "size": file_size, - "platform": platform_name or platform.system().lower(), - } - if sources: - post_body["sources"] = sources - - route = self._get_dependency_package_route() - response = self.post(route, **post_body) - response.raise_for_status() - - def update_dependency_package( - self, filename: str, sources: List[Dict[str, Any]] - ): - """Update dependency package metadata on server. - - Args: - filename (str): Filename of dependency package. - sources (list[dict[str, Any]]): Information about - sources from where it is possible to get file. Fully replaces - existing sources. - - """ - response = self.patch( - self._get_dependency_package_route(filename), - sources=sources - ) - response.raise_for_status() - - def delete_dependency_package( - self, filename: str, platform_name: Optional[str] = None - ): - """Remove dependency package for specific platform. - - Args: - filename (str): Filename of dependency package. - platform_name (Optional[str]): Deprecated. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'delete_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - - route = self._get_dependency_package_route(filename) - response = self.delete(route) - response.raise_for_status("Failed to delete dependency file") - return response.data - - def download_dependency_package( - self, - src_filename: str, - dst_directory: str, - dst_filename: str, - platform_name: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> str: - """Download dependency package from server. - - This method requires to have authorized token available. The package - is only downloaded. - - Args: - src_filename (str): Filename of dependency pacakge. - For server version 0.2.0 and lower it is name of package - to download. - dst_directory (str): Where the file should be downloaded. - dst_filename (str): Name of destination filename. - platform_name (Optional[str]): Deprecated. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'download_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - route = self._get_dependency_package_route(src_filename) - package_filepath = os.path.join(dst_directory, dst_filename) - self.download_file( - route, - package_filepath, - chunk_size=chunk_size, - progress=progress - ) - return package_filepath - - def upload_dependency_package( - self, - src_filepath: str, - dst_filename: str, - platform_name: Optional[str] = None, - progress: Optional[TransferProgress] = None, - ): - """Upload dependency package to server. - - Args: - src_filepath (str): Path to a package file. - dst_filename (str): Dependency package filename or name of package - for server version 0.2.0 or lower. Must be unique. - platform_name (Optional[str]): Deprecated. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'upload_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - - route = self._get_dependency_package_route(dst_filename) - self.upload_file(route, src_filepath, progress=progress) - - def delete_addon(self, addon_name: str, purge: Optional[bool] = None): - """Delete addon from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. - - """ - if purge is not None: - purge = "true" if purge else "false" - query = prepare_query_string({"purge": purge}) - - response = self.delete(f"addons/{addon_name}{query}") - response.raise_for_status() - - def delete_addon_version( - self, - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, - ): - """Delete addon version from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. - - """ - if purge is not None: - purge = "true" if purge else "false" - query = prepare_query_string({"purge": purge}) - response = self.delete(f"addons/{addon_name}/{addon_version}{query}") - response.raise_for_status() - - def upload_addon_zip( - self, - src_filepath: str, - progress: Optional[TransferProgress] = None, - ): - """Upload addon zip file to server. - - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). - - Example output:: - - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} - - Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - Returns: - dict[str, Any]: Response data from server. - - """ - response = self.upload_file( - "addons/install", - src_filepath, - progress=progress, - request_type=RequestTypes.post, - ) - return response.json() - - def get_bundles(self) -> "BundlesInfoDict": - """Server bundles with basic information. - - This is example output:: - - { - "bundles": [ - { - "name": "my_bundle", - "createdAt": "2023-06-12T15:37:02.420260", - "installerVersion": "1.0.0", - "addons": { - "core": "1.2.3" - }, - "dependencyPackages": { - "windows": "a_windows_package123.zip", - "linux": "a_linux_package123.zip", - "darwin": "a_mac_package123.zip" - }, - "isProduction": False, - "isStaging": False - } - ], - "productionBundle": "my_bundle", - "stagingBundle": "test_bundle" - } - - Returns: - dict[str, Any]: Server bundles with basic information. - - """ - response = self.get("bundles") - response.raise_for_status() - return response.data - - def create_bundle( - self, - name: str, - addon_versions: Dict[str, str], - installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, - ): - """Create bundle on server. - - Bundle cannot be changed once is created. Only isProduction, isStaging - and dependency packages can change after creation. In case dev bundle - is created, it is possible to change anything, but it is not possible - to mark bundle as dev and production or staging at the same time. - - Development addon config can define custom path to client code. It is - used only for dev bundles. - - Example of 'dev_addons_config':: - - ```json - { - "core": { - "enabled": true, - "path": "/path/to/ayon-core/client" - } - } - ``` - - Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - """ - body = { - "name": name, - "installerVersion": installer_version, - "addons": addon_versions, - } - - for key, value in ( - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ): - if value is not None: - body[key] = value - - response = self.post("bundles", **body) - response.raise_for_status() - - def update_bundle( - self, - bundle_name: str, - addon_versions: Optional[Dict[str, str]] = None, - installer_version: Optional[str] = None, - dependency_packages: Optional[Dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, - ): - """Update bundle on server. - - Dependency packages can be update only for single platform. Others - will be left untouched. Use 'None' value to unset dependency package - from bundle. - - Args: - bundle_name (str): Name of bundle. - addon_versions (Optional[dict[str, str]]): Addon versions, - possible only for dev bundles. - installer_version (Optional[str]): Installer version, possible - only for dev bundles. - dependency_packages (Optional[dict[str, str]]): Dependency pacakge - names that should be used with the bundle. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only for dev bundles. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only for dev bundles. - - """ - body = { - key: value - for key, value in ( - ("installerVersion", installer_version), - ("addons", addon_versions), - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ) - if value is not None - } - - response = self.patch( - f"bundles/{bundle_name}", - **body - ) - response.raise_for_status() - - def check_bundle_compatibility( - self, - name: str, - addon_versions: Dict[str, str], - installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, - ) -> Dict[str, Any]: - """Check bundle compatibility. - - Can be used as per-flight validation before creating bundle. - - Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - Returns: - Dict[str, Any]: Server response, with 'success' and 'issues'. - - """ - body = { - "name": name, - "installerVersion": installer_version, - "addons": addon_versions, - } - - for key, value in ( - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ): - if value is not None: - body[key] = value - - response = self.post("bundles/check", **body) - response.raise_for_status() - return response.data - - def delete_bundle(self, bundle_name: str): - """Delete bundle from server. - - Args: - bundle_name (str): Name of bundle to delete. - - """ - response = self.delete(f"bundles/{bundle_name}") - response.raise_for_status() - - # Anatomy presets - def get_project_anatomy_presets(self) -> List["AnatomyPresetDict"]: - """Anatomy presets available on server. - - Content has basic information about presets. Example output:: - - [ - { - "name": "netflix_VFX", - "primary": false, - "version": "1.0.0" - }, - { - ... - }, - ... - ] - - Returns: - list[dict[str, str]]: Anatomy presets available on server. - - """ - result = self.get("anatomy/presets") - result.raise_for_status() - return result.data.get("presets") or [] - - def get_default_anatomy_preset_name(self) -> str: - """Name of default anatomy preset. - - Primary preset is used as default preset. But when primary preset is - not set a built-in is used instead. Built-in preset is named '_'. - - Returns: - str: Name of preset that can be used by - 'get_project_anatomy_preset'. - - """ - for preset in self.get_project_anatomy_presets(): - if preset.get("primary"): - return preset["name"] - return "_" - - def get_project_anatomy_preset( - self, preset_name: Optional[str] = None - ) -> "AnatomyPresetDict": - """Anatomy preset values by name. - - Get anatomy preset values by preset name. Primary preset is returned - if preset name is set to 'None'. - - Args: - preset_name (Optional[str]): Preset name. - - Returns: - AnatomyPresetDict: Anatomy preset values. - - """ - if preset_name is None: - preset_name = "__primary__" - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - preset_name = self.get_default_anatomy_preset_name() - - result = self.get(f"anatomy/presets/{preset_name}") - result.raise_for_status() - return result.data - - def get_built_in_anatomy_preset(self) -> "AnatomyPresetDict": - """Get built-in anatomy preset. - - Returns: - AnatomyPresetDict: Built-in anatomy preset. - - """ - preset_name = "__builtin__" - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - preset_name = "_" - return self.get_project_anatomy_preset(preset_name) - - def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": - warnings.warn( - ( - "Used deprecated 'get_build_in_anatomy_preset' use" - " 'get_built_in_anatomy_preset' instead." - ), - DeprecationWarning - ) - return self.get_built_in_anatomy_preset() - - def get_project_root_overrides( - self, project_name: str - ) -> Dict[str, Dict[str, str]]: - """Root overrides per site name. - - Method is based on logged user and can't be received for any other - user on server. - - Output will contain only roots per site id used by logged user. - - Args: - project_name (str): Name of project. - - Returns: - dict[str, dict[str, str]]: Root values by root name by site id. - - """ - result = self.get(f"projects/{project_name}/roots") - result.raise_for_status() - return result.data - - def get_project_roots_by_site( - self, project_name: str - ) -> Dict[str, Dict[str, str]]: - """Root overrides per site name. - - Method is based on logged user and can't be received for any other - user on server. - - Output will contain only roots per site id used by logged user. - - Deprecated: - Use 'get_project_root_overrides' instead. Function - deprecated since 1.0.6 - - Args: - project_name (str): Name of project. - - Returns: - dict[str, dict[str, str]]: Root values by root name by site id. - - """ - warnings.warn( - ( - "Method 'get_project_roots_by_site' is deprecated." - " Please use 'get_project_root_overrides' instead." - ), - DeprecationWarning - ) - return self.get_project_root_overrides(project_name) - - def get_project_root_overrides_by_site_id( - self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. - - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name or None if - site does not have overrides. - - """ - if site_id is None: - site_id = self.site_id - - if site_id is None: - return {} - roots = self.get_project_root_overrides(project_name) - return roots.get(site_id, {}) - - def get_project_roots_for_site( - self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. - - Deprecated: - Use 'get_project_root_overrides_by_site_id' instead. Function - deprecated since 1.0.6 - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name, root name is not - available if it does not have overrides. - - """ - warnings.warn( - ( - "Method 'get_project_roots_for_site' is deprecated." - " Please use 'get_project_root_overrides_by_site_id' instead." - ), - DeprecationWarning - ) - return self.get_project_root_overrides_by_site_id(project_name) - - def _get_project_roots_values( - self, - project_name: str, - site_id: Optional[str] = None, - platform_name: Optional[str] = None, - ) -> Dict[str, str]: - """Root values for site or platform. - - Helper function that treats 'siteRoots' endpoint. The endpoint - requires to pass exactly one query value of site id - or platform name. - - When using platform name, it does return default project roots without - any site overrides. - - Output should contain all project roots with all filled values. If - value does not have override on a site, it should be filled with - project default value. - - Args: - project_name (str): Project name. - site_id (Optional[str]): Site id for which want to receive - site overrides. - platform_name (Optional[str]): Platform for which want to receive - roots. - - Returns: - dict[str, str]: Root values. - - """ - query_data = {} - if site_id is not None: - query_data["site_id"] = site_id - else: - if platform_name is None: - platform_name = platform.system() - query_data["platform"] = platform_name.lower() - - query = prepare_query_string(query_data) - response = self.get( - f"projects/{project_name}/siteRoots{query}" - ) - response.raise_for_status() - return response.data - - def get_project_roots_by_site_id( - self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: - """Root values for a site. - - If site id is not passed a site set in current api object is used - instead. If site id is not available, default roots are returned - for current platform. - - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - root values. - - Returns: - dict[str, str]: Root values. - - """ - if site_id is None: - site_id = self.site_id - - return self._get_project_roots_values(project_name, site_id=site_id) - - def get_project_roots_by_platform( - self, project_name: str, platform_name: Optional[str] = None - ) -> Dict[str, str]: - """Root values for a site. - - If platform name is not passed current platform name is used instead. - - This function does return root values without site overrides. It is - possible to use the function to receive default root values. - - Args: - project_name (str): Name of project. - platform_name (Optional[Literal["windows", "linux", "darwin"]]): - Platform name for which want to receive root values. Current - platform name is used if not passed. - - Returns: - dict[str, str]: Root values. - - """ - return self._get_project_roots_values( - project_name, platform_name=platform_name - ) - - def get_addon_settings_schema( - self, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None - ) -> Dict[str, Any]: - """Sudio/Project settings schema of an addon. - - Project schema may look differently as some enums are based on project - values. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Schema for specific project or - default studio schemas. - - Returns: - dict[str, Any]: Schema of studio/project settings. - - """ - args = tuple() - if project_name: - args = (project_name, ) - - endpoint = self.get_addon_endpoint( - addon_name, addon_version, "schema", *args - ) - result = self.get(endpoint) - result.raise_for_status() - return result.data - - def get_addon_site_settings_schema( - self, addon_name: str, addon_version: str - ) -> Dict[str, Any]: - """Site settings schema of an addon. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - - Returns: - dict[str, Any]: Schema of site settings. - - """ - result = self.get( - f"addons/{addon_name}/{addon_version}/siteSettings/schema" - ) - result.raise_for_status() - return result.data - - def get_addon_studio_settings( - self, - addon_name: str, - addon_version: str, - variant: Optional[str] = None, - ) -> Dict[str, Any]: - """Addon studio settings. - - Receive studio settings for specific version of an addon. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - - Returns: - dict[str, Any]: Addon settings. - - """ - if variant is None: - variant = self.default_settings_variant - - query = prepare_query_string({"variant": variant or None}) - - result = self.get( - f"addons/{addon_name}/{addon_version}/settings{query}" - ) - result.raise_for_status() - return result.data - - def get_addon_project_settings( - self, - addon_name: str, - addon_version: str, - project_name: str, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True - ) -> Dict[str, Any]: - """Addon project settings. - - Receive project settings for specific version of an addon. The settings - may be with site overrides when enabled. - - Site id is filled with current connection site id if not passed. To - make sure any site id is used set 'use_site' to 'False'. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (str): Name of project for which the settings are - received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. - - Returns: - dict[str, Any]: Addon settings. - - """ - if not use_site: - site_id = None - elif not site_id: - site_id = self.site_id - - if variant is None: - variant = self.default_settings_variant - - query = prepare_query_string({ - "site": site_id or None, - "variant": variant or None, - }) - result = self.get( - f"addons/{addon_name}/{addon_version}" - f"/settings/{project_name}{query}" - ) - result.raise_for_status() - return result.data - - def get_addon_settings( - self, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True - ) -> Dict[str, Any]: - """Receive addon settings. - - Receive addon settings based on project name value. Some arguments may - be ignored if 'project_name' is set to 'None'. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Name of project for which the - settings are received. A studio settings values are received - if is 'None'. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using - site overrides set to 'False'. In that case won't be applied - any site overrides. - - Returns: - dict[str, Any]: Addon settings. - - """ - if project_name is None: - return self.get_addon_studio_settings( - addon_name, addon_version, variant - ) - return self.get_addon_project_settings( - addon_name, addon_version, project_name, variant, site_id, use_site - ) - - def get_addon_site_settings( - self, - addon_name: str, - addon_version: str, - site_id: Optional[str] = None, - ) -> Dict[str, Any]: - """Site settings of an addon. - - If site id is not available an empty dictionary is returned. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - site_id (Optional[str]): Name of site for which should be settings - returned. using 'site_id' attribute if not passed. - - Returns: - dict[str, Any]: Site settings. - - """ - if site_id is None: - site_id = self.site_id - - if not site_id: - return {} - - query = prepare_query_string({"site": site_id}) - result = self.get( - f"addons/{addon_name}/{addon_version}/siteSettings{query}" - ) - result.raise_for_status() - return result.data - - def get_bundle_settings( - self, - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - ) -> Dict[str, Any]: - """Get complete set of settings for given data. - - If project is not passed then studio settings are returned. If variant - is not passed 'default_settings_variant' is used. If bundle name is - not passed then current production/staging bundle is used, based on - variant value. - - Output contains addon settings and site settings in single dictionary. - - Todos: - - test how it behaves if there is not any bundle. - - test how it behaves if there is not any production/staging - bundle. - - Example output:: - - { - "addons": [ - { - "name": "addon-name", - "version": "addon-version", - "settings": {...}, - "siteSettings": {...} - } - ] - } - - Returns: - dict[str, Any]: All settings for single bundle. - - """ - if not use_site: - site_id = None - elif not site_id: - site_id = self.site_id - - query = prepare_query_string({ - "project_name": project_name or None, - "bundle_name": bundle_name or None, - "variant": variant or self.default_settings_variant or None, - "site_id": site_id, - }) - response = self.get(f"settings{query}") - response.raise_for_status() - return response.data - - def get_addons_studio_settings( - self, - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> Dict[str, Any]: - """All addons settings in one bulk. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. - - Returns: - dict[str, Any]: Settings of all addons on server. - - """ - output = self.get_bundle_settings( - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site - ) - if only_values: - output = { - addon["name"]: addon["settings"] - for addon in output["addons"] - } - return output - - def get_addons_project_settings( - self, - project_name: str, - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> Dict[str, Any]: - """Project settings of all addons. - - Server returns information about used addon versions, so full output - looks like: - - ```json - { - "settings": {...}, - "addons": {...} - } - ``` - - The output can be limited to only values. To do so is 'only_values' - argument which is by default set to 'True'. In that case output - contains only value of 'settings' key. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - project_name (str): Name of project for which are settings - received. - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. - - Returns: - dict[str, Any]: Settings of all addons on server for passed - project. - - """ - if not project_name: - raise ValueError("Project name must be passed.") - - output = self.get_bundle_settings( - project_name=project_name, - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site - ) - if only_values: - output = { - addon["name"]: addon["settings"] - for addon in output["addons"] - } - return output - - def get_addons_settings( - self, - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> Dict[str, Any]: - """Universal function to receive all addon settings. - - Based on 'project_name' will receive studio settings or project - settings. In case project is not passed is 'site_id' ignored. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - project_name (Optional[str]): Name of project for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Id of site for which want to receive - site overrides. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. - only_values (Optional[bool]): Only settings values will be - returned. By default, is set to 'True'. - - """ - if project_name is None: - return self.get_addons_studio_settings( - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values - ) - - return self.get_addons_project_settings( - project_name=project_name, - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values - ) - - def get_secrets(self) -> List["SecretDict"]: - """Get all secrets. - - Example output:: - - [ - { - "name": "secret_1", - "value": "secret_value_1", - }, - { - "name": "secret_2", - "value": "secret_value_2", - } - ] - - Returns: - list[SecretDict]: List of secret entities. - - """ - response = self.get("secrets") - response.raise_for_status() - return response.data - - def get_secret(self, secret_name: str) -> "SecretDict": - """Get secret by name. - - Example output:: - - { - "name": "secret_name", - "value": "secret_value", - } - - Args: - secret_name (str): Name of secret. - - Returns: - dict[str, str]: Secret entity data. - - """ - response = self.get(f"secrets/{secret_name}") - response.raise_for_status() - return response.data - - def save_secret(self, secret_name: str, secret_value: str): - """Save secret. - - This endpoint can create and update secret. - - Args: - secret_name (str): Name of secret. - secret_value (str): Value of secret. - - """ - response = self.put( - f"secrets/{secret_name}", - name=secret_name, - value=secret_value, - ) - response.raise_for_status() - return response.data - - def delete_secret(self, secret_name: str): - """Delete secret by name. - - Args: - secret_name (str): Name of secret to delete. - - """ - response = self.delete(f"secrets/{secret_name}") - response.raise_for_status() - return response.data - - # Entity getters - def get_rest_project( - self, project_name: str - ) -> Optional["ProjectDict"]: - """Query project by name. - - This call returns project with anatomy data. - - Args: - project_name (str): Name of project. - - Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. - - """ - if not project_name: - return None - - response = self.get(f"projects/{project_name}") - # TODO ignore only error about not existing project - if response.status != 200: - return None - project = response.data - self._fill_project_entity_data(project) - return project - - def get_rest_projects( - self, - active: Optional[bool] = True, - library: Optional[bool] = None, - ) -> Generator["ProjectDict", None, None]: - """Query available project entities. - - User must be logged in. - - Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - Generator[ProjectDict, None, None]: Available projects. - - """ - for project_name in self.get_project_names(active, library): - project = self.get_rest_project(project_name) - if project: - yield project - - def get_rest_entity_by_id( - self, - project_name: str, - entity_type: str, - entity_id: str, - ) -> Optional["AnyEntityDict"]: - """Get entity using REST on a project by its id. - - Args: - project_name (str): Name of project where entity is. - entity_type (Literal["folder", "task", "product", "version"]): The - entity type which should be received. - entity_id (str): Id of entity. - - Returns: - Optional[AnyEntityDict]: Received entity data. - - """ - if not all((project_name, entity_type, entity_id)): - return None - - response = self.get( - f"projects/{project_name}/{entity_type}s/{entity_id}" - ) - if response.status == 200: - return response.data - return None - - def get_rest_folder( - self, project_name: str, folder_id: str - ) -> Optional["FolderDict"]: - return self.get_rest_entity_by_id( - project_name, "folder", folder_id - ) - - def get_rest_folders( - self, project_name: str, include_attrib: bool = False - ) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - raise UnsupportedServerVersion( - "Function 'get_folders_rest' is supported" - " for AYON server 1.0.8 and above." - ) - query = prepare_query_string({ - "attrib": "true" if include_attrib else "false" - }) - response = self.get( - f"projects/{project_name}/folders{query}" - ) - response.raise_for_status() - return response.data["folders"] - - def get_rest_task( - self, project_name: str, task_id: str - ) -> Optional["TaskDict"]: - return self.get_rest_entity_by_id(project_name, "task", task_id) - - def get_rest_product( - self, project_name: str, product_id: str - ) -> Optional["ProductDict"]: - return self.get_rest_entity_by_id(project_name, "product", product_id) - - def get_rest_version( - self, project_name: str, version_id: str - ) -> Optional["VersionDict"]: - return self.get_rest_entity_by_id(project_name, "version", version_id) - - def get_rest_representation( - self, project_name: str, representation_id: str - ) -> Optional["RepresentationDict"]: - return self.get_rest_entity_by_id( - project_name, "representation", representation_id - ) - - def get_project_names( - self, - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, - ) -> List[str]: - """Receive available project names. - - User must be logged in. - - Args: - active (Union[bool, None]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Union[bool, None]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - list[str]: List of available project names. - - """ - if active is not None: - active = "true" if active else "false" - - if library is not None: - library = "true" if library else "false" - - query = prepare_query_string({"active": active, "library": library}) - - response = self.get(f"projects{query}") - response.raise_for_status() - data = response.data - project_names = [] - if data: - for project in data["projects"]: - project_names.append(project["name"]) - return project_names - - def get_projects( - self, - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Generator["ProjectDict", None, None]: - """Get projects. - - Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[ProjectDict, None, None]: Queried projects. - - """ - if fields is not None: - fields = set(fields) - - graphql_fields, use_rest = self._get_project_graphql_fields(fields) - projects_by_name = {} - if graphql_fields: - projects = list(self._get_graphql_projects( - active, - library, - fields=graphql_fields, - own_attributes=own_attributes, - )) - if not use_rest: - yield from projects - return - projects_by_name = {p["name"]: p for p in projects} - - for project in self.get_rest_projects(active, library): - name = project["name"] - graphql_p = projects_by_name.get(name) - if graphql_p: - project["productTypes"] = graphql_p["productTypes"] - yield project - - def get_project( - self, - project_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["ProjectDict"]: - """Get project. - - Args: - project_name (str): Name of project. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. - - """ - if fields is not None: - fields = set(fields) - - graphql_fields, use_rest = self._get_project_graphql_fields(fields) - graphql_project = None - if graphql_fields: - graphql_project = next(self._get_graphql_projects( - None, - None, - fields=graphql_fields, - own_attributes=own_attributes, - ), None) - if not graphql_project or not use_rest: - return graphql_project - - project = self.get_rest_project(project_name) - if own_attributes: - fill_own_attribs(project) - if graphql_project: - project["productTypes"] = graphql_project["productTypes"] - return project - - def _get_project_graphql_fields( - self, fields: Optional[Set[str]] - ) -> Tuple[Set[str], bool]: - """Fetch of project must be done using REST endpoint. - - Returns: - set[str]: GraphQl fields. - - """ - if fields is None: - return set(), True - - has_product_types = False - graphql_fields = set() - for field in fields: - # Product types are available only in GraphQl - if field.startswith("productTypes"): - has_product_types = True - graphql_fields.add(field) - - if not has_product_types: - return set(), True - - inters = fields & {"name", "code", "active", "library"} - remainders = fields - (inters | graphql_fields) - if remainders: - graphql_fields.add("name") - return graphql_fields, True - graphql_fields |= inters - return graphql_fields, False - - def _fill_project_entity_data(self, project: Dict[str, Any]) -> None: - # Add fake scope to statuses if not available - if "statuses" in project: - for status in project["statuses"]: - scope = status.get("scope") - if scope is None: - status["scope"] = [ - "folder", - "task", - "product", - "version", - "representation", - "workfile" - ] - - # Convert 'data' from string to dict if needed - if "data" in project: - project_data = project["data"] - if isinstance(project_data, str): - project_data = json.loads(project_data) - project["data"] = project_data - - # Fill 'bundle' from data if is not filled - if "bundle" not in project: - bundle_data = project["data"].get("bundle", {}) - prod_bundle = bundle_data.get("production") - staging_bundle = bundle_data.get("staging") - project["bundle"] = { - "production": prod_bundle, - "staging": staging_bundle, - } - - # Convert 'config' from string to dict if needed - config = project.get("config") - if isinstance(config, str): - project["config"] = json.loads(config) - - # Unifiy 'linkTypes' data structure from REST and GraphQL - if "linkTypes" in project: - for link_type in project["linkTypes"]: - if "data" in link_type: - link_data = link_type.pop("data") - link_type.update(link_data) - if "style" not in link_type: - link_type["style"] = None - if "color" not in link_type: - link_type["color"] = None - - def _get_graphql_projects( - self, - active: Optional[bool], - library: Optional[bool], - fields: Set[str], - own_attributes: bool, - project_name: Optional[str] = None - ): - if active is not None: - fields.add("active") - - if library is not None: - fields.add("library") - - self._prepare_fields("project", fields, own_attributes) - - query = projects_graphql_query(fields) - if project_name is not None: - query.set_variable_value("projectName", project_name) - - for parsed_data in query.continuous_query(self): - for project in parsed_data["projects"]: - if active is not None and active is not project["active"]: - continue - if own_attributes: - fill_own_attribs(project) - self._fill_project_entity_data(project) - yield project - - def get_folders_hierarchy( - self, - project_name: str, - search_string: Optional[str] = None, - folder_types: Optional[Iterable[str]] = None - ) -> "ProjectHierarchyDict": - """Get project hierarchy. - - All folders in project in hierarchy data structure. - - Example output: - { - "hierarchy": [ - { - "id": "...", - "name": "...", - "label": "...", - "status": "...", - "folderType": "...", - "hasTasks": False, - "taskNames": [], - "parents": [], - "parentId": None, - "children": [...children folders...] - }, - ... - ] - } - - Args: - project_name (str): Project where to look for folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. - - Returns: - dict[str, Any]: Response data from server. - - """ - if folder_types: - folder_types = ",".join(folder_types) - - query = prepare_query_string({ - "search": search_string or None, - "types": folder_types or None, - }) - response = self.get( - f"projects/{project_name}/hierarchy{query}" - ) - response.raise_for_status() - return response.data - - def get_folders_rest( - self, project_name: str, include_attrib: bool = False - ) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Deprecated: - Use 'get_rest_folders' instead. Function was renamed to match - other rest functions, like 'get_rest_folder', - 'get_rest_project' etc. . - Will be removed in '1.0.7' or '1.1.0'. - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - warnings.warn( - ( - "DEPRECATION: Used deprecated 'get_folders_rest'," - " use 'get_rest_folders' instead." - ), - DeprecationWarning - ) - return self.get_rest_folders(project_name, include_attrib) - - def get_folders( - self, - project_name: str, - folder_ids: Optional[Iterable[str]] = None, - folder_paths: Optional[Iterable[str]] = None, - folder_names: Optional[Iterable[str]] = None, - folder_types: Optional[Iterable[str]] = None, - parent_ids: Optional[Iterable[str]] = None, - folder_path_regex: Optional[str] = None, - has_products: Optional[bool] = None, - has_tasks: Optional[bool] = None, - has_children: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Generator["FolderDict", None, None]: - """Query folders from server. - - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. - - Notes: - Filter 'active' don't have direct filter in GraphQl. - - Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Folder ids to filter. - folder_paths (Optional[Iterable[str]]): Folder paths used - for filtering. - folder_names (Optional[Iterable[str]]): Folder names used - for filtering. - folder_types (Optional[Iterable[str]]): Folder types used - for filtering. - parent_ids (Optional[Iterable[str]]): Ids of folder parents. - Use 'None' if folder is direct child of project. - folder_path_regex (Optional[str]): Folder path regex used - for filtering. - has_products (Optional[bool]): Filter folders with/without - products. Ignored when None, default behavior. - has_tasks (Optional[bool]): Filter folders with/without - tasks. Ignored when None, default behavior. - has_children (Optional[bool]): Filter folders with/without - children. Ignored when None, default behavior. - statuses (Optional[Iterable[str]]): Folder statuses used - for filtering. - assignees_all (Optional[Iterable[str]]): Filter by assigness - on children tasks. Task must have all of passed assignees. - tags (Optional[Iterable[str]]): Folder tags used - for filtering. - active (Optional[bool]): Filter active/inactive folders. - Both are returned if is set to None. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[FolderDict, None, None]: Queried folder entities. - - """ - if not project_name: - return - - filters = { - "projectName": project_name - } - if not _prepare_list_filters( - filters, - ("folderIds", folder_ids), - ("folderPaths", folder_paths), - ("folderNames", folder_names), - ("folderTypes", folder_types), - ("folderStatuses", statuses), - ("folderTags", tags), - ("folderAssigneesAll", assignees_all), - ): - return - - for filter_key, filter_value in ( - ("folderPathRegex", folder_path_regex), - ("folderHasProducts", has_products), - ("folderHasTasks", has_tasks), - ("folderHasLinks", has_links), - ("folderHasChildren", has_children), - ): - if filter_value is not None: - filters[filter_key] = filter_value - - if parent_ids is not None: - parent_ids = set(parent_ids) - if not parent_ids: - return - if None in parent_ids: - # Replace 'None' with '"root"' which is used during GraphQl - # query for parent ids filter for folders without folder - # parent - parent_ids.remove(None) - parent_ids.add("root") - - if project_name in parent_ids: - # Replace project name with '"root"' which is used during - # GraphQl query for parent ids filter for folders without - # folder parent - parent_ids.remove(project_name) - parent_ids.add("root") - - filters["parentFolderIds"] = list(parent_ids) - - if not fields: - fields = self.get_default_fields_for_type("folder") - else: - fields = set(fields) - self._prepare_fields("folder", fields) - - if active is not None: - fields.add("active") - - if own_attributes: - fields.add("ownAttrib") - - query = folders_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for folder in parsed_data["project"]["folders"]: - if active is not None and active is not folder["active"]: - continue - - self._convert_entity_data(folder) - - if own_attributes: - fill_own_attribs(folder) - yield folder - - def get_folder_by_id( - self, - project_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_ids=[folder_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_path( - self, - project_name: str, - folder_path: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by path. - - Folder path is a path to folder with all parent names joined by slash. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_path (str): Folder path. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_paths=[folder_path], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_name( - self, - project_name: str, - folder_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by path. - - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_names=[folder_name], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_ids_with_products( - self, project_name: str, folder_ids: Optional[Iterable[str]] = None - ) -> Set[str]: - """Find folders which have at least one product. - - Folders that have at least one product should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. - - Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. - - Returns: - set[str]: Folder ids that have at least one product. - - """ - if folder_ids is not None: - folder_ids = set(folder_ids) - if not folder_ids: - return set() - - query = folders_graphql_query({"id"}) - query.set_variable_value("projectName", project_name) - query.set_variable_value("folderHasProducts", True) - if folder_ids: - query.set_variable_value("folderIds", list(folder_ids)) - - parsed_data = query.query(self) - folders = parsed_data["project"]["folders"] - return { - folder["id"] - for folder in folders - } - - def create_folder( - self, - project_name: str, - name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, - ) -> str: - """Create new folder. - - Args: - project_name (str): Project name. - name (str): Folder name. - folder_type (Optional[str]): Folder type. - parent_id (Optional[str]): Parent folder id. Parent is project - if is ``None``. - label (Optional[str]): Label of folder. - attrib (Optional[dict[str, Any]]): Folder attributes. - data (Optional[dict[str, Any]]): Folder data. - tags (Optional[Iterable[str]]): Folder tags. - status (Optional[str]): Folder status. - active (Optional[bool]): Folder active state. - thumbnail_id (Optional[str]): Folder thumbnail id. - folder_id (Optional[str]): Folder id. If not passed new id is - generated. - - Returns: - str: Entity id. - - """ - if not folder_id: - folder_id = create_entity_id() - create_data = { - "id": folder_id, - "name": name, - } - for key, value in ( - ("folderType", folder_type), - ("parentId", parent_id), - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/folders", - **create_data - ) - response.raise_for_status() - return folder_id - - def update_folder( - self, - project_name: str, - folder_id: str, - name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, - label: Optional[str] = NOT_SET, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update folder entity on server. - - Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - folder_id (str): Folder id. - name (Optional[str]): New name. - folder_type (Optional[str]): New folder type. - parent_id (Optional[Union[str, None]]): New parent folder id. - label (Optional[Union[str, None]]): New label. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("folderType", folder_type), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("label", label), - ("parentId", parent_id), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/folders/{folder_id}", - **update_data - ) - response.raise_for_status() - - def delete_folder( - self, project_name: str, folder_id: str, force: bool = False - ): - """Delete folder. - - Args: - project_name (str): Project name. - folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. - - """ - url = f"projects/{project_name}/folders/{folder_id}" - if force: - url += "?force=true" - response = self.delete(url) - response.raise_for_status() - - def get_tasks( - self, - project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Generator["TaskDict", None, None]: - """Query task entities from server. - - Args: - project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[TaskDict, None, None]: Queried task entities. - - """ - if not project_name: - return - - filters = { - "projectName": project_name - } - if not _prepare_list_filters( - filters, - ("taskIds", task_ids), - ("taskNames", task_names), - ("taskTypes", task_types), - ("folderIds", folder_ids), - ("taskAssigneesAny", assignees), - ("taskAssigneesAll", assignees_all), - ("taskStatuses", statuses), - ("taskTags", tags), - ): - return - - if not fields: - fields = self.get_default_fields_for_type("task") - else: - fields = set(fields) - self._prepare_fields("task", fields, own_attributes) - - if active is not None: - fields.add("active") - - query = tasks_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for task in parsed_data["project"]["tasks"]: - if active is not None and active is not task["active"]: - continue - - self._convert_entity_data(task) - - if own_attributes: - fill_own_attribs(task) - yield task - - def get_task_by_name( - self, - project_name: str, - folder_id: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["TaskDict"]: - """Query task entity by name and folder id. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - task_name (str): Task name - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks( - project_name, - folder_ids=[folder_id], - task_names=[task_name], - active=None, - fields=fields, - own_attributes=own_attributes - ): - return task - return None - - def get_task_by_id( - self, - project_name: str, - task_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Optional["TaskDict"]: - """Query task entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - task_id (str): Task id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks( - project_name, - task_ids=[task_id], - active=None, - fields=fields, - own_attributes=own_attributes - ): - return task - return None - - def get_tasks_by_folder_paths( - self, - project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Dict[str, List["TaskDict"]]: - """Query task entities from server by folder paths. - - Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Dict[str, List[TaskDict]]: Task entities by - folder path. - - """ - folder_paths = set(folder_paths) - if not project_name or not folder_paths: - return {} - - filters = { - "projectName": project_name, - "folderPaths": list(folder_paths), - } - if not _prepare_list_filters( - filters, - ("taskNames", task_names), - ("taskTypes", task_types), - ("taskAssigneesAny", assignees), - ("taskAssigneesAll", assignees_all), - ("taskStatuses", statuses), - ("taskTags", tags), - ): - return {} - - if not fields: - fields = self.get_default_fields_for_type("task") - else: - fields = set(fields) - self._prepare_fields("task", fields, own_attributes) - - if active is not None: - fields.add("active") - - query = tasks_by_folder_paths_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - output = { - folder_path: [] - for folder_path in folder_paths - } - for parsed_data in query.continuous_query(self): - for folder in parsed_data["project"]["folders"]: - folder_path = folder["path"] - for task in folder["tasks"]: - if active is not None and active is not task["active"]: - continue - - self._convert_entity_data(task) - - if own_attributes: - fill_own_attribs(task) - output[folder_path].append(task) - return output - - def get_tasks_by_folder_path( - self, - project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> List["TaskDict"]: - """Query task entities from server by folder path. - - Args: - project_name (str): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - """ - return self.get_tasks_by_folder_paths( - project_name, - [folder_path], - task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes - )[folder_path] - - def get_task_by_folder_path( - self, - project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. - - Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks_by_folder_path( - project_name, - folder_path, - active=None, - task_names=[task_name], - fields=fields, - own_attributes=own_attributes, - ): - return task - return None - - def create_task( - self, - project_name: str, - name: str, - task_type: str, - folder_id: str, - label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, - ) -> str: - """Create new task. - - Args: - project_name (str): Project name. - name (str): Folder name. - task_type (str): Task type. - folder_id (str): Parent folder id. - label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is - generated. - - Returns: - str: Task id. - - """ - if not task_id: - task_id = create_entity_id() - create_data = { - "id": task_id, - "name": name, - "taskType": task_type, - "folderId": folder_id, - } - for key, value in ( - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("assignees", assignees), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/tasks", - **create_data - ) - response.raise_for_status() - return task_id - - def update_task( - self, - project_name: str, - task_id: str, - name: Optional[str] = None, - task_type: Optional[str] = None, - folder_id: Optional[str] = None, - label: Optional[str] = NOT_SET, - assignees: Optional[List[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update task entity on server. - - Do not pass ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - task_id (str): Task id. - name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. - assignees (Optional[str]): New assignees. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("taskType", task_type), - ("folderId", folder_id), - ("assignees", assignees), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("label", label), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/tasks/{task_id}", - **update_data - ) - response.raise_for_status() - - def delete_task(self, project_name: str, task_id: str): - """Delete task. - - Args: - project_name (str): Project name. - task_id (str): Task id to delete. - - """ - response = self.delete( - f"projects/{project_name}/tasks/{task_id}" - ) - response.raise_for_status() - - def _filter_product( - self, - project_name: str, - product: "ProductDict", - active: "Union[bool, None]", - ) -> Optional["ProductDict"]: - if active is not None and product["active"] is not active: - return None - - self._convert_entity_data(product) - - return product - - def get_products( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]]=None, - folder_ids: Optional[Iterable[str]]=None, - product_types: Optional[Iterable[str]]=None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Generator["ProductDict", None, None]: - """Query products from server. - - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. - - Args: - project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for - filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for - filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used - for filtering. - tags (Optional[Iterable[str]]): Product tags used - for filtering. - active (Optional[bool]): Filter active/inactive products. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Generator[ProductDict, None, None]: Queried product entities. - - """ - if not project_name: - return - - # Prepare these filters before 'name_by_filter_ids' filter - filter_product_names = None - if product_names is not None: - filter_product_names = set(product_names) - if not filter_product_names: - return - - filter_folder_ids = None - if folder_ids is not None: - filter_folder_ids = set(folder_ids) - if not filter_folder_ids: - return - - # This will disable 'folder_ids' and 'product_names' filters - # - maybe could be enhanced in future? - if names_by_folder_ids is not None: - filter_product_names = set() - filter_folder_ids = set() - - for folder_id, names in names_by_folder_ids.items(): - if folder_id and names: - filter_folder_ids.add(folder_id) - filter_product_names |= set(names) - - if not filter_product_names or not filter_folder_ids: - return - - # Convert fields and add minimum required fields - if fields: - fields = set(fields) | {"id"} - self._prepare_fields("product", fields) - else: - fields = self.get_default_fields_for_type("product") - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for products. The" - " argument will be removed from function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered - if names_by_folder_ids: - fields.add("name") - fields.add("folderId") - - # Prepare filters for query - filters = { - "projectName": project_name - } - - if filter_folder_ids: - filters["folderIds"] = list(filter_folder_ids) - - if filter_product_names: - filters["productNames"] = list(filter_product_names) - - if not _prepare_list_filters( - filters, - ("productIds", product_ids), - ("productTypes", product_types), - ("productStatuses", statuses), - ("productTags", tags), - ): - return - - for filter_key, filter_value in ( - ("productNameRegex", product_name_regex), - ("productPathRegex", product_path_regex), - ): - if filter_value: - filters[filter_key] = filter_value - - query = products_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - parsed_data = query.query(self) - - products = parsed_data.get("project", {}).get("products", []) - # Filter products by 'names_by_folder_ids' - if names_by_folder_ids: - products_by_folder_id = collections.defaultdict(list) - for product in products: - filtered_product = self._filter_product( - project_name, product, active - ) - if filtered_product is not None: - folder_id = filtered_product["folderId"] - products_by_folder_id[folder_id].append(filtered_product) - - for folder_id, names in names_by_folder_ids.items(): - for folder_product in products_by_folder_id[folder_id]: - if folder_product["name"] in names: - yield folder_product - - else: - for product in products: - filtered_product = self._filter_product( - project_name, product, active - ) - if filtered_product is not None: - yield filtered_product - - def get_product_by_id( - self, - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: - """Query product entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_id (str): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - products = self.get_products( - project_name, - product_ids=[product_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for product in products: - return product - return None - - def get_product_by_name( - self, - project_name: str, - product_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: - """Query product entity by name and folder id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - products = self.get_products( - project_name, - product_names=[product_name], - folder_ids=[folder_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for product in products: - return product - return None - - def get_product_types( - self, fields: Optional[Iterable[str]] = None - ) -> List["ProductTypeDict"]: - """Types of products. - - This is server wide information. Product types have 'name', 'icon' and - 'color'. - - Args: - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - list[ProductTypeDict]: Product types information. - - """ - if not fields: - fields = self.get_default_fields_for_type("productType") - - query = product_types_query(fields) - - parsed_data = query.query(self) - - return parsed_data.get("productTypes", []) - - def get_project_product_types( - self, project_name: str, fields: Optional[Iterable[str]] = None - ) -> List["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. - - Filter only product types available in a project. - - Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - List[ProductTypeDict]: Product types information. - - """ - warnings.warn( - "Used deprecated function 'get_project_product_types'." - " Use 'get_project' with 'productTypes' in 'fields' instead.", - DeprecationWarning, - stacklevel=2, - ) - if fields is None: - fields = {"productTypes"} - else: - fields = { - f"productTypes.{key}" - for key in fields - } - - project = self.get_project(project_name, fields=fields) - return project["productTypes"] - - def get_product_type_names( - self, - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, - ) -> Set[str]: - """DEPRECATED Product type names. - - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. - - Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. - - Returns: - set[str]: Product type names. - - """ - warnings.warn( - "Used deprecated function 'get_product_type_names'." - " Use 'get_product_types' or 'get_products' instead.", - DeprecationWarning, - stacklevel=2, - ) - if project_name: - if not product_ids: - return set() - products = self.get_products( - project_name, - product_ids=product_ids, - fields=["productType"], - active=None, - ) - return { - product["productType"] - for product in products - } - - return { - product_info["name"] - for product_info in self.get_product_types(project_name) - } - - def create_product( - self, - project_name: str, - name: str, - product_type: str, - folder_id: str, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] =None, - status: Optional[str] = None, - active: "Union[bool, None]" = None, - product_id: Optional[str] = None, - ) -> str: - """Create new product. - - Args: - project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. - folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is - generated. - - Returns: - str: Product id. - - """ - if not product_id: - product_id = create_entity_id() - create_data = { - "id": product_id, - "name": name, - "productType": product_type, - "folderId": folder_id, - } - for key, value in ( - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/products", - **create_data - ) - response.raise_for_status() - return product_id - - def update_product( - self, - project_name: str, - product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - ): - """Update product entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("productType", product_type), - ("folderId", folder_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/products/{product_id}", - **update_data - ) - response.raise_for_status() - - def delete_product(self, project_name: str, product_id: str): - """Delete product. - - Args: - project_name (str): Project name. - product_id (str): Product id to delete. - - """ - response = self.delete( - f"projects/{project_name}/products/{product_id}" - ) - response.raise_for_status() - - def get_versions( - self, - project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. - - Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Generator[VersionDict, None, None]: Queried version entities. - - """ - if not fields: - fields = self.get_default_fields_for_type("version") - else: - fields = set(fields) - self._prepare_fields("version", fields) - - # Make sure fields have minimum required fields - fields |= {"id", "version"} - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for versions. The" - " argument will be removed form function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - if not hero and not standard: - return - - filters = { - "projectName": project_name - } - if not _prepare_list_filters( - filters, - ("taskIds", task_ids), - ("versionIds", version_ids), - ("productIds", product_ids), - ("taskIds", task_ids), - ("versions", versions), - ("versionStatuses", statuses), - ("versionTags", tags), - ): - return - - queries = [] - # Add filters based on 'hero' and 'standard' - # NOTE: There is not a filter to "ignore" hero versions or to get - # latest and hero version - # - if latest and hero versions should be returned it must be done in - # 2 graphql queries - if standard and not latest: - # This query all versions standard + hero - # - hero must be filtered out if is not enabled during loop - query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - queries.append(query) - else: - if hero: - # Add hero query if hero is enabled - hero_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - hero_query.set_variable_value(attr, filter_value) - - hero_query.set_variable_value("heroOnly", True) - queries.append(hero_query) - - if standard: - standard_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - standard_query.set_variable_value(attr, filter_value) - - if latest: - standard_query.set_variable_value("latestOnly", True) - queries.append(standard_query) - - for query in queries: - for parsed_data in query.continuous_query(self): - for version in parsed_data["project"]["versions"]: - if active is not None and version["active"] is not active: - continue - - if not hero and version["version"] < 0: - continue - - self._convert_entity_data(version) - - yield version - - def get_version_by_id( - self, - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query version entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_versions( - project_name, - version_ids=[version_id], - active=None, - hero=True, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_version_by_name( - self, - project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query version entity by version and product id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_versions( - project_name, - product_ids=[product_id], - versions=[version], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_version_by_id( - self, - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query hero version entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_hero_versions( - project_name, - version_ids=[version_id], - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_version_by_product_id( - self, - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query hero version entity by product id. - - Only one hero version is available on a product. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_id (int): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_hero_versions( - project_name, - product_ids=[product_id], - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_versions( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - return self.get_versions( - project_name, - version_ids=version_ids, - product_ids=product_ids, - hero=True, - standard=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - - def get_last_versions( - self, - project_name: str, - product_ids: Iterable[str], - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. - - Args: - project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. - - """ - if fields: - fields = set(fields) - fields.add("productId") - product_ids = set(product_ids) - versions = self.get_versions( - project_name, - product_ids=product_ids, - latest=True, - hero=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - output = { - version["productId"]: version - for version in versions - } - for product_id in product_ids: - output.setdefault(product_id, None) - return output - - def get_last_version_by_product_id( - self, - project_name: str, - product_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: - """Query last version entity by product id. - - Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Queried version entity or None. - - """ - versions = self.get_versions( - project_name, - product_ids=[product_id], - latest=True, - hero=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_last_version_by_product_name( - self, - project_name: str, - product_name: str, - folder_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. - - Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[VersionDict]: Queried version entity or None. - - """ - if not folder_id: - return None - - product = self.get_product_by_name( - project_name, product_name, folder_id, fields={"id"} - ) - if not product: - return None - return self.get_last_version_by_product_id( - project_name, - product["id"], - active=active, - fields=fields, - own_attributes=own_attributes - ) - - def version_is_latest(self, project_name: str, version_id: str) -> bool: - """Is version latest from a product. - - Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. - - Returns: - bool: Version is latest or not. - - """ - query = GraphQlQuery("VersionIsLatest") - project_name_var = query.add_variable( - "projectName", "String!", project_name - ) - version_id_var = query.add_variable( - "versionId", "String!", version_id - ) - project_query = query.add_field("project") - project_query.set_filter("name", project_name_var) - version_query = project_query.add_field("version") - version_query.set_filter("id", version_id_var) - product_query = version_query.add_field("product") - latest_version_query = product_query.add_field("latestVersion") - latest_version_query.add_field("id") - - parsed_data = query.query(self) - latest_version = ( - parsed_data["project"]["version"]["product"]["latestVersion"] - ) - return latest_version["id"] == version_id - - def create_version( - self, - project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, - ) -> str: - """Create new version. - - Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is - generated. - - Returns: - str: Version id. - - """ - if not version_id: - version_id = create_entity_id() - create_data = { - "id": version_id, - "version": version, - "productId": product_id, - } - for key, value in ( - ("taskId", task_id), - ("author", author), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/versions", - **create_data - ) - response.raise_for_status() - return version_id - - def update_version( - self, - project_name: str, - version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update version entity on server. - - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. - author (Optional[str]): New author username. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("version", version), - ("productId", product_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("author", author), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("taskId", task_id), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/versions/{version_id}", - **update_data - ) - response.raise_for_status() - - def delete_version(self, project_name: str, version_id: str): - """Delete version. - - Args: - project_name (str): Project name. - version_id (str): Version id to delete. - - """ - response = self.delete( - f"projects/{project_name}/versions/{version_id}" - ) - response.raise_for_status() - - def _representation_conversion( - self, representation: "RepresentationDict" - ): - if "context" in representation: - orig_context = representation["context"] - context = {} - if orig_context and orig_context != "null": - context = json.loads(orig_context) - representation["context"] = context - - repre_files = representation.get("files") - if not repre_files: - return - - for repre_file in repre_files: - repre_file_size = repre_file.get("size") - if repre_file_size is not None: - repre_file["size"] = int(repre_file["size"]) - - def get_representations( - self, - project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: - - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. - - Args: - project_name (str): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. - version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Generator[RepresentationDict, None, None]: Queried - representation entities. - - """ - if not fields: - fields = self.get_default_fields_for_type("representation") - else: - fields = set(fields) - self._prepare_fields("representation", fields) - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for representations. " - "The argument will be removed form function signature in " - "future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - if "files" in fields: - fields.discard("files") - fields |= REPRESENTATION_FILES_FIELDS - - filters = { - "projectName": project_name - } - - if representation_ids is not None: - representation_ids = set(representation_ids) - if not representation_ids: - return - filters["representationIds"] = list(representation_ids) - - version_ids_filter = None - representation_names_filter = None - if names_by_version_ids is not None: - version_ids_filter = set() - representation_names_filter = set() - for version_id, names in names_by_version_ids.items(): - version_ids_filter.add(version_id) - representation_names_filter |= set(names) - - if not version_ids_filter or not representation_names_filter: - return - - else: - if representation_names is not None: - representation_names_filter = set(representation_names) - if not representation_names_filter: - return - - if version_ids is not None: - version_ids_filter = set(version_ids) - if not version_ids_filter: - return - - if version_ids_filter: - filters["versionIds"] = list(version_ids_filter) - - if representation_names_filter: - filters["representationNames"] = list(representation_names_filter) - - if statuses is not None: - statuses = set(statuses) - if not statuses: - return - filters["representationStatuses"] = list(statuses) - - if tags is not None: - tags = set(tags) - if not tags: - return - filters["representationTags"] = list(tags) - - if has_links is not None: - filters["representationHasLinks"] = has_links.upper() - - query = representations_graphql_query(fields) - - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for repre in parsed_data["project"]["representations"]: - if active is not None and active is not repre["active"]: - continue - - self._convert_entity_data(repre) - - self._representation_conversion(repre) - - yield repre - - def get_representation_by_id( - self, - project_name: str, - representation_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. - - Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation - entity or None. - - """ - representations = self.get_representations( - project_name, - representation_ids=[representation_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for representation in representations: - return representation - return None - - def get_representation_by_name( - self, - project_name: str, - representation_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. - - Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation entity - or None. - - """ - representations = self.get_representations( - project_name, - representation_names=[representation_name], - version_ids=[version_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for representation in representations: - return representation - return None - - def get_representations_hierarchy( - self, - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, - ) -> Dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. - - Representation entity with parent entities up to project. - - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. - - Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. - - Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. - - """ - if not representation_ids: - return {} - - if project_fields is not None: - project_fields = set(project_fields) - self._prepare_fields("project", project_fields) - - project = {} - if project_fields is None: - project = self.get_project(project_name) - - elif project_fields: - # Keep project as empty dictionary if does not have - # filled any fields - project = self.get_project( - project_name, fields=project_fields - ) - - repre_ids = set(representation_ids) - output = { - repre_id: RepresentationHierarchy( - project, None, None, None, None, None - ) - for repre_id in representation_ids - } - - if folder_fields is None: - folder_fields = self.get_default_fields_for_type("folder") - else: - folder_fields = set(folder_fields) - - if task_fields is None: - task_fields = self.get_default_fields_for_type("task") - else: - task_fields = set(task_fields) - - if product_fields is None: - product_fields = self.get_default_fields_for_type("product") - else: - product_fields = set(product_fields) - - if version_fields is None: - version_fields = self.get_default_fields_for_type("version") - else: - version_fields = set(version_fields) - - if representation_fields is None: - representation_fields = self.get_default_fields_for_type( - "representation" - ) - else: - representation_fields = set(representation_fields) - - for (entity_type, fields) in ( - ("folder", folder_fields), - ("task", task_fields), - ("product", product_fields), - ("version", version_fields), - ("representation", representation_fields), - ): - self._prepare_fields(entity_type, fields) - - representation_fields.add("id") - - query = representations_hierarchy_qraphql_query( - folder_fields, - task_fields, - product_fields, - version_fields, - representation_fields, - ) - query.set_variable_value("projectName", project_name) - query.set_variable_value("representationIds", list(repre_ids)) - - parsed_data = query.query(self) - for repre in parsed_data["project"]["representations"]: - repre_id = repre["id"] - version = repre.pop("version", {}) - product = version.pop("product", {}) - task = version.pop("task", None) - folder = product.pop("folder", {}) - self._convert_entity_data(repre) - self._representation_conversion(repre) - self._convert_entity_data(version) - self._convert_entity_data(product) - self._convert_entity_data(folder) - if task: - self._convert_entity_data(task) - - output[repre_id] = RepresentationHierarchy( - project, folder, task, product, version, repre - ) - - return output - - def get_representation_hierarchy( - self, - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, - ) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. - - Returns: - RepresentationHierarchy: Representation hierarchy entities. - - """ - if not representation_id: - return None - - parents_by_repre_id = self.get_representations_hierarchy( - project_name, - [representation_id], - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, - ) - return parents_by_repre_id[representation_id] - - def get_representations_parents( - self, - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - ) -> Dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. - - """ - hierarchy_by_repre_id = self.get_representations_hierarchy( - project_name, - representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=set(), - product_fields=product_fields, - version_fields=version_fields, - representation_fields={"id"}, - ) - return { - repre_id: RepresentationParents( - hierarchy.version, - hierarchy.product, - hierarchy.folder, - hierarchy.project, - ) - for repre_id, hierarchy in hierarchy_by_repre_id.items() - } - - def get_representation_parents( - self, - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - ) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - RepresentationParents: Representation parent entities. - - """ - if not representation_id: - return None - - parents_by_repre_id = self.get_representations_parents( - project_name, - [representation_id], - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, - ) - return parents_by_repre_id[representation_id] - - def get_repre_ids_by_context_filters( - self, - project_name: str, - context_filters: Optional[Dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - ) -> List[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. - - Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. - - Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) - - """ - if not isinstance(context_filters, dict): - raise TypeError( - f"Expected 'dict' got {str(type(context_filters))}" - ) - - filter_body = {} - if representation_names is not None: - if not representation_names: - return [] - filter_body["names"] = list(set(representation_names)) - - if version_ids is not None: - if not version_ids: - return [] - filter_body["versionIds"] = list(set(version_ids)) - - body_context_filters = [] - for key, filters in context_filters.items(): - if not isinstance(filters, (set, list, tuple)): - raise TypeError( - "Expected 'set', 'list', 'tuple' got {}".format( - str(type(filters)))) - - new_filters = set() - for filter_value in filters: - if isinstance(filter_value, PatternType): - filter_value = filter_value.pattern - new_filters.add(filter_value) - - body_context_filters.append({ - "key": key, - "values": list(new_filters) - }) - - response = self.post( - f"projects/{project_name}/repreContextFilter", - context=body_context_filters, - **filter_body - ) - response.raise_for_status() - return response.data["ids"] - - def create_representation( - self, - project_name: str, - name: str, - version_id: str, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]]=None, - status: Optional[str] = None, - active: Optional[bool] = None, - representation_id: Optional[str] = None, - ) -> str: - """Create new representation. - - Args: - project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. - - Returns: - str: Representation id. - - """ - if not representation_id: - representation_id = create_entity_id() - create_data = { - "id": representation_id, - "name": name, - "versionId": version_id, - } - for key, value in ( - ("files", files), - ("attrib", attrib), - ("data", data), - ("traits", traits), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/representations", - **create_data - ) - response.raise_for_status() - return representation_id - - def update_representation( - self, - project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - ): - """Update representation entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("versionId", version_id), - ("files", files), - ("attrib", attrib), - ("data", data), - ("traits", traits), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/representations/{representation_id}", - **update_data - ) - response.raise_for_status() - - def delete_representation( - self, project_name: str, representation_id: str - ): - """Delete representation. - - Args: - project_name (str): Project name. - representation_id (str): Representation id to delete. - - """ - response = self.delete( - f"projects/{project_name}/representations/{representation_id}" - ) - response.raise_for_status() - - def get_workfiles_info( - self, - project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] =None, - paths: Optional[Iterable[str]] =None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str]=None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["WorkfileInfoDict", None, None]: - """Workfile info entities by passed filters. - - Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. - - """ - filters = {"projectName": project_name} - if task_ids is not None: - task_ids = set(task_ids) - if not task_ids: - return - filters["taskIds"] = list(task_ids) - - if paths is not None: - paths = set(paths) - if not paths: - return - filters["paths"] = list(paths) - - if path_regex is not None: - filters["workfilePathRegex"] = path_regex - - if workfile_ids is not None: - workfile_ids = set(workfile_ids) - if not workfile_ids: - return - filters["workfileIds"] = list(workfile_ids) - - if statuses is not None: - statuses = set(statuses) - if not statuses: - return - filters["workfileStatuses"] = list(statuses) - - if tags is not None: - tags = set(tags) - if not tags: - return - filters["workfileTags"] = list(tags) - - if has_links is not None: - filters["workfilehasLinks"] = has_links.upper() - - if not fields: - fields = self.get_default_fields_for_type("workfile") - else: - fields = set(fields) - self._prepare_fields("workfile", fields) - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for workfiles. The" - " argument will be removed form function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - query = workfiles_info_graphql_query(fields) - - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for workfile_info in parsed_data["project"]["workfiles"]: - self._convert_entity_data(workfile_info) - yield workfile_info - - def get_workfile_info( - self, - project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - if not task_id or not path: - return None - - for workfile_info in self.get_workfiles_info( - project_name, - task_ids=[task_id], - paths=[path], - fields=fields, - own_attributes=own_attributes - ): - return workfile_info - return None - - def get_workfile_info_by_id( - self, - project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - if not workfile_id: - return None - - for workfile_info in self.get_workfiles_info( - project_name, - workfile_ids=[workfile_id], - fields=fields, - own_attributes=own_attributes - ): - return workfile_info - return None - - def _prepare_thumbnail_content( - self, - project_name: str, - response: RestApiResponse, - ) -> ThumbnailContent: - content = None - content_type = response.content_type - - # It is expected the response contains thumbnail id otherwise the - # content cannot be cached and filepath returned - thumbnail_id = response.headers.get("X-Thumbnail-Id") - if thumbnail_id is not None: - content = response.content - - return ThumbnailContent( - project_name, thumbnail_id, content, content_type - ) - - def get_thumbnail_by_id( - self, project_name: str, thumbnail_id: str - ) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - response = self.raw_get( - f"projects/{project_name}/thumbnails/{thumbnail_id}" - ) - return self._prepare_thumbnail_content(project_name, response) - - def get_thumbnail( - self, - project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - - if entity_type in ( - "folder", - "task", - "version", - "workfile", - ): - entity_type += "s" - - response = self.raw_get( - f"projects/{project_name}/{entity_type}/{entity_id}/thumbnail" - ) - return self._prepare_thumbnail_content(project_name, response) - - def get_folder_thumbnail( - self, - project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. - - Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_folder_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "folder", folder_id - ) - - def get_task_thumbnail( - self, - project_name: str, - task_id: str, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - return self.get_thumbnail(project_name, "task", task_id) - - def get_version_thumbnail( - self, - project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. - - Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_version_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "version", version_id - ) - - def get_workfile_thumbnail( - self, - project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_workfile_thumbnail' got 'thumbnail_id'" - " which is deprecated and will be removed in future" - " version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "workfile", workfile_id - ) - - def create_thumbnail( - self, - project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, - ) -> str: - """Create new thumbnail on server from passed path. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. - - Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - - if thumbnail_id: - self.update_thumbnail( - project_name, - thumbnail_id, - src_filepath - ) - return thumbnail_id - - mime_type = get_media_mime_type(src_filepath) - response = self.upload_file( - f"projects/{project_name}/thumbnails", - src_filepath, - request_type=RequestTypes.post, - headers={"Content-Type": mime_type}, - ) - response.raise_for_status() - return response.json()["id"] - - def update_thumbnail( - self, project_name: str, thumbnail_id: str, src_filepath: str - ): - """Change thumbnail content by id. - - Update can be also used to create new thumbnail. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - - mime_type = get_media_mime_type(src_filepath) - response = self.upload_file( - f"projects/{project_name}/thumbnails/{thumbnail_id}", - src_filepath, - request_type=RequestTypes.put, - headers={"Content-Type": mime_type}, - ) - response.raise_for_status() - - def create_project( - self, - project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, - ) -> "ProjectDict": - """Create project using AYON settings. - - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - - Raises: - ValueError: When project name already exists. - - Returns: - ProjectDict: Created project entity. - - """ - if self.get_project(project_name): - raise ValueError( - f"Project with name \"{project_name}\" already exists" - ) - - if not PROJECT_NAME_REGEX.match(project_name): - raise ValueError( - f"Project name \"{project_name}\" contain invalid characters" - ) - - preset = self.get_project_anatomy_preset(preset_name) - - result = self.post( - "projects", - name=project_name, - code=project_code, - anatomy=preset, - library=library_project - ) - - if result.status != 201: - details = f"Unknown details ({result.status})" - if result.data: - details = result.data.get("detail") or details - raise ValueError( - f"Failed to create project \"{project_name}\": {details}" - ) - - return self.get_project(project_name) - - def update_project( - self, - project_name: str, - library: Optional[bool] = None, - folder_types: Optional[List[Dict[str, Any]]] = None, - task_types: Optional[List[Dict[str, Any]]] = None, - link_types: Optional[List[Dict[str, Any]]] = None, - statuses: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[Dict[str, Any]]] = None, - config: Optional[Dict[str, Any]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes - ): - """Update project entity on server. - - Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. - - """ - changes.update({ - key: value - for key, value in ( - ("library", library), - ("folderTypes", folder_types), - ("taskTypes", task_types), - ("linkTypes", link_types), - ("statuses", statuses), - ("tags", tags), - ("config", config), - ("attrib", attrib), - ("data", data), - ("active", active), - ("code", project_code), - ) - if value is not None - }) - response = self.patch( - f"projects/{project_name}", - **changes - ) - response.raise_for_status() - - def delete_project(self, project_name: str): - """Delete project from server. - - This will completely remove project from server without any step back. - - Args: - project_name (str): Project name that will be removed. - - """ - if not self.get_project(project_name): - raise ValueError( - f"Project with name \"{project_name}\" was not found" - ) - - result = self.delete(f"projects/{project_name}") - if result.status_code != 204: - detail = result.data["detail"] - raise ValueError( - f"Failed to delete project \"{project_name}\". {detail}" - ) - - # --- Links --- - def get_full_link_type_name( - self, link_type_name: str, input_type: str, output_type: str - ) -> str: - """Calculate full link type name used for query from server. - - Args: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - str: Full name of link type used for query from server. - - """ - return "|".join([link_type_name, input_type, output_type]) - - def get_link_types(self, project_name: str) -> List[Dict[str, Any]]: - """All link types available on a project. - - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] - - Args: - project_name (str): Name of project where to look for link types. - - Returns: - list[dict[str, Any]]: Link types available on project. - - """ - response = self.get(f"projects/{project_name}/links/types") - response.raise_for_status() - return response.data["types"] - - def get_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - ) -> Optional[str]: - """Get link type data. - - There is not dedicated REST endpoint to get single link type, - so method 'get_link_types' is used. - - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - - Args: - project_name (str): Project where link type is available. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - Optional[str]: Link type information. - - """ - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - for link_type in self.get_link_types(project_name): - if link_type["name"] == full_type_name: - return link_type - return None - - def create_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, - ): - """Create or update link type on server. - - Warning: - Because PUT is used for creation it is also used for update. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Additional data related to link. - - Raises: - HTTPRequestError: Server error happened. - - """ - if data is None: - data = {} - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - response = self.put( - f"projects/{project_name}/links/types/{full_type_name}", - **data - ) - response.raise_for_status() - - def delete_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - ): - """Remove link type from project. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Raises: - HTTPRequestError: Server error happened. - - """ - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - response = self.delete( - f"projects/{project_name}/links/types/{full_type_name}" - ) - response.raise_for_status() - - def make_sure_link_type_exists( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, - ): - """Make sure link type exists on a project. - - Args: - project_name (str): Name of project. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Link type related data. - - """ - link_type = self.get_link_type( - project_name, link_type_name, input_type, output_type) - if ( - link_type - and (data is None or data == link_type["data"]) - ): - return - self.create_link_type( - project_name, link_type_name, input_type, output_type, data - ) - - def create_link( - self, - project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, - ): - """Create link between 2 entities. - - Link has a type which must already exists on a project. - - Example output:: - - { - "id": "59a212c0d2e211eda0e20242ac120002" - } - - Args: - project_name (str): Project where the link is created. - link_type_name (str): Type of link. - input_id (str): Input entity id. - input_type (str): Entity type of input entity. - output_id (str): Output entity id. - output_type (str): Entity type of output entity. - link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. - - Returns: - dict[str, str]: Information about link. - - Raises: - HTTPRequestError: Server error happened. - - """ - full_link_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type) - - kwargs = { - "input": input_id, - "output": output_id, - "linkType": full_link_type_name, - } - if link_name: - kwargs["name"] = link_name - - response = self.post( - f"projects/{project_name}/links", **kwargs - ) - response.raise_for_status() - return response.data - - def delete_link(self, project_name: str, link_id: str): - """Remove link by id. - - Args: - project_name (str): Project where link exists. - link_id (str): Id of link. - - Raises: - HTTPRequestError: Server error happened. - - """ - response = self.delete( - f"projects/{project_name}/links/{link_id}" - ) - response.raise_for_status() - - def _prepare_link_filters( - self, - filters: Dict[str, Any], - link_types: "Union[Iterable[str], None]", - link_direction: "Union[LinkDirection, None]", - link_names: "Union[Iterable[str], None]", - link_name_regex: "Union[str, None]", - ) -> bool: - """Add links filters for GraphQl queries. - - Args: - filters (dict[str, Any]): Object where filters will be added. - link_types (Union[Iterable[str], None]): Link types filters. - link_direction (Union[Literal["in", "out"], None]): Direction of - link "in", "out" or 'None' for both. - link_names (Union[Iterable[str], None]): Link name filters. - link_name_regex (Union[str, None]): Regex filter for link name. - - Returns: - bool: Links are valid, and query from server can happen. - - """ - if link_types is not None: - link_types = set(link_types) - if not link_types: - return False - filters["linkTypes"] = list(link_types) - - if link_names is not None: - link_names = set(link_names) - if not link_names: - return False - filters["linkNames"] = list(link_names) - - if link_direction is not None: - if link_direction not in ("in", "out"): - return False - filters["linkDirection"] = link_direction - - if link_name_regex is not None: - filters["linkNameRegex"] = link_name_regex - return True - - def get_entities_links( - self, - project_name: str, - entity_type: str, - entity_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - link_names: Optional[Iterable[str]] = None, - link_name_regex: Optional[str] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Helper method to get links from server for entity types. - - .. highlight:: text - .. code-block:: text - - Example output: - { - "59a212c0d2e211eda0e20242ac120001": [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, - ... - ], - ... - } - - Args: - project_name (str): Project where links are. - entity_type (Literal["folder", "task", "product", - "version", "representations"]): Entity type. - entity_ids (Optional[Iterable[str]]): Ids of entities for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. + progress (Optional[TransferProgress]): Object that gives ability + to track upload progress. + request_type (Optional[RequestType]): Type of request that will + be used to upload file. + **kwargs (Any): Additional arguments that will be passed + to request function. Returns: - dict[str, list[dict[str, Any]]]: Link info by entity ids. + requests.Response: Response object """ - if entity_type == "folder": - query_func = folders_graphql_query - id_filter_key = "folderIds" - project_sub_key = "folders" - elif entity_type == "task": - query_func = tasks_graphql_query - id_filter_key = "taskIds" - project_sub_key = "tasks" - elif entity_type == "product": - query_func = products_graphql_query - id_filter_key = "productIds" - project_sub_key = "products" - elif entity_type == "version": - query_func = versions_graphql_query - id_filter_key = "versionIds" - project_sub_key = "versions" - elif entity_type == "representation": - query_func = representations_graphql_query - id_filter_key = "representationIds" - project_sub_key = "representations" - else: - raise ValueError("Unknown type \"{}\". Expected {}".format( - entity_type, - ", ".join( - ("folder", "task", "product", "version", "representation") - ) - )) - - output = collections.defaultdict(list) - filters = { - "projectName": project_name - } - if entity_ids is not None: - entity_ids = set(entity_ids) - if not entity_ids: - return output - filters[id_filter_key] = list(entity_ids) - - if not self._prepare_link_filters( - filters, link_types, link_direction, link_names, link_name_regex - ): - return output + if progress is None: + progress = TransferProgress() - link_fields = {"id", "links"} - query = query_func(link_fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) + progress.set_source_url(filepath) - for parsed_data in query.continuous_query(self): - for entity in parsed_data["project"][project_sub_key]: - entity_id = entity["id"] - output[entity_id].extend(entity["links"]) - return output + with open(filepath, "rb") as stream: + return self.upload_file_from_stream( + endpoint, stream, progress, request_type, **kwargs + ) - def get_folders_links( + def upload_reviewable( self, project_name: str, - folder_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query folders links from server. + version_id: str, + filepath: str, + label: Optional[str] = None, + content_type: Optional[str] = None, + filename: Optional[str] = None, + progress: Optional[TransferProgress] = None, + headers: Optional[dict[str, Any]] = None, + **kwargs + ) -> requests.Response: + """Upload reviewable file to server. Args: - project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + version_id (str): Version id. + filepath (str): Reviewable file path to upload. + label (Optional[str]): Reviewable label. Filled automatically + server side with filename. + content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): User as original filename. Filename from + 'filepath' is used when not filled. + progress (Optional[TransferProgress]): Progress. + headers (Optional[dict[str, Any]]): Headers. Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. + requests.Response: Server response. """ - return self.get_entities_links( - project_name, "folder", folder_ids, link_types, link_direction - ) + if not content_type: + content_type = get_media_mime_type(filepath) - def get_folder_links( - self, - project_name: str, - folder_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query folder links from server. + if not content_type: + raise ValueError( + f"Could not determine MIME type of file '{filepath}'" + ) - Args: - project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + if headers is None: + headers = self.get_headers(content_type) + else: + # Make sure content-type is filled with file content type + content_type_key = next( + ( + key + for key in headers + if key.lower() == "content-type" + ), + "Content-Type" + ) + headers[content_type_key] = content_type - Returns: - list[dict[str, Any]]: Link info of folder. + # Fill original filename if not explicitly defined + if not filename: + filename = os.path.basename(filepath) + headers["x-file-name"] = filename + + query = prepare_query_string({"label": label or None}) + endpoint = ( + f"/projects/{project_name}" + f"/versions/{version_id}/reviewables{query}" + ) + return self.upload_file( + endpoint, + filepath, + progress=progress, + headers=headers, + request_type=RequestTypes.post, + **kwargs + ) + + def trigger_server_restart(self): + """Trigger server restart. + + Restart may be required when a change of specific value happened on + server. """ - return self.get_folders_links( - project_name, [folder_id], link_types, link_direction - )[folder_id] + result = self.post("system/restart") + if result.status_code != 204: + # TODO add better exception + raise ValueError("Failed to restart server") - def get_tasks_links( + def query_graphql( self, - project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query tasks links from server. + query: str, + variables: Optional[dict[str, Any]] = None, + ) -> GraphQlResponse: + """Execute GraphQl query. Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + query (str): GraphQl query string. + variables (Optional[dict[str, Any]): Variables that can be + used in query. Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + GraphQlResponse: Response from server. """ - return self.get_entities_links( - project_name, "task", task_ids, link_types, link_direction + data = {"query": query, "variables": variables or {}} + response = self._do_rest_request( + RequestTypes.post, + self._graphql_url, + json=data ) + response.raise_for_status() + return GraphQlResponse(response) - def get_task_links( - self, - project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query task links from server. + def get_graphql_schema(self) -> dict[str, Any]: + return self.query_graphql(INTROSPECTION_QUERY).data["data"] - Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + def get_server_schema(self) -> Optional[dict[str, Any]]: + """Get server schema with info, url paths, components etc. + + Todos: + Cache schema - How to find out it is outdated? Returns: - list[dict[str, Any]]: Link info of task. + dict[str, Any]: Full server schema. """ - return self.get_tasks_links( - project_name, [task_id], link_types, link_direction - )[task_id] + url = f"{self._base_url}/openapi.json" + response = self._do_rest_request(RequestTypes.get, url) + if response: + return response.data + return None - def get_products_links( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query products links from server. + def get_schemas(self) -> dict[str, Any]: + """Get components schema. - Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + Name of components does not match entity type names e.g. 'project' is + under 'ProjectModel'. We should find out some mapping. Also, there + are properties which don't have information about reference to object + e.g. 'config' has just object definition without reference schema. Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. + dict[str, Any]: Component schemas. """ - return self.get_entities_links( - project_name, "product", product_ids, link_types, link_direction - ) + server_schema = self.get_server_schema() + return server_schema["components"]["schemas"] - def get_product_links( - self, - project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query product links from server. + def get_default_fields_for_type(self, entity_type: str) -> set[str]: + """Default fields for entity type. + + Returns most of commonly used fields from server. Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + entity_type (str): Name of entity type. Returns: - list[dict[str, Any]]: Link info of product. + set[str]: Fields that should be queried from server. """ - return self.get_products_links( - project_name, [product_id], link_types, link_direction - )[product_id] + # Event does not have attributes + if entity_type == "event": + return set(DEFAULT_EVENT_FIELDS) - def get_versions_links( - self, - project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query versions links from server. + if entity_type == "activity": + return set(DEFAULT_ACTIVITY_FIELDS) - Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + if entity_type == "project": + entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + entity_type_defaults.add("productTypes") - Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. + elif entity_type == "folder": + entity_type_defaults = set(DEFAULT_FOLDER_FIELDS) - """ - return self.get_entities_links( - project_name, "version", version_ids, link_types, link_direction - ) + elif entity_type == "task": + entity_type_defaults = set(DEFAULT_TASK_FIELDS) - def get_version_links( - self, - project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query version links from server. + elif entity_type == "product": + entity_type_defaults = set(DEFAULT_PRODUCT_FIELDS) - Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + elif entity_type == "version": + entity_type_defaults = set(DEFAULT_VERSION_FIELDS) - Returns: - list[dict[str, Any]]: Link info of version. + elif entity_type == "representation": + entity_type_defaults = ( + DEFAULT_REPRESENTATION_FIELDS + | REPRESENTATION_FILES_FIELDS + ) - """ - return self.get_versions_links( - project_name, [version_id], link_types, link_direction - )[version_id] + if not self.graphql_allows_traits_in_representations: + entity_type_defaults.discard("traits") - def get_representations_links( - self, - project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query representations links from server. + elif entity_type == "productType": + entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) - Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + elif entity_type == "workfile": + entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS) - Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. + elif entity_type == "user": + entity_type_defaults = set(DEFAULT_USER_FIELDS) - """ - return self.get_entities_links( - project_name, - "representation", - representation_ids, - link_types, - link_direction + elif entity_type == "entityList": + entity_type_defaults = set(DEFAULT_ENTITY_LIST_FIELDS) + + else: + raise ValueError(f"Unknown entity type \"{entity_type}\"") + return ( + entity_type_defaults + | self.get_attributes_fields_for_type(entity_type) ) - def get_representation_links( + def get_rest_entity_by_id( self, project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None - ) -> List[Dict[str, Any]]: - """Query representation links from server. + entity_type: str, + entity_id: str, + ) -> Optional["AnyEntityDict"]: + """Get entity using REST on a project by its id. Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where entity is. + entity_type (Literal["folder", "task", "product", "version"]): The + entity type which should be received. + entity_id (str): Id of entity. Returns: - list[dict[str, Any]]: Link info of representation. + Optional[AnyEntityDict]: Received entity data. """ - return self.get_representations_links( - project_name, [representation_id], link_types, link_direction - )[representation_id] + if not all((project_name, entity_type, entity_id)): + return None + + response = self.get( + f"projects/{project_name}/{entity_type}s/{entity_id}" + ) + if response.status == 200: + return response.data + return None # --- Batch operations processing --- def send_batch_operations( self, project_name: str, - operations: List[Dict[str, Any]], + operations: list[dict[str, Any]], can_fail: bool = False, raise_on_fail: bool = True - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Post multiple CRUD operations to server. When multiple changes should be made on server side this is the best @@ -8919,52 +1905,13 @@ def send_batch_operations( raise_on_fail, ) - def send_activities_batch_operations( - self, - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True - ) -> List[Dict[str, Any]]: - """Post multiple CRUD activities operations to server. - - When multiple changes should be made on server side this is the best - way to go. It is possible to pass multiple operations to process on a - server side and do the changes in a transaction. - - Args: - project_name (str): On which project should be operations - processed. - operations (list[dict[str, Any]]): Operations to be processed. - can_fail (Optional[bool]): Server will try to process all - operations even if one of them fails. - raise_on_fail (Optional[bool]): Raise exception if an operation - fails. You can handle failed operations on your own - when set to 'False'. - - Raises: - ValueError: Operations can't be converted to json string. - FailedOperations: When output does not contain server operations - or 'raise_on_fail' is enabled and any operation fails. - - Returns: - list[dict[str, Any]]: Operations result with process details. - - """ - return self._send_batch_operations( - f"projects/{project_name}/operations/activities", - operations, - can_fail, - raise_on_fail, - ) - def _send_batch_operations( self, uri: str, - operations: List[Dict[str, Any]], + operations: list[dict[str, Any]], can_fail: bool, raise_on_fail: bool - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: if not operations: return [] @@ -9027,7 +1974,7 @@ def _send_batch_operations( return op_results def _prepare_fields( - self, entity_type: str, fields: Set[str], own_attributes: bool = False + self, entity_type: str, fields: set[str], own_attributes: bool = False ): if not fields: return diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 32c582bea..aa31065ea 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import io from typing import ( Literal, - Dict, - List, Any, TypedDict, Union, @@ -11,6 +11,8 @@ ) +ServerVersion = tuple[int, int, int, str, str] + ActivityType = Literal[ "comment", "watch", @@ -47,7 +49,7 @@ EventFilterValueType = Union[ None, str, int, float, - List[str], List[int], List[float], + list[str], list[int], list[float], ] @@ -82,7 +84,7 @@ class EventFilterCondition(TypedDict): class EventFilter(TypedDict): - conditions: List[EventFilterCondition] + conditions: list[EventFilterCondition] operator: Literal["and", "or"] @@ -136,38 +138,38 @@ class AttributeSchemaDataDict(TypedDict): minItems: Optional[int] maxItems: Optional[int] regex: Optional[str] - enum: Optional[List[AttributeEnumItemDict]] + enum: Optional[list[AttributeEnumItemDict]] class AttributeSchemaDict(TypedDict): name: str position: int - scope: List[AttributeScope] + scope: list[AttributeScope] builtin: bool data: AttributeSchemaDataDict class AttributesSchemaDict(TypedDict): - attributes: List[AttributeSchemaDict] + attributes: list[AttributeSchemaDict] class AddonVersionInfoDict(TypedDict): hasSettings: bool hasSiteSettings: bool - frontendScopes: Dict[str, Any] - clientPyproject: Dict[str, Any] - clientSourceInfo: List[Dict[str, Any]] + frontendScopes: dict[str, Any] + clientPyproject: dict[str, Any] + clientSourceInfo: list[dict[str, Any]] isBroken: bool class AddonInfoDict(TypedDict): name: str title: str - versions: Dict[str, AddonVersionInfoDict] + versions: dict[str, AddonVersionInfoDict] class AddonsInfoDict(TypedDict): - addons: List[AddonInfoDict] + addons: list[AddonInfoDict] class InstallerInfoDict(TypedDict): @@ -176,15 +178,15 @@ class InstallerInfoDict(TypedDict): size: int checksum: str checksumAlgorithm: str - sources: List[Dict[str, Any]] + sources: list[dict[str, Any]] version: str pythonVersion: str - pythonModules: Dict[str, str] - runtimePythonModules: Dict[str, str] + pythonModules: dict[str, str] + runtimePythonModules: dict[str, str] class InstallersInfoDict(TypedDict): - installers: List[InstallerInfoDict] + installers: list[InstallerInfoDict] class DependencyPackageDict(TypedDict): @@ -193,14 +195,14 @@ class DependencyPackageDict(TypedDict): size: int checksum: str checksumAlgorithm: str - sources: List[Dict[str, Any]] + sources: list[dict[str, Any]] installerVersion: str - sourceAddons: Dict[str, str] - pythonModules: Dict[str, str] + sourceAddons: dict[str, str] + pythonModules: dict[str, str] class DependencyPackagesDict(TypedDict): - packages: List[DependencyPackageDict] + packages: list[DependencyPackageDict] class DevBundleAddonInfoDict(TypedDict): @@ -211,10 +213,10 @@ class DevBundleAddonInfoDict(TypedDict): class BundleInfoDict(TypedDict): name: str createdAt: str - addons: Dict[str, str] + addons: dict[str, str] installerVersion: str - dependencyPackages: Dict[str, str] - addonDevelopment: Dict[str, DevBundleAddonInfoDict] + dependencyPackages: dict[str, str] + addonDevelopment: dict[str, DevBundleAddonInfoDict] isProduction: bool isStaging: bool isArchived: bool @@ -223,9 +225,9 @@ class BundleInfoDict(TypedDict): class BundlesInfoDict(TypedDict): - bundles: List[BundleInfoDict] + bundles: list[BundleInfoDict] productionBundle: str - devBundles: List[str] + devBundles: list[str] class AnatomyPresetInfoDict(TypedDict): @@ -252,12 +254,12 @@ class AnatomyPresetTemplatesDict(TypedDict): version: str frame_padding: int frame: str - work: List[AnatomyPresetTemplateDict] - publish: List[AnatomyPresetTemplateDict] - hero: List[AnatomyPresetTemplateDict] - delivery: List[AnatomyPresetTemplateDict] - staging: List[AnatomyPresetTemplateDict] - others: List[AnatomyPresetTemplateDict] + work: list[AnatomyPresetTemplateDict] + publish: list[AnatomyPresetTemplateDict] + hero: list[AnatomyPresetTemplateDict] + delivery: list[AnatomyPresetTemplateDict] + staging: list[AnatomyPresetTemplateDict] + others: list[AnatomyPresetTemplateDict] class AnatomyPresetSubtypeDict(TypedDict): @@ -291,7 +293,7 @@ class AnatomyPresetStatusDict(TypedDict): state: str icon: str color: str - scope: List[StatusScope] + scope: list[StatusScope] original_name: str @@ -302,29 +304,32 @@ class AnatomyPresetTagDict(TypedDict): class AnatomyPresetDict(TypedDict): - roots: List[AnatomyPresetRootDict] + roots: list[AnatomyPresetRootDict] templates: AnatomyPresetTemplatesDict - attributes: Dict[str, Any] - folder_types: List[AnatomyPresetSubtypeDict] - task_types: List[AnatomyPresetSubtypeDict] - link_types: List[AnatomyPresetLinkTypeDict] - statuses: List[AnatomyPresetStatusDict] - tags: List[AnatomyPresetTagDict] + attributes: dict[str, Any] + folder_types: list[AnatomyPresetSubtypeDict] + task_types: list[AnatomyPresetSubtypeDict] + link_types: list[AnatomyPresetLinkTypeDict] + statuses: list[AnatomyPresetStatusDict] + tags: list[AnatomyPresetTagDict] + primary: bool + name: str class SecretDict(TypedDict): name: str value: str -ProjectDict = Dict[str, Any] -FolderDict = Dict[str, Any] -TaskDict = Dict[str, Any] -ProductDict = Dict[str, Any] -VersionDict = Dict[str, Any] -RepresentationDict = Dict[str, Any] -WorkfileInfoDict = Dict[str, Any] -EventDict = Dict[str, Any] -ActivityDict = Dict[str, Any] + +ProjectDict = dict[str, Any] +FolderDict = dict[str, Any] +TaskDict = dict[str, Any] +ProductDict = dict[str, Any] +VersionDict = dict[str, Any] +RepresentationDict = dict[str, Any] +WorkfileInfoDict = dict[str, Any] +EventDict = dict[str, Any] +ActivityDict = dict[str, Any] AnyEntityDict = Union[ ProjectDict, FolderDict, @@ -337,21 +342,37 @@ class SecretDict(TypedDict): ActivityDict, ] +EventStatus = Literal[ + "pending", + "in_progress", + "finished", + "failed", + "aborted", + "restarted", +] + + +class EnrollEventData(TypedDict): + id: str + dependsOn: str + hash: str + status: EventStatus + class FlatFolderDict(TypedDict): id: str parentId: Optional[str] path: str - parents: List[str] + parents: list[str] name: str label: Optional[str] folderType: str hasTasks: bool hasChildren: bool - taskNames: List[str] + taskNames: list[str] status: str - attrib: Dict[str, Any] - ownAttrib: List[str] + attrib: dict[str, Any] + ownAttrib: list[str] updatedAt: str @@ -362,14 +383,14 @@ class ProjectHierarchyItemDict(TypedDict): status: str folderType: str hasTasks: bool - taskNames: List[str] - parents: List[str] + taskNames: list[str] + parents: list[str] parentId: Optional[str] - children: List["ProjectHierarchyItemDict"] + children: list["ProjectHierarchyItemDict"] class ProjectHierarchyDict(TypedDict): - hierarchy: List[ProjectHierarchyItemDict] + hierarchy: list[ProjectHierarchyItemDict] class ProductTypeDict(TypedDict): @@ -399,7 +420,7 @@ class ActionManifestDict(TypedDict): icon: Optional[IconDefType] adminOnly: bool managerOnly: bool - configFields: List[Dict[str, Any]] + configFields: list[dict[str, Any]] featured: bool addonName: str addonVersion: str @@ -442,7 +463,7 @@ class ActionQueryPayload(BaseActionPayload): class ActionFormPayload(BaseActionPayload): title: str - fields: List[Dict[str, Any]] + fields: list[dict[str, Any]] submit_label: str submit_icon: str cancel_label: str @@ -469,8 +490,8 @@ class ActionTriggerResponse(TypedDict): class ActionTakeResponse(TypedDict): eventId: str actionIdentifier: str - args: List[str] - context: Dict[str, Any] + args: list[str] + context: dict[str, Any] addonName: str addonVersion: str variant: str @@ -480,10 +501,10 @@ class ActionTakeResponse(TypedDict): class ActionConfigResponse(TypedDict): projectName: str entityType: str - entitySubtypes: List[str] - entityIds: List[str] - formData: Dict[str, Any] - value: Dict[str, Any] + entitySubtypes: list[str] + entityIds: list[str] + formData: dict[str, Any] + value: dict[str, Any] StreamType = Union[io.BytesIO, BinaryIO] @@ -491,4 +512,4 @@ class ActionConfigResponse(TypedDict): class EntityListAttributeDefinitionDict(TypedDict): name: str - data: Dict[str, Any] + data: dict[str, Any] diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 6eb1941c7..7f04d64cd 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1,11 +1,15 @@ +from __future__ import annotations + import os import re import datetime +import copy import uuid import string import platform import traceback import collections +import itertools from urllib.parse import urlparse, urlencode import typing from typing import Optional, Dict, Set, Any, Iterable @@ -19,7 +23,19 @@ DEFAULT_VARIANT_ENV_KEY, SITE_ID_ENV_KEY, ) -from .exceptions import UrlError +from .exceptions import ( + UrlError, + ServerError, + UnauthorizedError, + HTTPRequestError, + RequestsJSONDecodeError, +) + +try: + from http import HTTPStatus +except ImportError: + HTTPStatus = None + if typing.TYPE_CHECKING: from typing import Union @@ -30,6 +46,8 @@ SLUGIFY_WHITELIST = string.ascii_letters + string.digits SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_=" +PatternType = type(re.compile("")) + RepresentationParents = collections.namedtuple( "RepresentationParents", ("version", "product", "folder", "project") @@ -62,6 +80,190 @@ def parse_value(cls, value, default=None): return default +class RequestType: + def __init__(self, name: str): + self.name: str = name + + def __hash__(self): + return self.name.__hash__() + + +class RequestTypes: + get = RequestType("GET") + post = RequestType("POST") + put = RequestType("PUT") + patch = RequestType("PATCH") + delete = RequestType("DELETE") + + +def _get_description(response): + if HTTPStatus is None: + return str(response.orig_response) + return HTTPStatus(response.status).description + + +class RestApiResponse(object): + """API Response.""" + + def __init__(self, response, data=None): + if response is None: + status_code = 500 + else: + status_code = response.status_code + self._response = response + self.status = status_code + self._data = data + + @property + def text(self): + if self._response is None: + return self.detail + return self._response.text + + @property + def orig_response(self): + return self._response + + @property + def headers(self): + if self._response is None: + return {} + return self._response.headers + + @property + def data(self): + if self._data is None: + try: + self._data = self.orig_response.json() + except RequestsJSONDecodeError: + self._data = {} + return self._data + + @property + def content(self): + if self._response is None: + return b"" + return self._response.content + + @property + def content_type(self) -> Optional[str]: + return self.headers.get("Content-Type") + + @property + def detail(self): + detail = self.get("detail") + if detail: + return detail + return _get_description(self) + + @property + def status_code(self) -> int: + return self.status + + @property + def ok(self) -> bool: + if self._response is not None: + return self._response.ok + return False + + def raise_for_status(self, message=None): + if self._response is None: + if self._data and self._data.get("detail"): + raise ServerError(self._data["detail"]) + raise ValueError("Response is not available.") + + if self.status_code == 401: + raise UnauthorizedError("Missing or invalid authentication token") + try: + self._response.raise_for_status() + except requests.exceptions.HTTPError as exc: + if message is None: + message = str(exc) + raise HTTPRequestError(message, exc.response) + + def __enter__(self, *args, **kwargs): + return self._response.__enter__(*args, **kwargs) + + def __contains__(self, key): + return key in self.data + + def __repr__(self): + return f"<{self.__class__.__name__} [{self.status}]>" + + def __len__(self): + return int(200 <= self.status < 400) + + def __bool__(self): + return 200 <= self.status < 400 + + def __getitem__(self, key): + return self.data[key] + + def get(self, key, default=None): + data = self.data + if isinstance(data, dict): + return self.data.get(key, default) + return default + + +def fill_own_attribs(entity: "AnyEntityDict") -> None: + """Fill own attributes. + + Prepare data with own attributes. Prepare data based on a list of + attribute names in 'ownAttrib' and 'attrib'. If is not attribute in + 'ownAttrib' then it's value is set to 'None'. + + This can be used with a project, folder or task entity. All other entities + don't use hierarchical attributes and 'attrib' values are + "real values". + + Args: + entity (dict): Entity dictionary. + + """ + if not entity or not entity.get("attrib"): + return + + attributes = entity.get("ownAttrib") + if attributes is None: + return + attributes = set(attributes) + + own_attrib = {} + entity["ownAttrib"] = own_attrib + + for key, value in entity["attrib"].items(): + if key not in attributes: + own_attrib[key] = None + else: + own_attrib[key] = copy.deepcopy(value) + + +def _convert_list_filter_value(value: Any) -> Optional[list[Any]]: + if value is None: + return None + + if isinstance(value, PatternType): + return [value.pattern] + + if isinstance(value, (int, float, str, bool)): + return [value] + return list(set(value)) + + +def prepare_list_filters( + output: dict[str, Any], *args: tuple[str, Any], **kwargs: Any +) -> bool: + for key, value in itertools.chain(args, kwargs.items()): + value = _convert_list_filter_value(value) + if value is None: + continue + if not value: + return False + output[key] = value + return True + + def get_default_timeout() -> float: """Default value for requests timeout.