From 524b131cb17695a269bfc344d17b4c6315652e3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:20:40 +0200 Subject: [PATCH 1/5] Add methods for endpoints missing from AYON server 1.16.8 Cover REST endpoints of server 1.16.8 that had no wrapper: - Users: create/update/delete/rename user, password, access groups, sessions, api keys, permissions; access groups; teams; project users. - Addon settings: set studio/project settings, studio/project overrides and raw overrides, site settings, settings migration. - Projects: anatomy presets management, project anatomy, rename, stats, tags, project bundle, root overrides, entity URIs, URI resolve, project file info; folder search and entity grouping. - Services, hosts, sites, restart required, server config schema and patch, enums, bulk event delete. - Reviewables listing/sorting/update, task assignment, entity list materialize/order, entity and project thumbnails. Co-Authored-By: Claude Opus 5.5 --- automated_api.py | 8 + ayon_api/__init__.py | 182 + ayon_api/_api.py | 9423 ++++++++++++++--------- ayon_api/_api_helpers/__init__.py | 8 + ayon_api/_api_helpers/bundles_addons.py | 544 +- ayon_api/_api_helpers/events.py | 32 + ayon_api/_api_helpers/folders.py | 88 + ayon_api/_api_helpers/lists.py | 37 + ayon_api/_api_helpers/projects.py | 389 + ayon_api/_api_helpers/reviewables.py | 229 + ayon_api/_api_helpers/services.py | 128 + ayon_api/_api_helpers/system.py | 201 + ayon_api/_api_helpers/tasks.py | 31 +- ayon_api/_api_helpers/thumbnails.py | 170 + ayon_api/_api_helpers/users.py | 592 ++ ayon_api/server_api.py | 8 + ayon_api/typing.py | 7 + 17 files changed, 8537 insertions(+), 3540 deletions(-) create mode 100644 ayon_api/_api_helpers/reviewables.py create mode 100644 ayon_api/_api_helpers/services.py create mode 100644 ayon_api/_api_helpers/system.py create mode 100644 ayon_api/_api_helpers/users.py diff --git a/automated_api.py b/automated_api.py index c5289fe4b..761f15b51 100644 --- a/automated_api.py +++ b/automated_api.py @@ -349,6 +349,10 @@ def prepare_api_functions(api_globals): ActionsAPI, LinksAPI, ListsAPI, + UsersAPI, + ServicesAPI, + SystemAPI, + ReviewablesAPI, ) functions = [] @@ -371,6 +375,10 @@ def prepare_api_functions(api_globals): _items.extend(LinksAPI.__dict__.items()) _items.extend(ListsAPI.__dict__.items()) _items.extend(ThumbnailsAPI.__dict__.items()) + _items.extend(UsersAPI.__dict__.items()) + _items.extend(ServicesAPI.__dict__.items()) + _items.extend(SystemAPI.__dict__.items()) + _items.extend(ReviewablesAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 7d700ce99..cd11f40b3 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -159,12 +159,28 @@ get_addons_studio_settings, get_addons_project_settings, get_addons_settings, + set_addon_studio_settings, + set_addon_project_settings, + get_addon_studio_overrides, + modify_addon_studio_overrides, + delete_addon_studio_overrides, + get_addon_project_overrides, + modify_addon_project_overrides, + delete_addon_project_overrides, + get_addon_studio_raw_overrides, + set_addon_studio_raw_overrides, + get_addon_project_raw_overrides, + set_addon_project_raw_overrides, + set_addon_site_settings, + migrate_addon_settings, + migrate_settings_by_bundle, get_event, get_events, update_event, dispatch_event, create_event, delete_event, + delete_events, enroll_event_job, get_attributes_schema, reset_attributes_schema, @@ -180,6 +196,12 @@ get_project_anatomy_preset, get_built_in_anatomy_preset, get_build_in_anatomy_preset, + get_anatomy_schema, + save_project_anatomy_preset, + rename_project_anatomy_preset, + delete_project_anatomy_preset, + set_primary_anatomy_preset, + unset_primary_anatomy_preset, get_rest_project, get_rest_projects, get_rest_projects_list, @@ -189,6 +211,17 @@ create_project, update_project, delete_project, + rename_project, + get_project_anatomy, + set_project_anatomy, + get_project_stats, + get_project_tags, + get_project_bundle, + set_project_bundle, + unset_project_bundle, + get_project_entity_uris, + resolve_uris, + get_project_file_info, get_raw_project_folders, get_project_folders, create_project_folder, @@ -199,6 +232,7 @@ get_project_root_overrides, get_project_roots_by_site, get_project_root_overrides_by_site_id, + set_project_root_overrides, get_project_roots_for_site, get_project_roots_by_site_id, get_project_roots_by_platform, @@ -211,6 +245,8 @@ get_folder_by_path, get_folder_by_name, get_folder_ids_with_products, + search_folders, + get_entity_groups, create_folder, update_folder, delete_folder, @@ -224,6 +260,7 @@ create_task, update_task, delete_task, + assign_users_to_task, get_rest_product, get_products, get_product_by_id, @@ -310,6 +347,8 @@ update_entity_list_folder, delete_entity_list_folder, set_entity_list_folders_order, + materialize_entity_list, + set_entity_list_items_order, get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, @@ -320,6 +359,58 @@ create_thumbnail_with_stream, update_thumbnail, update_thumbnail_from_stream, + set_entity_thumbnail, + set_entity_thumbnail_from_stream, + get_project_thumbnail, + set_project_thumbnail, + set_project_thumbnail_from_stream, + create_user, + update_user, + delete_user, + rename_user, + change_password, + set_user_access_groups, + get_user_sessions, + delete_user_session, + get_user_api_keys, + create_user_api_key, + update_user_api_key, + delete_user_api_key, + get_user_permissions, + get_access_groups, + get_access_group_schema, + get_access_group, + save_access_group, + delete_access_group, + set_projects_access, + get_teams, + update_teams, + save_team, + delete_team, + save_team_member, + delete_team_member, + get_project_users, + set_project_user_access_groups, + get_services, + spawn_service, + update_service, + delete_service, + get_hosts, + get_sites, + delete_site, + get_restart_required, + set_restart_required, + get_server_config_schema, + update_server_config, + get_enums, + get_enum, + get_reviewables, + get_version_reviewables, + get_product_reviewables, + get_folder_reviewables, + get_task_reviewables, + sort_version_reviewables, + update_reviewable, ) @@ -482,12 +573,28 @@ "get_addons_studio_settings", "get_addons_project_settings", "get_addons_settings", + "set_addon_studio_settings", + "set_addon_project_settings", + "get_addon_studio_overrides", + "modify_addon_studio_overrides", + "delete_addon_studio_overrides", + "get_addon_project_overrides", + "modify_addon_project_overrides", + "delete_addon_project_overrides", + "get_addon_studio_raw_overrides", + "set_addon_studio_raw_overrides", + "get_addon_project_raw_overrides", + "set_addon_project_raw_overrides", + "set_addon_site_settings", + "migrate_addon_settings", + "migrate_settings_by_bundle", "get_event", "get_events", "update_event", "dispatch_event", "create_event", "delete_event", + "delete_events", "enroll_event_job", "get_attributes_schema", "reset_attributes_schema", @@ -503,6 +610,12 @@ "get_project_anatomy_preset", "get_built_in_anatomy_preset", "get_build_in_anatomy_preset", + "get_anatomy_schema", + "save_project_anatomy_preset", + "rename_project_anatomy_preset", + "delete_project_anatomy_preset", + "set_primary_anatomy_preset", + "unset_primary_anatomy_preset", "get_rest_project", "get_rest_projects", "get_rest_projects_list", @@ -512,6 +625,17 @@ "create_project", "update_project", "delete_project", + "rename_project", + "get_project_anatomy", + "set_project_anatomy", + "get_project_stats", + "get_project_tags", + "get_project_bundle", + "set_project_bundle", + "unset_project_bundle", + "get_project_entity_uris", + "resolve_uris", + "get_project_file_info", "get_raw_project_folders", "get_project_folders", "create_project_folder", @@ -522,6 +646,7 @@ "get_project_root_overrides", "get_project_roots_by_site", "get_project_root_overrides_by_site_id", + "set_project_root_overrides", "get_project_roots_for_site", "get_project_roots_by_site_id", "get_project_roots_by_platform", @@ -534,6 +659,8 @@ "get_folder_by_path", "get_folder_by_name", "get_folder_ids_with_products", + "search_folders", + "get_entity_groups", "create_folder", "update_folder", "delete_folder", @@ -547,6 +674,7 @@ "create_task", "update_task", "delete_task", + "assign_users_to_task", "get_rest_product", "get_products", "get_product_by_id", @@ -633,6 +761,8 @@ "update_entity_list_folder", "delete_entity_list_folder", "set_entity_list_folders_order", + "materialize_entity_list", + "set_entity_list_items_order", "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", @@ -643,4 +773,56 @@ "create_thumbnail_with_stream", "update_thumbnail", "update_thumbnail_from_stream", + "set_entity_thumbnail", + "set_entity_thumbnail_from_stream", + "get_project_thumbnail", + "set_project_thumbnail", + "set_project_thumbnail_from_stream", + "create_user", + "update_user", + "delete_user", + "rename_user", + "change_password", + "set_user_access_groups", + "get_user_sessions", + "delete_user_session", + "get_user_api_keys", + "create_user_api_key", + "update_user_api_key", + "delete_user_api_key", + "get_user_permissions", + "get_access_groups", + "get_access_group_schema", + "get_access_group", + "save_access_group", + "delete_access_group", + "set_projects_access", + "get_teams", + "update_teams", + "save_team", + "delete_team", + "save_team_member", + "delete_team_member", + "get_project_users", + "set_project_user_access_groups", + "get_services", + "spawn_service", + "update_service", + "delete_service", + "get_hosts", + "get_sites", + "delete_site", + "get_restart_required", + "set_restart_required", + "get_server_config_schema", + "update_server_config", + "get_enums", + "get_enum", + "get_reviewables", + "get_version_reviewables", + "get_product_reviewables", + "get_folder_reviewables", + "get_task_reviewables", + "sort_version_reviewables", + "update_reviewable", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 444f085af..32f1f9792 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -53,6 +53,7 @@ EntityListEntityType, EntityListItemMode, EntityListScope, + ReviewablesEntityType, BackgroundOperationTask, LinkDirection, CreateLinkData, @@ -3219,6 +3220,8 @@ def get_addon_settings_schema( addon_name: str, addon_version: str, project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, ) -> dict[str, Any]: """Sudio/Project settings schema of an addon. @@ -3230,6 +3233,10 @@ def get_addon_settings_schema( addon_version (str): Version of addon. project_name (Optional[str]): Schema for specific project or default studio schemas. + variant (Optional[str]): Settings variant used as context for + the schema. Server uses 'production' if not passed. + site_id (Optional[str]): Site id used as context for project + schema. Ignored if 'project_name' is not passed. Returns: dict[str, Any]: Schema of studio/project settings. @@ -3240,6 +3247,8 @@ def get_addon_settings_schema( addon_name=addon_name, addon_version=addon_version, project_name=project_name, + variant=variant, + site_id=site_id, ) @@ -3614,442 +3623,950 @@ def get_addons_settings( ) -def get_event( - event_id: str, -) -> Optional[dict[str, Any]]: - """Query full event data by id. +def set_addon_studio_settings( + addon_name: str, + addon_version: str, + settings: dict[str, Any], + variant: Optional[str] = None, +) -> None: + """Set studio settings of an addon. - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. + Server calculates studio overrides from passed settings values. + Explicit (un)pinning of fields can be done with + '__pinned_fields__' and '__unpinned_fields__' keys in settings. Args: - event_id (str): Event id. - - Returns: - dict[str, Any]: Full event data. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + settings (dict[str, Any]): Full settings values. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. """ con = get_server_api_connection() - return con.get_event( - event_id=event_id, + return con.set_addon_studio_settings( + addon_name=addon_name, + addon_version=addon_version, + settings=settings, + variant=variant, ) -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, - text_filter: Optional[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, - first: Optional[int] = None, - last: Optional[int] = None, - states: Optional[Iterable[str]] = None, -) -> Generator[dict[str, Any], None, None]: - """Get events from server with filtering options. +def set_addon_project_settings( + addon_name: str, + addon_version: str, + project_name: str, + settings: dict[str, Any], + variant: Optional[str] = None, + site_id: Optional[str] = None, +) -> None: + """Set project settings of an addon. - Notes: - Not all event happen on a project. + Server calculates project overrides from passed settings values. + If 'site_id' is passed then project site overrides of current user + are set instead. 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. - text_filter (Optional[str]): Filtering by text in event payload. - 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. - first (Optional[int]): Get first n events. - last (Optional[int]): Get last n events. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + settings (dict[str, Any]): Full settings values. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which are site overrides + set. Project overrides are set if not passed. + + """ + con = get_server_api_connection() + return con.set_addon_project_settings( + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + settings=settings, + variant=variant, + site_id=site_id, + ) + + +def get_addon_studio_overrides( + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + as_version: Optional[str] = None, +) -> dict[str, Any]: + """Get studio overrides of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + as_version (Optional[str]): Use overrides of different addon + version. Returns: - Generator[dict[str, Any]]: Available events matching filters. + dict[str, Any]: Overrides information by settings path. """ con = get_server_api_connection() - return con.get_events( - topics=topics, - event_ids=event_ids, - project_names=project_names, - statuses=statuses, - users=users, - text_filter=text_filter, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields, - limit=limit, - order=order, - first=first, - last=last, - states=states, + return con.get_addon_studio_overrides( + addon_name=addon_name, + addon_version=addon_version, + variant=variant, + as_version=as_version, ) -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, +def modify_addon_studio_overrides( + addon_name: str, + addon_version: str, + action: Literal["delete", "pin"], + path: list[str], + variant: Optional[str] = None, ) -> None: - """Update event data. + """Modify studio override of an addon on a settings path. 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. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + action (Literal["delete", "pin"]): Remove override or pin + current value as override. + path (list[str]): Path to settings key. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. """ 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, + return con.modify_addon_studio_overrides( + addon_name=addon_name, + addon_version=addon_version, + action=action, + path=path, + variant=variant, ) -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. +def delete_addon_studio_overrides( + addon_name: str, + addon_version: str, + variant: Optional[str] = None, +) -> None: + """Delete all studio overrides of an addon. 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. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + """ + con = get_server_api_connection() + return con.delete_addon_studio_overrides( + addon_name=addon_name, + addon_version=addon_version, + variant=variant, + ) + + +def get_addon_project_overrides( + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + as_version: Optional[str] = None, +) -> dict[str, Any]: + """Get project overrides of an addon. + + Output contains studio and project overrides, and site overrides + if 'site_id' is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to include site overrides. + as_version (Optional[str]): Use overrides of different addon + version. Returns: - RestApiResponse: Response from server. + dict[str, Any]: Overrides information by settings path. """ con = get_server_api_connection() - return con.dispatch_event( - topic=topic, - sender=sender, - event_hash=event_hash, + return con.get_addon_project_overrides( + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + variant=variant, + site_id=site_id, + as_version=as_version, ) -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. +def modify_addon_project_overrides( + addon_name: str, + addon_version: str, + project_name: str, + action: Literal["delete", "pin"], + path: list[str], + variant: Optional[str] = None, + site_id: Optional[str] = None, +) -> None: + """Modify project override of an addon on a settings path. - Returns: - str: Event id. + Project site override of current user is modified if 'site_id' + is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + action (Literal["delete", "pin"]): Remove override or pin + current value as override. + path (list[str]): Path to settings key. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to modify site overrides. """ con = get_server_api_connection() - return con.create_event( - topic=topic, - sender=sender, - event_hash=event_hash, + return con.modify_addon_project_overrides( + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + action=action, + path=path, + variant=variant, + site_id=site_id, ) -def delete_event( - event_id: str, +def delete_addon_project_overrides( + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, ) -> None: - """Delete event by id. + """Delete all project overrides of an addon. - Supported since AYON server 1.6.0. + Project site overrides of current user are deleted if 'site_id' + is passed. Args: - event_id (str): Event id. - - Returns: - RestApiResponse: Response from server. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to delete site overrides. """ con = get_server_api_connection() - return con.delete_event( - event_id=event_id, + return con.delete_addon_project_overrides( + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + variant=variant, + site_id=site_id, ) -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. - - 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. +def get_addon_studio_raw_overrides( + addon_name: str, + addon_version: str, + variant: Optional[str] = None, +) -> dict[str, Any]: + """Get raw studio overrides of an addon. - 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' + Raw overrides are stored data without any processing. Only admins + can access them. 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. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. Returns: - Optional[EnrollEventData]: None if there is no event matching - filters. Created event with 'target_topic'. + dict[str, Any]: Raw overrides data. """ 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.get_addon_studio_raw_overrides( + addon_name=addon_name, + addon_version=addon_version, + variant=variant, ) -def get_attributes_schema( - use_cache: bool = True, -) -> AttributesSchemaDict: - con = get_server_api_connection() - return con.get_attributes_schema( - use_cache=use_cache, - ) - +def set_addon_studio_raw_overrides( + addon_name: str, + addon_version: str, + overrides: dict[str, Any], + variant: Optional[str] = None, +) -> None: + """Set raw studio overrides of an addon. -def reset_attributes_schema() -> None: - """Reset attributes schema cache. + Warnings: + Data are stored without any validation. Only admins can use + this. Use with caution. - DEPRECATED: - Use 'reset_attributes_cache' instead. + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + overrides (dict[str, Any]): Raw overrides data. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. """ con = get_server_api_connection() - return con.reset_attributes_schema() + return con.set_addon_studio_raw_overrides( + addon_name=addon_name, + addon_version=addon_version, + overrides=overrides, + variant=variant, + ) -def reset_attributes_cache() -> None: - con = get_server_api_connection() - return con.reset_attributes_cache() +def get_addon_project_raw_overrides( + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, +) -> dict[str, Any]: + """Get raw project overrides of an addon. + Raw overrides are stored data without any processing. Project + overrides are accessible only by admins, project site overrides + of current user are returned if 'site_id' is passed. -def set_attributes_cache_timeout( - timeout: int, -) -> None: - con = get_server_api_connection() - return con.set_attributes_cache_timeout( - timeout=timeout, - ) + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to get site overrides. + Returns: + dict[str, Any]: Raw overrides data. -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.set_attribute_config( - attribute_name=attribute_name, - data=data, - scope=scope, - position=position, - builtin=builtin, + return con.get_addon_project_raw_overrides( + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + variant=variant, + site_id=site_id, ) -def delete_attribute_config( - attribute_name: str, +def set_addon_project_raw_overrides( + addon_name: str, + addon_version: str, + project_name: str, + overrides: dict[str, Any], + variant: Optional[str] = None, + site_id: Optional[str] = None, ) -> None: - """Remove attribute from server. + """Set raw project overrides of an addon. - This can't be un-done, please use carefully. + Project site overrides of current user are set if 'site_id' + is passed. + + Warnings: + Data are stored without any validation. Use with caution. Args: - attribute_name (str): Name of attribute to remove. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + overrides (dict[str, Any]): Raw overrides data. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to set site overrides. """ con = get_server_api_connection() - return con.delete_attribute_config( - attribute_name=attribute_name, + return con.set_addon_project_raw_overrides( + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + overrides=overrides, + variant=variant, + site_id=site_id, ) -def remove_attribute_config( - attribute_name: str, +def set_addon_site_settings( + addon_name: str, + addon_version: str, + settings: dict[str, Any], + site_id: Optional[str] = None, ) -> None: - """Remove attribute from server. - - DEPRECATED: Use 'delete_attribute_config' instead. + """Set site settings of an addon for current user. Args: - attribute_name (str): Name of attribute to remove. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + settings (dict[str, Any]): Site settings values. + site_id (Optional[str]): Site id for which are settings set. + Using 'site_id' attribute if not passed. + + Raises: + ValueError: If site id is not available. """ con = get_server_api_connection() - return con.remove_attribute_config( - attribute_name=attribute_name, + return con.set_addon_site_settings( + addon_name=addon_name, + addon_version=addon_version, + settings=settings, + site_id=site_id, + ) + + +def migrate_addon_settings( + addon_name: str, + source_version: str, + target_version: str, + source_variant: Optional[str] = None, + target_variant: Optional[str] = None, + with_projects: Optional[bool] = None, +) -> dict[str, Any]: + """Migrate settings of an addon between addon versions. + + Args: + addon_name (str): Name of addon. + source_version (str): Source addon version. + target_version (str): Target addon version. + source_variant (Optional[str]): Source settings variant. Server + uses 'production' if not passed. + target_variant (Optional[str]): Target settings variant. Server + uses 'production' if not passed. + with_projects (Optional[bool]): Migrate also project settings. + Server uses 'True' if not passed. + + Returns: + dict[str, Any]: Server response with 'events' created during + the migration. + + """ + con = get_server_api_connection() + return con.migrate_addon_settings( + addon_name=addon_name, + source_version=source_version, + target_version=target_version, + source_variant=source_variant, + target_variant=target_variant, + with_projects=with_projects, + ) + + +def migrate_settings_by_bundle( + source_bundle: str, + target_bundle: str, + source_variant: str, + target_variant: str, + with_projects: bool = True, +) -> None: + """Migrate settings of addons from source to target bundle. + + Settings of addons present in both bundles are migrated. + + Args: + source_bundle (str): Source bundle name. + target_bundle (str): Target bundle name. + source_variant (str): Source settings variant. + target_variant (str): Target settings variant. + with_projects (bool): Migrate also project settings. + + """ + con = get_server_api_connection() + return con.migrate_settings_by_bundle( + source_bundle=source_bundle, + target_bundle=target_bundle, + source_variant=source_variant, + target_variant=target_variant, + with_projects=with_projects, + ) + + +def get_event( + 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. + + """ + con = get_server_api_connection() + return con.get_event( + event_id=event_id, + ) + + +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, + text_filter: Optional[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, + first: Optional[int] = None, + last: Optional[int] = 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. + text_filter (Optional[str]): Filtering by text in event payload. + 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. + first (Optional[int]): Get first n events. + last (Optional[int]): Get last n events. + 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, + text_filter=text_filter, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields, + limit=limit, + order=order, + first=first, + last=last, + 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: + event_id (str): Event id. + + Returns: + RestApiResponse: Response from server. + + """ + con = get_server_api_connection() + return con.delete_event( + event_id=event_id, + ) + + +def delete_events( + events_filter: EventFilter, +) -> None: + """Delete events matching filter. + + Requires administrator privileges. All events are deleted if + filter does not contain any conditions. + + Example filter:: + + { + "conditions": [ + { + "key": "topic", + "value": "my.topic", + "operator": "eq", + } + ], + "operator": "and", + } + + Args: + events_filter (EventFilter): Filtering conditions to find events + to delete. For more technical specifications look to server + backed 'ayon_server.sqlfilter.QueryFilter'. + + """ + con = get_server_api_connection() + return con.delete_events( + events_filter=events_filter, + ) + + +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. + + 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'. + + """ + 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, + ) + + +def get_attributes_schema( + use_cache: bool = True, +) -> AttributesSchemaDict: + con = get_server_api_connection() + return con.get_attributes_schema( + use_cache=use_cache, + ) + + +def reset_attributes_schema() -> None: + """Reset attributes schema cache. + + DEPRECATED: + Use 'reset_attributes_cache' instead. + + """ + con = get_server_api_connection() + return con.reset_attributes_schema() + + +def reset_attributes_cache() -> None: + con = get_server_api_connection() + return con.reset_attributes_cache() + + +def set_attributes_cache_timeout( + timeout: int, +) -> None: + con = get_server_api_connection() + return con.set_attributes_cache_timeout( + timeout=timeout, + ) + + +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.set_attribute_config( + attribute_name=attribute_name, + data=data, + scope=scope, + position=position, + builtin=builtin, + ) + + +def delete_attribute_config( + 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. + + """ + con = get_server_api_connection() + return con.delete_attribute_config( + attribute_name=attribute_name, + ) + + +def remove_attribute_config( + attribute_name: str, +) -> None: + """Remove attribute from server. + + DEPRECATED: Use 'delete_attribute_config' instead. + + Args: + attribute_name (str): Name of attribute to remove. + + """ + con = get_server_api_connection() + return con.remove_attribute_config( + attribute_name=attribute_name, ) @@ -4058,1114 +4575,3399 @@ def get_attributes_for_type( ) -> dict[str, AttributeSchemaDataDict]: """Get attribute schemas available for an entity type. - Example:: + 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. + + """ + con = get_server_api_connection() + return con.get_attributes_for_type( + entity_type=entity_type, + ) + + +def get_attributes_fields_for_type( + entity_type: AttributeScope, +) -> set[str]: + """Prepare attribute fields for entity type. + + DEPRECATED: Field 'attrib' is marked as deprecated and should not be + used for GraphQL queries. + + Returns: + set[str]: Attributes fields for entity type. + + """ + con = get_server_api_connection() + return con.get_attributes_fields_for_type( + entity_type=entity_type, + ) + + +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. + + 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. + + """ + con = get_server_api_connection() + return con.get_project_anatomy_preset( + preset_name=preset_name, + ) + + +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_built_in_anatomy_preset() + + +def get_build_in_anatomy_preset() -> AnatomyPresetDict: + con = get_server_api_connection() + return con.get_build_in_anatomy_preset() + + +def get_anatomy_schema() -> dict[str, Any]: + """Get anatomy JSON schema. + + The schema is used to display the anatomy preset editor form. + + Returns: + dict[str, Any]: Anatomy JSON schema. + + """ + con = get_server_api_connection() + return con.get_anatomy_schema() + + +def save_project_anatomy_preset( + preset_name: str, + preset: AnatomyPresetDict, +) -> None: + """Create or update anatomy preset. + + Built-in preset '__builtin__' and primary preset reference + '__primary__' cannot be updated. + + Args: + preset_name (str): Anatomy preset name. + preset (AnatomyPresetDict): Anatomy preset values. + + """ + con = get_server_api_connection() + return con.save_project_anatomy_preset( + preset_name=preset_name, + preset=preset, + ) + + +def rename_project_anatomy_preset( + preset_name: str, + new_preset_name: str, +) -> None: + """Rename anatomy preset. + + Args: + preset_name (str): Current anatomy preset name. + new_preset_name (str): New anatomy preset name. + + """ + con = get_server_api_connection() + return con.rename_project_anatomy_preset( + preset_name=preset_name, + new_preset_name=new_preset_name, + ) + + +def delete_project_anatomy_preset( + preset_name: str, +) -> None: + """Delete anatomy preset. + + Args: + preset_name (str): Anatomy preset name. + + """ + con = get_server_api_connection() + return con.delete_project_anatomy_preset( + preset_name=preset_name, + ) + + +def set_primary_anatomy_preset( + preset_name: str, +) -> None: + """Set anatomy preset as primary. + + Primary preset is used as default preset for new projects. Use + preset name '_' to unset any primary preset (built-in preset + is used as default). + + Args: + preset_name (str): Anatomy preset name. + + """ + con = get_server_api_connection() + return con.set_primary_anatomy_preset( + preset_name=preset_name, + ) + + +def unset_primary_anatomy_preset( + preset_name: str, +) -> None: + """Unset primary flag of anatomy preset. + + Args: + preset_name (str): Anatomy preset name. + + """ + con = get_server_api_connection() + return con.unset_primary_anatomy_preset( + preset_name=preset_name, + ) + + +def get_rest_project( + 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. + + """ + con = get_server_api_connection() + return con.get_rest_project( + project_name=project_name, + ) + + +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, + include_skeleton: bool = False, +) -> 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. + include_skeleton (bool): Include skeleton projects. + + Returns: + Generator[ProjectDict, None, None]: Available projects. + + """ + con = get_server_api_connection() + return con.get_rest_projects( + active=active, + library=library, + include_skeleton=include_skeleton, + ) + + +def get_rest_projects_list( + active: Optional[bool] = True, + library: Optional[bool] = None, + include_skeleton: bool = False, +) -> list[ProjectListDict]: + """Receive available projects. + + 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. + include_skeleton (bool): Include skeleton projects. + + Returns: + list[ProjectListDict]: List of available projects. + + """ + con = get_server_api_connection() + return con.get_rest_projects_list( + active=active, + library=library, + include_skeleton=include_skeleton, + ) + + +def get_project_names( + active: Optional[bool] = True, + library: Optional[bool] = None, + include_skeleton: bool = False, +) -> 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. + include_skeleton (bool): Include skeleton projects. + + Returns: + list[str]: List of available project names. + + """ + con = get_server_api_connection() + return con.get_project_names( + active=active, + library=library, + include_skeleton=include_skeleton, + ) + + +def get_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, + include_skeleton: bool = False, + 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. + include_skeleton (bool): Include skeleton projects. + 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. + + """ + con = get_server_api_connection() + return con.get_projects( + active=active, + library=library, + include_skeleton=include_skeleton, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_project( + 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. + + """ + con = get_server_api_connection() + return con.get_project( + project_name=project_name, + fields=fields, + own_attributes=own_attributes, + ) + + +def create_project( + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, + data: dict[str, Any] | None = None, + skeleton: bool = False, +) -> 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. + data (dict[str, Any]): Project data. + skeleton (bool): Project is skeleton project. + + Raises: + ValueError: When project name already exists. + + Returns: + ProjectDict: Created project entity. + + """ + con = get_server_api_connection() + return con.create_project( + project_name=project_name, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, + data=data, + skeleton=skeleton, + ) + + +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 delete_project( + 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. + + """ + con = get_server_api_connection() + return con.delete_project( + project_name=project_name, + ) + + +def rename_project( + project_name: str, + new_project_name: str, + new_project_code: Optional[str] = None, +) -> None: + """Rename project. + + Args: + project_name (str): Current project name. + new_project_name (str): New project name. + new_project_code (Optional[str]): New project code. Project code + is unchanged if not passed. + + """ + con = get_server_api_connection() + return con.rename_project( + project_name=project_name, + new_project_name=new_project_name, + new_project_code=new_project_code, + ) + + +def get_project_anatomy( + project_name: str, +) -> AnatomyPresetDict: + """Get project anatomy. + + Args: + project_name (str): Project name. + + Returns: + AnatomyPresetDict: Project anatomy in anatomy preset format. + + """ + con = get_server_api_connection() + return con.get_project_anatomy( + project_name=project_name, + ) + + +def set_project_anatomy( + project_name: str, + anatomy: AnatomyPresetDict, +) -> None: + """Set project anatomy. + + Args: + project_name (str): Project name. + anatomy (AnatomyPresetDict): Anatomy data in anatomy + preset format. + + """ + con = get_server_api_connection() + return con.set_project_anatomy( + project_name=project_name, + anatomy=anatomy, + ) + + +def get_project_stats( + project_name: str, +) -> dict[str, Any]: + """Get project statistics. + + Example output:: + + { + "counts": { + "folders": 10, + "products": 20, + "versions": 30, + "representations": 40, + "tasks": 50 + } + } + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Project statistics. + + """ + con = get_server_api_connection() + return con.get_project_stats( + project_name=project_name, + ) + + +def get_project_tags( + project_name: str, +) -> dict[str, list[str]]: + """Get tags used on entities in project. + + Example output:: + + { + "folders": ["tag1"], + "tasks": [], + "products": [], + "versions": ["tag2"], + "representations": [], + "workfiles": [] + } + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Used tags by entity type (plural). + + """ + con = get_server_api_connection() + return con.get_project_tags( + project_name=project_name, + ) + + +def get_project_bundle( + project_name: str, + variant: str = 'production', +) -> dict[str, Any]: + """Get project bundle information. + + Information contains addon versions used by project, installer + version, dependency packages and available options for each. + + Args: + project_name (str): Project name. + variant (str): Bundle variant 'production' or 'staging'. + + Returns: + dict[str, Any]: Project bundle information. + + """ + con = get_server_api_connection() + return con.get_project_bundle( + project_name=project_name, + variant=variant, + ) + + +def set_project_bundle( + project_name: str, + addons: dict[str, Optional[str]], + installer_version: Optional[str] = None, + dependency_packages: Optional[dict[str, Optional[str]]] = None, + variant: str = 'production', +) -> None: + """Set (freeze) project bundle. + + Args: + project_name (str): Project name. + addons (dict[str, Optional[str]]): Addon versions by addon name. + Use 'None' to disable an addon. + installer_version (Optional[str]): Installer version. + dependency_packages (Optional[dict[str, Optional[str]]]): + Dependency package names by platform name. + variant (str): Bundle variant 'production' or 'staging'. + + """ + con = get_server_api_connection() + return con.set_project_bundle( + project_name=project_name, + addons=addons, + installer_version=installer_version, + dependency_packages=dependency_packages, + variant=variant, + ) + + +def unset_project_bundle( + project_name: str, + variant: str = 'production', +) -> None: + """Unset (unfreeze) project bundle. + + Args: + project_name (str): Project name. + variant (str): Bundle variant 'production' or 'staging'. + + """ + con = get_server_api_connection() + return con.unset_project_bundle( + project_name=project_name, + variant=variant, + ) + + +def get_project_entity_uris( + project_name: str, + entity_type: str, + entity_ids: Iterable[str], +) -> list[dict[str, str]]: + """Get AYON URIs for project entities. + + Example output:: + + [ + { + "id": "0254c370005811ee9a740242ac130004", + "uri": "ayon+entity://demo/assets/hero" + }, + ... + ] + + Args: + project_name (str): Project name. + entity_type (str): Entity type, e.g. 'folder', 'task', + 'product', 'version', 'representation' or 'workfile'. + entity_ids (Iterable[str]): Entity ids. + + Returns: + list[dict[str, str]]: Entity URIs with entity ids. + + """ + con = get_server_api_connection() + return con.get_project_entity_uris( + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + ) + + +def resolve_uris( + uris: Iterable[str], + resolve_roots: bool = False, + path_only: bool = False, + site_id: Optional[str] = None, +) -> list[dict[str, Any]]: + """Resolve AYON URIs to entities. + + Each URI starts with 'ayon://{project_name}/{path}' with optional + query arguments 'product', 'version', 'representation' or + 'task', 'workfile'. + + Example output:: + + [ + { + "uri": "ayon://demo/assets/hero?product=modelMain", + "entities": [ + { + "projectName": "demo", + "folderId": "0254c370005811ee9a740242ac130004", + "productId": "0255ce50005811ee9a740242ac130004", + "target": "product" + } + ] + }, + ... + ] + + Args: + uris (Iterable[str]): AYON URIs to resolve. + resolve_roots (bool): Resolve roots in representation paths. + Requires site id. + path_only (bool): Return only file paths of entities. + site_id (Optional[str]): Site id used to resolve roots. Site id + set on connection is used if not passed. + + Returns: + list[dict[str, Any]]: Resolved URIs. + + """ + con = get_server_api_connection() + return con.resolve_uris( + uris=uris, + resolve_roots=resolve_roots, + path_only=path_only, + site_id=site_id, + ) + + +def get_project_file_info( + project_name: str, + file_id: str, +) -> dict[str, Any]: + """Get information about project file. + + Example output:: + + { + "size": 1024, + "filename": "image.png", + "contentType": "image/png", + "mediaInfo": None + } + + Args: + project_name (str): Project name. + file_id (str): File id. + + Returns: + dict[str, Any]: File information. + + """ + con = get_server_api_connection() + return con.get_project_file_info( + project_name=project_name, + file_id=file_id, + ) + + +def get_raw_project_folders() -> dict[str, Any]: + """Get project folders (raw data).""" + con = get_server_api_connection() + return con.get_raw_project_folders() + + +def get_project_folders() -> list[dict[str, Any]]: + con = get_server_api_connection() + return con.get_project_folders() + + +def create_project_folder( + label: str, + parent_id: str | None = None, + data: dict[str, Any] | None = None, +) -> str: + """Create project folder.""" + con = get_server_api_connection() + return con.create_project_folder( + label=label, + parent_id=parent_id, + data=data, + ) + + +def update_project_folder( + folder_id: str, + label: str | None = None, + parent_id: str | None = None, + data: dict[str, Any] | None = None, +) -> None: + con = get_server_api_connection() + return con.update_project_folder( + folder_id=folder_id, + label=label, + parent_id=parent_id, + data=data, + ) + + +def set_project_folders_order( + folder_ids: list[str], +) -> None: + """Set project folders order.""" + con = get_server_api_connection() + return con.set_project_folders_order( + folder_ids=folder_ids, + ) + + +def assign_projects_to_project_folder( + folder_id: str, + project_names: list[str], +) -> None: + """Assign project folder to project.""" + con = get_server_api_connection() + return con.assign_projects_to_project_folder( + folder_id=folder_id, + project_names=project_names, + ) + + +def delete_project_folder( + folder_id: str, +): + """Delete project folder.""" + con = get_server_api_connection() + return con.delete_project_folder( + folder_id=folder_id, + ) + + +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_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. + + """ + con = get_server_api_connection() + return con.get_project_roots_by_site( + project_name=project_name, + ) + + +def get_project_root_overrides_by_site_id( + 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. + + """ + con = get_server_api_connection() + return con.get_project_root_overrides_by_site_id( + project_name=project_name, + site_id=site_id, + ) + + +def set_project_root_overrides( + project_name: str, + site_id: str, + roots: dict[str, Optional[str]], +) -> None: + """Set root overrides of logged user for a site. + + Only roots defined in project anatomy are stored. Roots with empty + value are removed, and all overrides for the site are removed + if no root value is passed. + + Args: + project_name (str): Project name. + site_id (str): Site id for which overrides are set. + roots (dict[str, Optional[str]]): Root values by root name. + + """ + con = get_server_api_connection() + return con.set_project_root_overrides( + project_name=project_name, + site_id=site_id, + roots=roots, + ) + + +def get_project_roots_for_site( + 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. + + """ + con = get_server_api_connection() + return con.get_project_roots_for_site( + project_name=project_name, + site_id=site_id, + ) + + +def get_project_roots_by_site_id( + 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. + + """ + con = get_server_api_connection() + return con.get_project_roots_by_site_id( + project_name=project_name, + site_id=site_id, + ) + + +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. + + 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. + + """ + con = get_server_api_connection() + return con.get_project_roots_by_platform( + project_name=project_name, + platform_name=platform_name, + ) + + +def get_rest_folder( + project_name: str, + folder_id: str, +) -> Optional[FolderDict]: + con = get_server_api_connection() + return con.get_rest_folder( + project_name=project_name, + folder_id=folder_id, + ) + + +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): Project name. + 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.get_rest_folders( + project_name=project_name, + include_attrib=include_attrib, + ) + + +def get_folders_hierarchy( + 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. + + """ + con = get_server_api_connection() + return con.get_folders_hierarchy( + project_name=project_name, + search_string=search_string, + folder_types=folder_types, + ) + + +def get_folders_rest( + 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. + + """ + con = get_server_api_connection() + return con.get_folders_rest( + project_name=project_name, + include_attrib=include_attrib, + ) + + +def get_folders( + 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, + filters: Optional[AdvancedFilterDict] = 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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. + + """ + con = get_server_api_connection() + return con.get_folders( + 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, + filters=filters, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_folder_by_id( + 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. + + """ + con = get_server_api_connection() + return con.get_folder_by_id( + project_name=project_name, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_folder_by_path( + 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. + + """ + con = get_server_api_connection() + return con.get_folder_by_path( + project_name=project_name, + folder_path=folder_path, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_folder_by_name( + 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. + + """ + con = get_server_api_connection() + return con.get_folder_by_name( + project_name=project_name, + folder_name=folder_name, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_folder_ids_with_products( + 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. + + """ + con = get_server_api_connection() + return con.get_folder_ids_with_products( + project_name=project_name, + folder_ids=folder_ids, + ) + + +def search_folders( + project_name: str, + search: Optional[str] = None, + folder_search: Optional[str] = None, + folder_filter: Optional[AdvancedFilterDict] = None, + task_search: Optional[str] = None, + task_filter: Optional[AdvancedFilterDict] = None, +) -> list[str]: + """Search folders using text search and filters. + + Returns ids of folders matching the folder conditions, or containing + tasks matching the task conditions. + + Args: + project_name (str): Project name. + search (Optional[str]): Fulltext search applied to both + folders and tasks. + folder_search (Optional[str]): Fulltext search applied + to folders. + folder_filter (Optional[AdvancedFilterDict]): Filter applied + to folders. + task_search (Optional[str]): Fulltext search applied to tasks. + task_filter (Optional[AdvancedFilterDict]): Filter applied + to tasks. + + Returns: + list[str]: Matching folder ids. + + """ + con = get_server_api_connection() + return con.search_folders( + project_name=project_name, + search=search, + folder_search=folder_search, + folder_filter=folder_filter, + task_search=task_search, + task_filter=task_filter, + ) + + +def get_entity_groups( + project_name: str, + entity_type: str, + grouping_key: str, + empty: bool = False, +) -> dict[str, Any]: + """Get groups of entities based on grouping key. + + Example output:: + + { + "groups": [ + {"value": "john.doe", "count": 42, ...}, + ... + ], + "key": "assignees", + "entityType": "task" + } + + Args: + project_name (str): Project name. + entity_type (str): Entity type, e.g. 'folder' or 'task'. + grouping_key (str): Grouping key, e.g. 'status', 'tags', + 'taskType', 'folderType', 'productType', 'productBaseType', + 'assignees' (tasks only) or 'attrib.{attribute name}'. + empty (bool): Include empty groups. + + Returns: + dict[str, Any]: Entity groups data. + + """ + con = get_server_api_connection() + return con.get_entity_groups( + project_name=project_name, + entity_type=entity_type, + grouping_key=grouping_key, + empty=empty, + ) + + +def create_folder( + 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. + + """ + con = get_server_api_connection() + return con.create_folder( + 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, + ) + + +def update_folder( + 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. + + """ + 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, + ) + + +def delete_folder( + 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. + + """ + con = get_server_api_connection() + 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_tasks( + 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, + filters: Optional[AdvancedFilterDict] = None, + 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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. + + """ + con = get_server_api_connection() + return con.get_tasks( + 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, + statuses=statuses, + tags=tags, + active=active, + filters=filters, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_task_by_name( + 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. + + """ + con = get_server_api_connection() + return con.get_task_by_name( + project_name=project_name, + folder_id=folder_id, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_task_by_id( + 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. + + """ + con = get_server_api_connection() + return con.get_task_by_id( + project_name=project_name, + task_id=task_id, + fields=fields, + own_attributes=own_attributes, + ) + + +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, + filters: Optional[AdvancedFilterDict] = None, + 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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. + + """ + con = get_server_api_connection() + 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, + filters=filters, + fields=fields, + own_attributes=own_attributes, + ) + + +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, + 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, + ) + + +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 (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. + + """ + con = get_server_api_connection() + return con.get_task_by_folder_path( + project_name=project_name, + folder_path=folder_path, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, + ) + + +def create_task( + 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. + + """ + con = get_server_api_connection() + return con.create_task( + project_name=project_name, + name=name, + task_type=task_type, + folder_id=folder_id, + label=label, + assignees=assignees, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + task_id=task_id, + ) + + +def update_task( + 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. + + """ + con = get_server_api_connection() + return con.update_task( + project_name=project_name, + task_id=task_id, + name=name, + task_type=task_type, + folder_id=folder_id, + label=label, + assignees=assignees, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + ) + - ``` - # 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 - } - ``` +def delete_task( + project_name: str, + task_id: str, +) -> None: + """Delete task. Args: - entity_type (str): Entity type for which should be attributes - received. + project_name (str): Project name. + task_id (str): Task id to delete. + + """ + con = get_server_api_connection() + return con.delete_task( + project_name=project_name, + task_id=task_id, + ) - Returns: - dict[str, dict[str, Any]]: Attribute schemas that are available - for entered entity type. + +def assign_users_to_task( + project_name: str, + task_id: str, + users: Iterable[str], + mode: Literal["add", "remove", "set"] = 'add', +) -> None: + """Change users assigned to a task. + + Notes: + Users without manager role can only assign or unassign + themselves. + + Args: + project_name (str): Project name. + task_id (str): Task id. + users (Iterable[str]): User names. + mode (Literal["add", "remove", "set"]): How passed users are + applied. 'add' adds users to current assignees, 'remove' + removes them and 'set' replaces assignees with passed users. """ con = get_server_api_connection() - return con.get_attributes_for_type( - entity_type=entity_type, + return con.assign_users_to_task( + project_name=project_name, + task_id=task_id, + users=users, + mode=mode, ) -def get_attributes_fields_for_type( - entity_type: AttributeScope, -) -> set[str]: - """Prepare attribute fields for entity type. +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, + product_id=product_id, + ) - DEPRECATED: Field 'attrib' is marked as deprecated and should not be - used for GraphQL queries. + +def get_products( + 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_base_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, + filters: Optional[AdvancedFilterDict] = None, + 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_base_types (Optional[Iterable[str]]): Product base 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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: - set[str]: Attributes fields for entity type. + Generator[ProductDict, None, None]: Queried product entities. """ con = get_server_api_connection() - return con.get_attributes_fields_for_type( - entity_type=entity_type, + return con.get_products( + project_name=project_name, + product_ids=product_ids, + product_names=product_names, + folder_ids=folder_ids, + product_types=product_types, + product_base_types=product_base_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, + filters=filters, + fields=fields, + own_attributes=own_attributes, ) -def get_project_anatomy_presets() -> list[AnatomyPresetDict]: - """Anatomy presets available on server. - - Content has basic information about presets. Example output:: +def get_product_by_id( + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[ProductDict]: + """Query product entity by id. - [ - { - "name": "netflix_VFX", - "primary": false, - "version": "1.0.0" - }, - { - ... - }, - ... - ] + 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: - list[dict[str, str]]: Anatomy presets available on server. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_project_anatomy_presets() + return con.get_product_by_id( + project_name=project_name, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, + ) -def get_default_anatomy_preset_name() -> str: - """Name of default anatomy preset. +def get_product_by_name( + 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. - 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 '_'. + 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: - str: Name of preset that can be used by - 'get_project_anatomy_preset'. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_default_anatomy_preset_name() + return con.get_product_by_name( + project_name=project_name, + product_name=product_name, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, + ) -def get_project_anatomy_preset( - preset_name: Optional[str] = None, -) -> AnatomyPresetDict: - """Anatomy preset values by name. +def get_product_types( + fields: Optional[Iterable[str]] = None, +) -> list[ProductTypeDict]: + """Types of products. - Get anatomy preset values by preset name. Primary preset is returned - if preset name is set to 'None'. + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - preset_name (Optional[str]): Preset name. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - AnatomyPresetDict: Anatomy preset values. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_project_anatomy_preset( - preset_name=preset_name, + return con.get_product_types( + fields=fields, ) -def get_built_in_anatomy_preset() -> AnatomyPresetDict: - """Get built-in anatomy preset. +def get_project_product_types( + 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: - AnatomyPresetDict: Built-in anatomy preset. + list[ProductTypeDict]: Product types information. """ 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.get_project_product_types( + project_name=project_name, + fields=fields, + ) -def get_rest_project( - project_name: str, -) -> Optional[ProjectDict]: - """Query project by name. +def get_product_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """DEPRECATED Product type names. - This call returns project with anatomy data. + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. Args: - project_name (str): Name of project. + 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: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. + set[str]: Product type names. """ con = get_server_api_connection() - return con.get_rest_project( + return con.get_product_type_names( project_name=project_name, + product_ids=product_ids, ) -def get_rest_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, - include_skeleton: bool = False, -) -> Generator[ProjectDict, None, None]: - """Query available project entities. - - User must be logged in. +def create_product( + 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_base_type: Optional[str] = None, + product_id: Optional[str] = None, +) -> str: + """Create new product. 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. - include_skeleton (bool): Include skeleton projects. + 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_base_type (Optional[str]): Product base type. + product_id (Optional[str]): Product id. If not passed new id is + generated. Returns: - Generator[ProjectDict, None, None]: Available projects. + str: Product id. """ con = get_server_api_connection() - return con.get_rest_projects( + return con.create_product( + project_name=project_name, + name=name, + product_type=product_type, + folder_id=folder_id, + attrib=attrib, + data=data, + tags=tags, + status=status, active=active, - library=library, - include_skeleton=include_skeleton, + product_base_type=product_base_type, + product_id=product_id, ) -def get_rest_projects_list( - active: Optional[bool] = True, - library: Optional[bool] = None, - include_skeleton: bool = False, -) -> list[ProjectListDict]: - """Receive available projects. +def update_product( + project_name: str, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + product_base_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. - User must be logged in. + Update of ``data`` will override existing value on folder entity. - 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. - include_skeleton (bool): Include skeleton projects. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. - Returns: - list[ProjectListDict]: List of available projects. + 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. + product_base_type (Optional[str]): New product base 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.get_rest_projects_list( + return con.update_product( + project_name=project_name, + product_id=product_id, + name=name, + folder_id=folder_id, + product_type=product_type, + product_base_type=product_base_type, + attrib=attrib, + data=data, + tags=tags, + status=status, active=active, - library=library, - include_skeleton=include_skeleton, ) -def get_project_names( - active: Optional[bool] = True, - library: Optional[bool] = None, - include_skeleton: bool = False, -) -> list[str]: - """Receive available project names. - - User must be logged in. +def delete_product( + project_name: str, + product_id: str, +) -> None: + """Delete product. 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. - include_skeleton (bool): Include skeleton projects. - - Returns: - list[str]: List of available project names. + project_name (str): Project name. + product_id (str): Product id to delete. """ con = get_server_api_connection() - return con.get_project_names( - active=active, - library=library, - include_skeleton=include_skeleton, + return con.delete_product( + project_name=project_name, + product_id=product_id, ) -def get_projects( +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_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: Optional[bool] = True, - library: Optional[bool] = None, - include_skeleton: bool = False, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator[ProjectDict, None, None]: - """Get projects. + own_attributes=_PLACEHOLDER, +) -> Generator[VersionDict, None, None]: + """Get version entities based on passed filters from server. 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. - include_skeleton (bool): Include skeleton projects. - 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. + 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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[ProjectDict, None, None]: Queried projects. + Generator[VersionDict, None, None]: Queried version entities. """ con = get_server_api_connection() - return con.get_projects( + 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, - library=library, - include_skeleton=include_skeleton, + filters=filters, fields=fields, own_attributes=own_attributes, ) -def get_project( +def get_version_by_id( project_name: str, + version_id: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[ProjectDict]: - """Get project. + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query version entity by id. 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. + 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[ProjectDict]: Project entity data or None - if project was not found. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_project( + return con.get_version_by_id( project_name=project_name, + version_id=version_id, fields=fields, own_attributes=own_attributes, ) -def create_project( +def get_version_by_name( project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, - data: dict[str, Any] | None = None, - skeleton: bool = False, -) -> 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. + 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): 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. - data (dict[str, Any]): Project data. - skeleton (bool): Project is skeleton project. - - Raises: - ValueError: When project name already exists. + 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: - ProjectDict: Created project entity. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.create_project( + return con.get_version_by_name( project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, - data=data, - skeleton=skeleton, + version=version, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def update_project( +def get_hero_version_by_id( 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. + 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. - 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): 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.update_project( + return con.get_hero_version_by_id( 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, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def delete_project( +def get_hero_version_by_product_id( project_name: str, -): - """Delete project from server. + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[VersionDict]: + """Query hero version entity by product id. - This will completely remove project from server without any step back. + Only one hero version is available on a product. Args: - project_name (str): Project name that will be removed. + 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. """ con = get_server_api_connection() - return con.delete_project( + return con.get_hero_version_by_product_id( project_name=project_name, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_raw_project_folders() -> dict[str, Any]: - """Get project folders (raw data).""" - con = get_server_api_connection() - return con.get_raw_project_folders() - - -def get_project_folders() -> list[dict[str, Any]]: - con = get_server_api_connection() - return con.get_project_folders() - - -def create_project_folder( - label: str, - parent_id: str | None = None, - data: dict[str, Any] | None = None, -) -> str: - """Create project folder.""" - con = get_server_api_connection() - return con.create_project_folder( - label=label, - parent_id=parent_id, - data=data, - ) - - -def update_project_folder( - folder_id: str, - label: str | None = None, - parent_id: str | None = None, - data: dict[str, Any] | None = None, -) -> None: - con = get_server_api_connection() - return con.update_project_folder( - folder_id=folder_id, - label=label, - parent_id=parent_id, - data=data, - ) +def get_hero_versions( + 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. -def set_project_folders_order( - folder_ids: list[str], -) -> None: - """Set project folders order.""" - con = get_server_api_connection() - return con.set_project_folders_order( - folder_ids=folder_ids, - ) + 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. -def assign_projects_to_project_folder( - folder_id: str, - project_names: list[str], -) -> None: - """Assign project folder to project.""" + """ con = get_server_api_connection() - return con.assign_projects_to_project_folder( - folder_id=folder_id, - project_names=project_names, + return con.get_hero_versions( + project_name=project_name, + product_ids=product_ids, + version_ids=version_ids, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def delete_project_folder( - folder_id: str, -): - """Delete project folder.""" +def get_last_versions( + 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. + + """ con = get_server_api_connection() - return con.delete_project_folder( - folder_id=folder_id, + return con.get_last_versions( + project_name=project_name, + product_ids=product_ids, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_project_root_overrides( +def get_last_version_by_product_id( 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. + 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): Name of project. + 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, dict[str, str]]: Root values by root name by site id. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_project_root_overrides( + return con.get_last_version_by_product_id( project_name=project_name, + product_id=product_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_project_roots_by_site( +def get_last_version_by_product_name( 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 + 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): Name of project. + 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: - dict[str, dict[str, str]]: Root values by root name by site id. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_project_roots_by_site( + return con.get_last_version_by_product_name( project_name=project_name, + product_name=product_name, + folder_id=folder_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_project_root_overrides_by_site_id( +def version_is_latest( 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. + version_id: str, +) -> bool: + """Is version latest from a product. Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. + project_name (str): Project where to look for representation. + version_id (str): Version id. Returns: - dict[str, str]: Root values by root name or None if - site does not have overrides. + bool: Version is latest or not. """ con = get_server_api_connection() - return con.get_project_root_overrides_by_site_id( + return con.version_is_latest( project_name=project_name, - site_id=site_id, + version_id=version_id, ) -def get_project_roots_for_site( +def create_version( 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. + 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. - 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. + 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: - dict[str, str]: Root values by root name, root name is not - available if it does not have overrides. + str: Version id. """ con = get_server_api_connection() - return con.get_project_roots_for_site( + return con.create_version( project_name=project_name, - site_id=site_id, + version=version, + product_id=product_id, + task_id=task_id, + author=author, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + version_id=version_id, ) -def get_project_roots_by_site_id( +def update_version( project_name: str, - site_id: Optional[str] = None, -) -> dict[str, str]: - """Root values for a site. + 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. - 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. + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - root values. + Update of ``data`` will override existing value on folder entity. - Returns: - dict[str, str]: Root values. + 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. """ con = get_server_api_connection() - return con.get_project_roots_by_site_id( + return con.update_version( project_name=project_name, - site_id=site_id, + version_id=version_id, + version=version, + product_id=product_id, + task_id=task_id, + author=author, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def get_project_roots_by_platform( +def delete_version( 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. + version_id: str, +) -> None: + """Delete version. 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. + project_name (str): Project name. + version_id (str): Version id to delete. """ con = get_server_api_connection() - return con.get_project_roots_by_platform( + return con.delete_version( project_name=project_name, - platform_name=platform_name, + version_id=version_id, ) -def get_rest_folder( +def get_rest_representation( project_name: str, - folder_id: str, -) -> Optional[FolderDict]: + representation_id: str, +) -> Optional[RepresentationDict]: con = get_server_api_connection() - return con.get_rest_folder( + return con.get_rest_representation( project_name=project_name, - folder_id=folder_id, + representation_id=representation_id, ) -def get_rest_folders( +def get_representations( 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. + 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, + filters: Optional[AdvancedFilterDict] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator[RepresentationDict, None, None]: + """Get representation entities based on passed filters from server. - Example:: + .. todo:: - [ - { - "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", - }, - ... - ] + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + 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. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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: - list[FlatFolderDict]: List of folder entities. + Generator[RepresentationDict, None, None]: Queried + representation entities. """ con = get_server_api_connection() - return con.get_rest_folders( + return con.get_representations( project_name=project_name, - include_attrib=include_attrib, + 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, + filters=filters, + fields=fields, + own_attributes=own_attributes, ) -def get_folders_hierarchy( +def get_representation_by_id( 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...] - }, - ... - ] - } + 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 folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. + 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: - dict[str, Any]: Response data from server. + Optional[RepresentationDict]: Queried representation + entity or None. """ con = get_server_api_connection() - return con.get_folders_hierarchy( + return con.get_representation_by_id( project_name=project_name, - search_string=search_string, - folder_types=folder_types, + representation_id=representation_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folders_rest( +def get_representation_by_name( 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'. + 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 name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + 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: - list[FlatFolderDict]: List of folder entities. + Optional[RepresentationDict]: Queried representation entity + or None. """ con = get_server_api_connection() - return con.get_folders_rest( + return con.get_representation_by_name( project_name=project_name, - include_attrib=include_attrib, + representation_name=representation_name, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folders( +def get_representations_hierarchy( 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, - filters: Optional[AdvancedFilterDict] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator[FolderDict, None, None]: - """Query folders 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. - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. + Representation entity with parent entities up to project. - Notes: - Filter 'active' don't have direct filter in GraphQl. + 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): 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. - 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. + 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: - Generator[FolderDict, None, None]: Queried folder entities. + dict[str, RepresentationHierarchy]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_folders( + return con.get_representations_hierarchy( 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, - filters=filters, - fields=fields, - own_attributes=own_attributes, + 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_by_id( +def get_representation_hierarchy( project_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[FolderDict]: - """Query folder entity by id. + 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): 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): 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: - Optional[FolderDict]: Folder entity data or None - if was not found. + RepresentationHierarchy: Representation hierarchy entities. """ con = get_server_api_connection() - return con.get_folder_by_id( + return con.get_representation_hierarchy( project_name=project_name, - folder_id=folder_id, - fields=fields, - own_attributes=own_attributes, + 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_folder_by_path( +def get_representations_parents( project_name: str, - folder_path: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[FolderDict]: - """Query folder entity by path. + 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. - Folder path is a path to folder with all parent names joined by slash. + Representation parent entities up to project. 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): 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: - Optional[FolderDict]: Folder entity data or None - if was not found. + dict[str, RepresentationParents]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_folder_by_path( + return con.get_representations_parents( project_name=project_name, - folder_path=folder_path, - fields=fields, - own_attributes=own_attributes, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_folder_by_name( +def get_representation_parents( project_name: str, - folder_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[FolderDict]: - """Query folder entity by path. + 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. - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. + Representation parent entities up to project. 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): 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: - Optional[FolderDict]: Folder entity data or None - if was not found. + RepresentationParents: Representation parent entities. """ con = get_server_api_connection() - return con.get_folder_by_name( + return con.get_representation_parents( project_name=project_name, - folder_name=folder_name, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_folder_ids_with_products( - project_name: str, - folder_ids: Optional[Iterable[str]] = None, -) -> set[str]: - """Find folders which have at least one product. +def get_repre_ids_by_context_filters( + 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. - 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. + 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): 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 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: - set[str]: Folder ids that have at least one product. + 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_folder_ids_with_products( + return con.get_repre_ids_by_context_filters( project_name=project_name, - folder_ids=folder_ids, + context_filters=context_filters, + representation_names=representation_names, + version_ids=version_ids, ) -def create_folder( +def create_representation( project_name: str, name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, - label: Optional[str] = None, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, + representation_id: Optional[str] = None, ) -> str: - """Create new folder. + """Create new representation. 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. + 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: Entity id. + str: Representation id. """ con = get_server_api_connection() - return con.create_folder( + return con.create_representation( project_name=project_name, name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, + version_id=version_id, + files=files, attrib=attrib, data=data, + traits=traits, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, - folder_id=folder_id, + representation_id=representation_id, ) -def update_folder( +def update_representation( project_name: str, - folder_id: str, + representation_id: str, name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, - label: Optional[str] = NOT_SET, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + traits: 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 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 representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -5174,3674 +7976,3223 @@ def update_folder( Args: project_name (str): Project name. - folder_id (str): Folder id. + representation_id (str): Representation 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. + 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. - thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_folder( + return con.update_representation( project_name=project_name, - folder_id=folder_id, + representation_id=representation_id, name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, + version_id=version_id, + files=files, attrib=attrib, data=data, + traits=traits, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, ) -def delete_folder( +def delete_representation( project_name: str, - folder_id: str, - force: bool = False, + representation_id: str, ) -> None: - """Delete folder. + """Delete representation. 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. + representation_id (str): Representation id to delete. """ con = get_server_api_connection() - return con.delete_folder( + return con.delete_representation( project_name=project_name, - folder_id=folder_id, - force=force, + representation_id=representation_id, ) -def get_rest_task( +def get_workfile_entities( project_name: str, - task_id: str, -) -> Optional[TaskDict]: + *, + 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, +) -> 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. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ con = get_server_api_connection() - return con.get_rest_task( + return con.get_workfile_entities( project_name=project_name, - task_id=task_id, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, + fields=fields, ) -def get_tasks( +def get_workfile_entity( 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, - filters: Optional[AdvancedFilterDict] = None, + task_id: str, + path: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator[TaskDict, None, None]: - """Query task entities from server. +) -> Optional[WorkfileInfoDict]: + """Workfile info entity by task id and workfile path. 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. + 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 - 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. + representation. All possible fields are returned if 'None' is + passed. Returns: - Generator[TaskDict, None, None]: Queried task entities. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_tasks( + return con.get_workfile_entity( 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, - statuses=statuses, - tags=tags, - active=active, - filters=filters, + task_id=task_id, + path=path, fields=fields, - own_attributes=own_attributes, ) -def get_task_by_name( +def get_workfile_entity_by_id( project_name: str, - folder_id: str, - task_name: str, + workfile_id: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[TaskDict]: - """Query task entity by name and folder id. +) -> Optional[WorkfileInfoDict]: + """Workfile info 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. + 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. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_task_by_name( + return con.get_workfile_entity_by_id( project_name=project_name, - folder_id=folder_id, - task_name=task_name, + workfile_id=workfile_id, fields=fields, - own_attributes=own_attributes, ) -def get_task_by_id( +def create_workfile_entity( project_name: str, + path: str, task_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[TaskDict]: - """Query task entity by id. + *, + thumbnail_id: Optional[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, + workfile_id: Optional[str] = None, +) -> str: + """Create new workfile. 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. + project_name (str): Project name. + path (str): Representation name. + task_id (str): Parent task id. + thumbnail_id (Optional[str]): Thumbnail id. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + workfile_id (Optional[str]): Workfile info id. If not + passed new id is generated. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + str: Workfile info id. """ con = get_server_api_connection() - return con.get_task_by_id( + return con.create_workfile_entity( project_name=project_name, + path=path, task_id=task_id, - fields=fields, - own_attributes=own_attributes, + thumbnail_id=thumbnail_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + workfile_id=workfile_id, ) -def get_tasks_by_folder_paths( +def update_workfile_entity( 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, + workfile_id: str, + *, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - filters: Optional[AdvancedFilterDict] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> dict[str, list[TaskDict]]: - """Query task entities from server by folder paths. + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, +) -> None: + """Update workfile entity on server. - 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. - 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. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. - Returns: - dict[str, list[TaskDict]]: Task entities by - folder path. + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + 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. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. """ con = get_server_api_connection() - return con.get_tasks_by_folder_paths( + return con.update_workfile_entity( project_name=project_name, - folder_paths=folder_paths, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, + workfile_id=workfile_id, + path=path, + task_id=task_id, + attrib=attrib, + data=data, tags=tags, + status=status, active=active, - filters=filters, - fields=fields, - own_attributes=own_attributes, + thumbnail_id=thumbnail_id, + created_by=created_by, + updated_by=updated_by, ) -def get_tasks_by_folder_path( +def delete_workfile_entity( 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, + workfile_id: str, +) -> None: + """Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + con = get_server_api_connection() + return con.delete_workfile_entity( + project_name=project_name, + workfile_id=workfile_id, + ) + + +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, - active: Optional[bool] = True, + has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> list[TaskDict]: - """Query task entities from server by folder path. + own_attributes=_PLACEHOLDER, +) -> Generator[WorkfileInfoDict, None, None]: + """DEPRECATED Workfile info entities by passed filters. 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. + 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 - 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. + 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. """ con = get_server_api_connection() - return con.get_tasks_by_folder_path( + return con.get_workfiles_info( project_name=project_name, - folder_path=folder_path, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, statuses=statuses, tags=tags, - active=active, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_task_by_folder_path( +def get_workfile_info( project_name: str, - folder_path: str, - task_name: str, + task_id: str, + path: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional[TaskDict]: - """Query task entity by folder path and task name. + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by task id and workfile path. 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): 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[TaskDict]: Task entity data or None if was not found. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_task_by_folder_path( + return con.get_workfile_info( project_name=project_name, - folder_path=folder_path, - task_name=task_name, + task_id=task_id, + path=path, fields=fields, own_attributes=own_attributes, ) -def create_task( +def get_workfile_info_by_id( 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. + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by id. 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. + 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: - str: Task id. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.create_task( + return con.get_workfile_info_by_id( project_name=project_name, - name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - task_id=task_id, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, ) -def update_task( +def update_workfile_info( 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, + workfile_id: str, + path: Optional[str] = None, + task_id: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - tags: Optional[list[str]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, ) -> 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. + """DEPRECATED Update workfile entity on server. 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. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. 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. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. """ con = get_server_api_connection() - return con.update_task( + return con.update_workfile_info( project_name=project_name, + workfile_id=workfile_id, + path=path, task_id=task_id, - name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, thumbnail_id=thumbnail_id, + created_by=created_by, + updated_by=updated_by, ) -def delete_task( +def delete_workfile_info( project_name: str, - task_id: str, + workfile_id: str, ) -> None: - """Delete task. + """DEPRECATED Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + con = get_server_api_connection() + return con.delete_workfile_info( + project_name=project_name, + workfile_id=workfile_id, + ) + + +def get_full_link_type_name( + 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. + + """ + con = get_server_api_connection() + return con.get_full_link_type_name( + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + ) + + +def get_link_types( + 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. + + """ + con = get_server_api_connection() + return con.get_link_types( + project_name=project_name, + ) + + +def get_link_type( + 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. - Args: - project_name (str): Project name. - task_id (str): Task id to delete. + Returns: + Optional[dict[str, Any]]: Link type information. """ con = get_server_api_connection() - return con.delete_task( + return con.get_link_type( project_name=project_name, - task_id=task_id, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_rest_product( +def create_link_type( project_name: str, - product_id: str, -) -> Optional[ProductDict]: + 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. + + """ con = get_server_api_connection() - return con.get_rest_product( + return con.create_link_type( project_name=project_name, - product_id=product_id, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_products( +def delete_link_type( 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_base_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, - filters: Optional[AdvancedFilterDict] = None, - 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. + link_type_name: str, + input_type: str, + output_type: str, +) -> None: + """Remove link type from project. 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_base_types (Optional[Iterable[str]]): Product base 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. - 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. + 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. - Returns: - Generator[ProductDict, None, None]: Queried product entities. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_products( + return con.delete_link_type( project_name=project_name, - product_ids=product_ids, - product_names=product_names, - folder_ids=folder_ids, - product_types=product_types, - product_base_types=product_base_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, - filters=filters, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_product_by_id( +def make_sure_link_type_exists( project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[ProductDict]: - """Query product entity by id. + 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 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. + 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. """ con = get_server_api_connection() - return con.get_product_by_id( + return con.make_sure_link_type_exists( project_name=project_name, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_product_by_name( +def create_link( 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. + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, + data: Optional[dict[str, Any]] = None, +) -> CreateLinkResponseData: + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } 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. + 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. + data (Optional[dict[str, Any]]): Additional data to be stored + with the link. Returns: - Optional[ProductDict]: Product entity data or None - if was not found. + CreateLinkResponseData: Information about link. + + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_product_by_name( + return con.create_link( project_name=project_name, - product_name=product_name, - folder_id=folder_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_id=input_id, + input_type=input_type, + output_id=output_id, + output_type=output_type, + link_name=link_name, + data=data, ) -def get_product_types( - fields: Optional[Iterable[str]] = None, -) -> list[ProductTypeDict]: - """Types of products. +def create_links( + project_name: str, + links: list[CreateLinkData], +) -> list[CreateLinksResponseItem]: + """Create multiple links in a single request. - This is server wide information. Product types have 'name', 'icon' and - 'color'. + Example of link data:: + [ + { + "input": "59a212c0d2e211eda0e20242ac120001", + "output": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference|folder|folder", + "name": "my_link", + "data": {"key": "value"} + } + ] Args: - fields (Optional[Iterable[str]]): Product types fields to query. + project_name (str): Project where links are created. + links (list[CreateLinkData]): List of link data. Returns: - list[ProductTypeDict]: Product types information. + list[CreateLinksResponseItem]: Information about created links. + + Raises: + ValueError: Link data is invalid. """ con = get_server_api_connection() - return con.get_product_types( - fields=fields, + return con.create_links( + project_name=project_name, + links=links, ) -def get_project_product_types( +def delete_link( 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. + link_id: str, +) -> None: + """Remove link by id. Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. + project_name (str): Project where link exists. + link_id (str): Id of link. - Returns: - list[ProductTypeDict]: Product types information. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_project_product_types( + return con.delete_link( project_name=project_name, - fields=fields, + link_id=link_id, ) -def get_product_type_names( - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, -) -> set[str]: - """DEPRECATED Product type names. +def get_entities_links( + 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. - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. + .. 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 (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 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: - set[str]: Product type names. + dict[str, list[dict[str, Any]]]: Link info by entity ids. """ con = get_server_api_connection() - return con.get_product_type_names( + return con.get_entities_links( project_name=project_name, - product_ids=product_ids, + entity_type=entity_type, + entity_ids=entity_ids, + link_types=link_types, + link_direction=link_direction, + link_names=link_names, + link_name_regex=link_name_regex, ) -def create_product( +def get_folders_links( 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_base_type: Optional[str] = None, - product_id: Optional[str] = None, -) -> str: - """Create new product. + 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 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_base_type (Optional[str]): Product base type. - product_id (Optional[str]): Product id. If not passed new id is - generated. + 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: - str: Product id. + dict[str, list[dict[str, Any]]]: Link info by folder ids. """ con = get_server_api_connection() - return con.create_product( + return con.get_folders_links( project_name=project_name, - name=name, - product_type=product_type, - folder_id=folder_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - product_base_type=product_base_type, - product_id=product_id, + folder_ids=folder_ids, + link_types=link_types, + link_direction=link_direction, ) -def update_product( +def get_folder_links( project_name: str, - product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - product_base_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``. + 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 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. - product_base_type (Optional[str]): New product base 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. + 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. """ con = get_server_api_connection() - return con.update_product( + return con.get_folder_links( project_name=project_name, - product_id=product_id, - name=name, folder_id=folder_id, - product_type=product_type, - product_base_type=product_base_type, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, + link_types=link_types, + link_direction=link_direction, ) -def delete_product( +def get_tasks_links( project_name: str, - product_id: str, -) -> None: - """Delete product. + 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 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, - ) + 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. -def get_rest_version( - project_name: str, - version_id: str, -) -> Optional[VersionDict]: + """ con = get_server_api_connection() - return con.get_rest_version( + return con.get_tasks_links( project_name=project_name, - version_id=version_id, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, ) - -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: Optional[bool] = True, - filters: Optional[AdvancedFilterDict] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator[VersionDict, None, None]: - """Get version entities based on passed filters from server. - + +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): 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. - 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. + 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: - Generator[VersionDict, None, None]: Queried version entities. + list[dict[str, Any]]: Link info of task. """ con = get_server_api_connection() - return con.get_versions( + return con.get_task_links( 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, - filters=filters, - fields=fields, - own_attributes=own_attributes, + task_id=task_id, + link_types=link_types, + link_direction=link_direction, ) -def get_version_by_id( +def get_products_links( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[VersionDict]: - """Query version entity by id. + 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): 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. + 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: - Optional[VersionDict]: Version entity data or None - if was not found. + dict[str, list[dict[str, Any]]]: Link info by product ids. """ con = get_server_api_connection() - return con.get_version_by_id( + return con.get_products_links( project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_version_by_name( +def get_product_links( 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. + link_types: Optional[Iterable[str]] = None, + link_direction: Optional[LinkDirection] = None, +) -> list[dict[str, Any]]: + """Query product links from server. 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. + 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: - Optional[VersionDict]: Version entity data or None - if was not found. + list[dict[str, Any]]: Link info of product. """ con = get_server_api_connection() - return con.get_version_by_name( + return con.get_product_links( project_name=project_name, - version=version, product_id=product_id, - fields=fields, - own_attributes=own_attributes, + link_types=link_types, + link_direction=link_direction, ) -def get_hero_version_by_id( +def get_versions_links( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[VersionDict]: - """Query hero version entity by id. + 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): 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. + 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: - Optional[VersionDict]: Version entity data or None - if was not found. + dict[str, list[dict[str, Any]]]: Link info by version ids. """ con = get_server_api_connection() - return con.get_hero_version_by_id( + return con.get_versions_links( project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_hero_version_by_product_id( +def get_version_links( 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. + 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): 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. + 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: - Optional[VersionDict]: Version entity data or None - if was not found. + list[dict[str, Any]]: Link info of version. """ con = get_server_api_connection() - return con.get_hero_version_by_product_id( + return con.get_version_links( project_name=project_name, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, ) -def get_hero_versions( +def get_representations_links( 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. + 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): 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. + 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: - Optional[VersionDict]: Version entity data or None - if was not found. + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.get_hero_versions( + return con.get_representations_links( project_name=project_name, - product_ids=product_ids, - version_ids=version_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_ids=representation_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_last_versions( - 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. +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 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. + 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: - dict[str, Optional[VersionDict]]: Last versions by product id. + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.get_last_versions( + return con.get_representation_links( project_name=project_name, - product_ids=product_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + link_types=link_types, + link_direction=link_direction, ) -def get_last_version_by_product_id( +def get_entity_lists( project_name: str, - product_id: str, - active: Optional[bool] = True, + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[VersionDict]: - """Query last version entity by product id. +) -> Generator[dict[str, Any], None, None]: + """Fetch entity lists from AYON server. + + Warnings: + You can't get list items for lists with different 'entityType' in + one call. + + Notes: + To get list items, you have to pass 'items' field or + 'items.{sub-fields you want}' to 'fields' argument. 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. + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Optional[VersionDict]: Queried version entity or None. + Generator[dict[str, Any], None, None]: Entity list entities + matching defined filters. """ con = get_server_api_connection() - return con.get_last_version_by_product_id( + return con.get_entity_lists( project_name=project_name, - product_id=product_id, + list_ids=list_ids, active=active, fields=fields, - own_attributes=own_attributes, ) -def get_last_version_by_product_name( +def get_entity_list_rest( 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. + list_id: str, +) -> Optional[dict[str, Any]]: + """Get entity list by id using REST API. 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): Project name. + list_id (str): Entity list id. Returns: - Optional[VersionDict]: Queried version entity or None. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.get_last_version_by_product_name( + return con.get_entity_list_rest( project_name=project_name, - product_name=product_name, - folder_id=folder_id, - active=active, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, ) -def version_is_latest( +def get_entity_list_by_id( project_name: str, - version_id: str, -) -> bool: - """Is version latest from a product. + list_id: str, + fields: Optional[Iterable[str]] = None, +) -> Optional[dict[str, Any]]: + """Get entity list by id using GraphQl. Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - bool: Version is latest or not. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.version_is_latest( + return con.get_entity_list_by_id( project_name=project_name, - version_id=version_id, + list_id=list_id, + fields=fields, ) -def create_version( +def create_entity_list( project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, + entity_type: EntityListEntityType, + label: str, + *, + list_type: Optional[str] = None, + access: Optional[dict[str, Any]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, + tags: Optional[list[str]] = None, + template: Optional[dict[str, Any]] = None, + entity_list_folder_id: Optional[str] = None, + owner: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, + items: Optional[list[dict[str, Any]]] = None, + list_id: Optional[str] = None, ) -> str: - """Create new version. + """Create entity list. 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. + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + entity_list_folder_id (Optional[str]): Entity list folder id. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. """ con = get_server_api_connection() - return con.create_version( + return con.create_entity_list( project_name=project_name, - version=version, - product_id=product_id, - task_id=task_id, - author=author, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, attrib=attrib, data=data, tags=tags, - status=status, + template=template, + entity_list_folder_id=entity_list_folder_id, + owner=owner, active=active, - thumbnail_id=thumbnail_id, - version_id=version_id, + items=items, + list_id=list_id, ) -def update_version( +def update_entity_list( 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, + list_id: str, + *, + label: Optional[str] = None, + access: Optional[dict[str, Any]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, + tags: Optional[list[str]] = None, + entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, + owner: Optional[str] = None, active: Optional[bool] = None, - 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``. + """Update entity list. 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. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + entity_list_folder_id (str | None | type[NOT_SET]): New entity + list folder id. Use ``None`` to move entity list to root. + Use 'NOT_SET' to keep current folder. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. """ con = get_server_api_connection() - return con.update_version( + return con.update_entity_list( project_name=project_name, - version_id=version_id, - version=version, - product_id=product_id, - task_id=task_id, - author=author, + list_id=list_id, + label=label, + access=access, attrib=attrib, data=data, tags=tags, - status=status, + entity_list_folder_id=entity_list_folder_id, + owner=owner, active=active, - thumbnail_id=thumbnail_id, ) -def delete_version( +def delete_entity_list( project_name: str, - version_id: str, + list_id: str, ) -> None: - """Delete version. + """Delete entity list from project. Args: project_name (str): Project name. - version_id (str): Version id to delete. + list_id (str): Entity list id that will be removed. """ con = get_server_api_connection() - return con.delete_version( + return con.delete_entity_list( project_name=project_name, - version_id=version_id, + list_id=list_id, ) -def get_rest_representation( +def get_entity_list_attribute_definitions( project_name: str, - representation_id: str, -) -> Optional[RepresentationDict]: + list_id: str, +) -> list[EntityListAttributeDefinitionDict]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + list[EntityListAttributeDefinitionDict]: List of attribute + definitions. + + """ con = get_server_api_connection() - return con.get_rest_representation( + return con.get_entity_list_attribute_definitions( project_name=project_name, - representation_id=representation_id, + list_id=list_id, ) -def get_representations( +def set_entity_list_attribute_definitions( 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, - filters: Optional[AdvancedFilterDict] = 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. + list_id: str, + attribute_definitions: list[EntityListAttributeDefinitionDict], +) -> None: + """Set attribute definitioins on entity list. 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. - filters (Optional[AdvancedFilterDict]): Advanced filtering options. - 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. + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (list[EntityListAttributeDefinitionDict]): + List of attribute definitions. """ con = get_server_api_connection() - return con.get_representations( + return con.set_entity_list_attribute_definitions( project_name=project_name, - 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, - filters=filters, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, + attribute_definitions=attribute_definitions, ) -def get_representation_by_id( +def create_entity_list_item( 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. + list_id: str, + entity_id: 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, + item_id: Optional[str] = None, +) -> str: + """Create entity list item. 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. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + entity_id (str): Id of entity added to the list. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. Returns: - Optional[RepresentationDict]: Queried representation - entity or None. + str: Item id. """ con = get_server_api_connection() - return con.get_representation_by_id( + return con.create_entity_list_item( project_name=project_name, - representation_id=representation_id, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, + entity_id=entity_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + item_id=item_id, ) -def get_representation_by_name( +def update_entity_list_items( 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. + list_id: str, + items: list[dict[str, Any]], + mode: EntityListItemMode, +) -> None: + """Update items in entity list. 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. + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (list[dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. """ con = get_server_api_connection() - return con.get_representation_by_name( + return con.update_entity_list_items( project_name=project_name, - representation_name=representation_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, + items=items, + mode=mode, ) -def get_representations_hierarchy( +def update_entity_list_item( 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. + list_id: str, + item_id: str, + *, + new_list_id: Optional[str] = None, + 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, +) -> None: + """Update item in entity list. 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. + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. """ con = get_server_api_connection() - return con.get_representations_hierarchy( + return con.update_entity_list_item( 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, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, ) -def get_representation_hierarchy( +def delete_entity_list_item( 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. + list_id: str, + item_id: str, +) -> None: + """Delete item from entity list. 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. + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. """ con = get_server_api_connection() - return con.get_representation_hierarchy( + return con.delete_entity_list_item( 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, + list_id=list_id, + item_id=item_id, ) -def get_representations_parents( +def get_entity_list_entities( 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. + entity_list_id: str, +) -> dict[str, Any]: + """Get entity list items using REST API. 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 name. + entity_list_id (str): Entity list id. Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. + dict[str, Any]: Information about entities on the list. """ con = get_server_api_connection() - return con.get_representations_parents( + return con.get_entity_list_entities( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + entity_list_id=entity_list_id, ) -def get_representation_parents( +def get_entity_list_folders_raw( 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. +) -> dict[str, Any]: + """Get entity list folders. 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 name. Returns: - RepresentationParents: Representation parent entities. + dict[str, Any]: Raw output of entity list folders output. At this + moment contains only "folders" key with list of folders, + but it can be extended in the future. """ con = get_server_api_connection() - return con.get_representation_parents( + return con.get_entity_list_folders_raw( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, ) -def get_repre_ids_by_context_filters( +def get_entity_list_folders( 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. +) -> list[dict[str, Any]]: + """Get entity list folders. 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) + list[dict[str, Any]]: List of entity list folders. """ con = get_server_api_connection() - return con.get_repre_ids_by_context_filters( + return con.get_entity_list_folders( project_name=project_name, - context_filters=context_filters, - representation_names=representation_names, - version_ids=version_ids, ) -def create_representation( +def create_entity_list_folder( 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, + label: str, + *, + parent_id: str | None = None, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, + entity_list_folder_id: str | None = None, ) -> str: - """Create new representation. + """Create entity list folder. 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. - - """ - con = get_server_api_connection() - return con.create_representation( - project_name=project_name, - 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_representation( - 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``. + label (str): Folder label. + parent_id (str | None): Parent folder id. If None, the folder will + be created in root. + color (str | None): Folder color. + icon (str | None): Folder icon. + scope (list[EntityListScope] | None): Folder scope. Empty list can + be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. + entity_list_folder_id (str | None): Id of folder that will be + created. If None, a new id will be generated. - 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. + Returns: + str: Created entity list folder id. """ con = get_server_api_connection() - return con.update_representation( + return con.create_entity_list_folder( project_name=project_name, - representation_id=representation_id, - name=name, - version_id=version_id, - files=files, - attrib=attrib, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, data=data, - traits=traits, - tags=tags, - status=status, - active=active, + access=access, + entity_list_folder_id=entity_list_folder_id, ) -def delete_representation( +def update_entity_list_folder( project_name: str, - representation_id: str, + entity_list_folder_id: str, + *, + label: str | None = None, + parent_id: str | None | type[NOT_SET] = NOT_SET, + color: str | None = None, + icon: str | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, ) -> None: - """Delete representation. + """Update entity list folder. Args: project_name (str): Project name. - representation_id (str): Representation id to delete. + entity_list_folder_id (str): Folder id that will be updated. + label (str | None): New label of entity list folder. + parent_id (str | None | type[NOT_SET]): New parent id of entity + list folder. If None, the folder will be moved to root. + color (str | None): New color of entity list folder. + icon (str | None): New icon of entity list folder. + scope (list[EntityListScope] | None): New scope of entity list + folder. Empty list can be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. """ con = get_server_api_connection() - return con.delete_representation( + return con.update_entity_list_folder( project_name=project_name, - representation_id=representation_id, + entity_list_folder_id=entity_list_folder_id, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, + data=data, + access=access, ) -def get_workfile_entities( +def delete_entity_list_folder( 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, -) -> 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. - - Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. - - """ + entity_list_folder_id: str, +) -> None: + """Delete entity list folder.""" con = get_server_api_connection() - return con.get_workfile_entities( + return con.delete_entity_list_folder( project_name=project_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, + entity_list_folder_id=entity_list_folder_id, ) -def get_workfile_entity( +def set_entity_list_folders_order( project_name: str, - task_id: str, - path: str, - *, - fields: Optional[Iterable[str]] = None, -) -> Optional[WorkfileInfoDict]: - """Workfile info entity by task id and workfile path. + order: list[str], +) -> None: + """Change order of entity list folders. 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. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + project_name (str): Project name. + order (list[str]): List of folder ids in desired order. """ con = get_server_api_connection() - return con.get_workfile_entity( + return con.set_entity_list_folders_order( project_name=project_name, - task_id=task_id, - path=path, - fields=fields, + order=order, ) -def get_workfile_entity_by_id( +def materialize_entity_list( project_name: str, - workfile_id: str, - *, - fields: Optional[Iterable[str]] = None, -) -> Optional[WorkfileInfoDict]: - """Workfile info entity by id. + list_id: str, +) -> dict[str, Any]: + """Materialize entity list. 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 name. + list_id (str): Entity list id. Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + dict[str, Any]: Summary of materialized entity list. """ con = get_server_api_connection() - return con.get_workfile_entity_by_id( + return con.materialize_entity_list( project_name=project_name, - workfile_id=workfile_id, - fields=fields, + list_id=list_id, ) -def create_workfile_entity( +def set_entity_list_items_order( project_name: str, - path: str, - task_id: str, - *, - thumbnail_id: Optional[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, - workfile_id: Optional[str] = None, -) -> str: - """Create new workfile. + list_id: str, + order: list[str], +) -> None: + """Change order of items in entity list. Args: project_name (str): Project name. - path (str): Representation name. - task_id (str): Parent task id. - thumbnail_id (Optional[str]): Thumbnail id. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - workfile_id (Optional[str]): Workfile info id. If not - passed new id is generated. - - Returns: - str: Workfile info id. + list_id (str): Entity list id. + order (list[str]): Item ids in desired order. Must contain + each item of the entity list exactly once. """ con = get_server_api_connection() - return con.create_workfile_entity( + return con.set_entity_list_items_order( project_name=project_name, - path=path, - task_id=task_id, - thumbnail_id=thumbnail_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - workfile_id=workfile_id, + list_id=list_id, + order=order, ) -def update_workfile_entity( +def get_thumbnail_by_id( project_name: str, - workfile_id: str, - *, - path: Optional[str] = None, - task_id: 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, - created_by: Optional[str] = None, - updated_by: Optional[str] = None, -) -> None: - """Update workfile entity on server. + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + 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. - Args: - project_name (str): Project name. - workfile_id (str): Workfile id. - path (Optional[str]): New rootless workfile path.. - task_id (Optional[str]): New parent task id. - 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. - created_by (Optional[str]): New created by username. - updated_by (Optional[str]): New updated by username. + 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.update_workfile_entity( + return con.get_thumbnail_by_id( project_name=project_name, - workfile_id=workfile_id, - path=path, - task_id=task_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, thumbnail_id=thumbnail_id, - created_by=created_by, - updated_by=updated_by, ) -def delete_workfile_entity( +def get_thumbnail( project_name: str, - workfile_id: str, -) -> None: - """Delete workfile entity on server. + 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 name. - workfile_id (str): Workfile id to delete. + 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.delete_workfile_entity( + return con.get_thumbnail( project_name=project_name, - workfile_id=workfile_id, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, ) -def get_workfiles_info( +def get_folder_thumbnail( 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]: - """DEPRECATED Workfile info entities by passed filters. + 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. - 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. + 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: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.get_folder_thumbnail( project_name=project_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, + folder_id=folder_id, + thumbnail_id=thumbnail_id, ) -def get_workfile_info( +def get_task_thumbnail( project_name: str, task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[WorkfileInfoDict]: - """DEPRECATED Workfile info entity by task id and workfile path. +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. 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. + task_id (str): Folder id for which thumbnail should be returned. Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_workfile_info( + return con.get_task_thumbnail( project_name=project_name, task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, ) -def get_workfile_info_by_id( +def get_version_thumbnail( project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional[WorkfileInfoDict]: - """DEPRECATED Workfile info entity by id. + 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. - 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. + 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: - Optional[WorkfileInfoDict]: Workfile info entity or None. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.get_version_thumbnail( project_name=project_name, - workfile_id=workfile_id, - fields=fields, - own_attributes=own_attributes, + version_id=version_id, + thumbnail_id=thumbnail_id, ) -def update_workfile_info( +def get_workfile_thumbnail( project_name: str, workfile_id: str, - path: Optional[str] = None, - task_id: 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, - created_by: Optional[str] = None, - updated_by: Optional[str] = None, -) -> None: - """DEPRECATED Update workfile entity on server. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. Args: - project_name (str): Project name. - workfile_id (str): Workfile id. - path (Optional[str]): New rootless workfile path.. - task_id (Optional[str]): New parent task id. - 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. - created_by (Optional[str]): New created by username. - updated_by (Optional[str]): New updated by username. + 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.update_workfile_info( + return con.get_workfile_thumbnail( project_name=project_name, workfile_id=workfile_id, - path=path, - task_id=task_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, thumbnail_id=thumbnail_id, - created_by=created_by, - updated_by=updated_by, ) -def delete_workfile_info( +def create_thumbnail( project_name: str, - workfile_id: str, -) -> None: - """DEPRECATED Delete workfile entity on server. + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. Args: - project_name (str): Project name. - workfile_id (str): Workfile id to delete. + 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.delete_workfile_info( + return con.create_thumbnail( project_name=project_name, - workfile_id=workfile_id, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, ) - - -def get_full_link_type_name( - link_type_name: str, - input_type: str, - output_type: str, + + +def create_thumbnail_with_stream( + project_name: str, + stream: StreamType, + thumbnail_id: Optional[str] = None, ) -> str: - """Calculate full link type name used for query from server. + """Create new thumbnail on server from byte stream. Args: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + project_name (str): Project where the thumbnail will be created + and can be used. + stream (StreamType): Thumbnail content stream. + thumbnail_id (Optional[str]): Prepared if of thumbnail. Returns: - str: Full name of link type used for query from server. + str: Created thumbnail id. + + Raises: + ValueError: When a thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.get_full_link_type_name( - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + return con.create_thumbnail_with_stream( + project_name=project_name, + stream=stream, + thumbnail_id=thumbnail_id, ) -def get_link_types( +def update_thumbnail( project_name: str, -) -> list[dict[str, Any]]: - """All link types available on a project. + thumbnail_id: str, + src_filepath: str, +) -> None: + """Change thumbnail content by id. - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] + Update can be also used to create new thumbnail. Args: - project_name (str): Name of project where to look for link types. + 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. - Returns: - list[dict[str, Any]]: Link types available on project. + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.get_link_types( + return con.update_thumbnail( project_name=project_name, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, ) -def get_link_type( +def update_thumbnail_from_stream( 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. + thumbnail_id: str, + stream: StreamType, +) -> None: + """Change thumbnail content by id. - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } + Update can be also used to create new thumbnail. 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. + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + stream (StreamType): Thumbnail content stream. """ con = get_server_api_connection() - return con.get_link_type( + return con.update_thumbnail_from_stream( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + thumbnail_id=thumbnail_id, + stream=stream, ) -def create_link_type( +def set_entity_thumbnail( 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. + entity_type: str, + entity_id: str, + src_filepath: str, +) -> str: + """Upload new thumbnail for an entity. - Warning: - Because PUT is used for creation it is also used for update. + Created thumbnail is automatically set on the entity. 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. + project_name (str): Project under which the entity is located. + entity_type (str): Entity type. Supported types are 'folder', + 'task', 'version' and 'workfile'. + entity_id (str): Entity id. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Returns: + str: Created thumbnail id. Raises: - HTTPRequestError: Server error happened. + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.create_link_type( + return con.set_entity_thumbnail( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + entity_type=entity_type, + entity_id=entity_id, + src_filepath=src_filepath, ) -def delete_link_type( +def set_entity_thumbnail_from_stream( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -) -> None: - """Remove link type from project. + entity_type: str, + entity_id: str, + stream: StreamType, +) -> str: + """Upload new thumbnail for an entity from byte stream. + + Created thumbnail is automatically set on the entity. 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. + project_name (str): Project under which the entity is located. + entity_type (str): Entity type. Supported types are 'folder', + 'task', 'version' and 'workfile'. + entity_id (str): Entity id. + stream (StreamType): Thumbnail content stream. - Raises: - HTTPRequestError: Server error happened. + Returns: + str: Created thumbnail id. """ con = get_server_api_connection() - return con.delete_link_type( + return con.set_entity_thumbnail_from_stream( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + entity_type=entity_type, + entity_id=entity_id, + stream=stream, ) -def make_sure_link_type_exists( +def get_project_thumbnail( 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. +) -> ThumbnailContent: + """Get project thumbnail. 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. + project_name (str): Project name. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.make_sure_link_type_exists( + return con.get_project_thumbnail( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, ) -def create_link( +def set_project_thumbnail( project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, - data: Optional[dict[str, Any]] = None, -) -> CreateLinkResponseData: - """Create link between 2 entities. - - Link has a type which must already exists on a project. - - Example output:: - - { - "id": "59a212c0d2e211eda0e20242ac120002" - } + src_filepath: str, +) -> None: + """Upload project thumbnail. 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. - data (Optional[dict[str, Any]]): Additional data to be stored - with the link. - - Returns: - CreateLinkResponseData: Information about link. + project_name (str): Project name. + src_filepath (str): Filepath to thumbnail which should be uploaded. Raises: - HTTPRequestError: Server error happened. + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.create_link( + return con.set_project_thumbnail( project_name=project_name, - link_type_name=link_type_name, - input_id=input_id, - input_type=input_type, - output_id=output_id, - output_type=output_type, - link_name=link_name, - data=data, + src_filepath=src_filepath, ) -def create_links( +def set_project_thumbnail_from_stream( project_name: str, - links: list[CreateLinkData], -) -> list[CreateLinksResponseItem]: - """Create multiple links in a single request. - - Example of link data:: - [ - { - "input": "59a212c0d2e211eda0e20242ac120001", - "output": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference|folder|folder", - "name": "my_link", - "data": {"key": "value"} - } - ] - - Args: - project_name (str): Project where links are created. - links (list[CreateLinkData]): List of link data. - - Returns: - list[CreateLinksResponseItem]: Information about created links. + stream: StreamType, +) -> None: + """Upload project thumbnail from byte stream. - Raises: - ValueError: Link data is invalid. + Args: + project_name (str): Project name. + stream (StreamType): Thumbnail content stream. """ con = get_server_api_connection() - return con.create_links( + return con.set_project_thumbnail_from_stream( project_name=project_name, - links=links, + stream=stream, ) -def delete_link( - project_name: str, - link_id: str, +def create_user( + user_name: str, + *, + password: Optional[str] = None, + api_key: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, ) -> None: - """Remove link by id. + """Create new user. - Args: - project_name (str): Project where link exists. - link_id (str): Id of link. + User level and access is defined in 'data', e.g. 'isAdmin', + 'isManager', 'isService', 'isGuest', 'accessGroups' + ({project_name: [access_group_name, ...]}) or + 'defaultAccessGroups' ([access_group_name, ...]). - Raises: - HTTPRequestError: Server error happened. + Args: + user_name (str): Name of the user. + password (Optional[str]): Password of the user. Service users + cannot have password. + api_key (Optional[str]): Api key for a service user. + attrib (Optional[dict[str, Any]]): User attributes. + data (Optional[dict[str, Any]]): User data. + active (Optional[bool]): User active state. """ con = get_server_api_connection() - return con.delete_link( - project_name=project_name, - link_id=link_id, + return con.create_user( + user_name=user_name, + password=password, + api_key=api_key, + attrib=attrib, + data=data, + active=active, ) -def get_entities_links( - 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 +def update_user( + user_name: str, + *, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, +) -> None: + """Update user. - Example output: - { - "59a212c0d2e211eda0e20242ac120001": [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, - ... - ], - ... - } + Non-manager users can change only their own attributes. 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. + user_name (str): Name of the user. + attrib (Optional[dict[str, Any]]): User attributes to change. + data (Optional[dict[str, Any]]): User data to change. + active (Optional[bool]): User active state. """ con = get_server_api_connection() - return con.get_entities_links( - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - link_types=link_types, - link_direction=link_direction, - link_names=link_names, - link_name_regex=link_name_regex, + return con.update_user( + user_name=user_name, + attrib=attrib, + data=data, + active=active, ) -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]]]: - """Query folders links from server. +def delete_user( + user_name: str, +) -> None: + """Delete user. 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. + user_name (str): Name of the user. """ con = get_server_api_connection() - return con.get_folders_links( - project_name=project_name, - folder_ids=folder_ids, - link_types=link_types, - link_direction=link_direction, + return con.delete_user( + user_name=user_name, ) -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]]: - """Query folder links from server. +def rename_user( + user_name: str, + new_user_name: str, +) -> None: + """Rename user. - 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. + This is a manager-only operation. Target user name must not exist. - Returns: - list[dict[str, Any]]: Link info of folder. + Args: + user_name (str): Current name of the user. + new_user_name (str): New name of the user. """ con = get_server_api_connection() - return con.get_folder_links( - project_name=project_name, - folder_id=folder_id, - link_types=link_types, - link_direction=link_direction, + return con.rename_user( + user_name=user_name, + new_user_name=new_user_name, ) -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]]]: - """Query tasks links from server. +def change_password( + user_name: str, + *, + password: Optional[str] = None, + api_key: Optional[str] = None, +) -> None: + """Change user password or service user api key. + + Only one of 'password' or 'api_key' should be passed. If both are + passed the password is changed. 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. + user_name (str): Name of the user. + password (Optional[str]): New password. + api_key (Optional[str]): New api key of a service user. Must be + 32 characters long hex string. - Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + """ + con = get_server_api_connection() + return con.change_password( + user_name=user_name, + password=password, + api_key=api_key, + ) + + +def set_user_access_groups( + user_name: str, + access_groups: dict[str, list[str]], +) -> None: + """Set user access groups on projects. + + Projects that are not in 'access_groups' are not affected. Pass + an empty list to remove access to a project. + + Args: + user_name (str): Name of the user. + access_groups (dict[str, list[str]]): Access groups by + project name. """ 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, + return con.set_user_access_groups( + user_name=user_name, + access_groups=access_groups, ) -def get_task_links( - project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional[LinkDirection] = None, +def get_user_sessions( + user_name: str, ) -> list[dict[str, Any]]: - """Query task links from server. + """Get active sessions of user. 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. + user_name (str): Name of the user. Returns: - list[dict[str, Any]]: Link info of task. + list[dict[str, Any]]: Sessions with 'token', 'isService', + 'lastUsed' and 'clientInfo' keys. """ 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, + return con.get_user_sessions( + user_name=user_name, ) -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. +def delete_user_session( + user_name: str, + session_id: str, +) -> None: + """Delete user session (logout). 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. + user_name (str): Name of the user. + session_id (str): Session id (token). """ 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, + return con.delete_user_session( + user_name=user_name, + session_id=session_id, ) -def get_product_links( - project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional[LinkDirection] = None, +def get_user_api_keys( + user_name: str, ) -> list[dict[str, Any]]: - """Query product links from server. + """Get api keys of user. 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. + user_name (str): Name of the user. Returns: - list[dict[str, Any]]: Link info of product. + list[dict[str, Any]]: Api keys with 'id', 'label', 'preview', + 'created' and 'expires' keys. """ 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, + return con.get_user_api_keys( + user_name=user_name, ) -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. +def create_user_api_key( + user_name: str, + label: str, + key: str, + *, + expires: Optional[float] = None, +) -> None: + """Create api key for user. 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. + user_name (str): Name of the user. + label (str): Label of the api key. Must be unique for the user. + key (str): The api key value. + expires (Optional[float]): Expiration timestamp. """ 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, + return con.create_user_api_key( + user_name=user_name, + label=label, + key=key, + expires=expires, ) -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. +def update_user_api_key( + user_name: str, + api_key_id: str, + *, + label: Optional[str] = None, + expires: Optional[float] = None, +) -> None: + """Update api key of user. 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. + user_name (str): Name of the user. + api_key_id (str): Api key id. + label (Optional[str]): New label of the api key. + expires (Optional[float]): New expiration timestamp. """ 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, + return con.update_user_api_key( + user_name=user_name, + api_key_id=api_key_id, + label=label, + expires=expires, ) -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. +def delete_user_api_key( + user_name: str, + api_key_id: str, +) -> None: + """Delete api key of user. 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. + user_name (str): Name of the user. + api_key_id (str): Api key id. """ 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, + return con.delete_user_api_key( + user_name=user_name, + api_key_id=api_key_id, ) -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. +def get_user_permissions( + user_name: Optional[str] = None, + project_name: Optional[str] = None, +) -> dict[str, Any]: + """Get user permissions. + + Without 'user_name' are returned permissions of current user. For + current user without 'project_name' is returned user level with + studio and per-project permissions. Otherwise, are returned + studio permissions, or project permissions if 'project_name' + is passed. 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. + user_name (Optional[str]): Name of the user. Current user is used + if not passed. + project_name (Optional[str]): Project name. Returns: - list[dict[str, Any]]: Link info of representation. + dict[str, Any]: User permissions. """ con = get_server_api_connection() - return con.get_representation_links( + return con.get_user_permissions( + user_name=user_name, project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, ) -def get_entity_lists( - project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, -) -> Generator[dict[str, Any], None, None]: - """Fetch entity lists from AYON server. - - Warnings: - You can't get list items for lists with different 'entityType' in - one call. - - Notes: - To get list items, you have to pass 'items' field or - 'items.{sub-fields you want}' to 'fields' argument. +def get_access_groups( + project_name: Optional[str] = None, +) -> list[dict[str, Any]]: + """Get access groups. Args: - project_name (str): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. + project_name (Optional[str]): Project name. Studio level access + groups are returned if not passed. Returns: - Generator[dict[str, Any], None, None]: Entity list entities - matching defined filters. + list[dict[str, Any]]: Access groups with 'name' and + 'isProjectLevel' keys. """ con = get_server_api_connection() - return con.get_entity_lists( + return con.get_access_groups( project_name=project_name, - list_ids=list_ids, - active=active, - fields=fields, ) -def get_entity_list_rest( - project_name: str, - list_id: str, -) -> Optional[dict[str, Any]]: - """Get entity list by id using REST API. +def get_access_group_schema( + project_name: Optional[str] = None, +) -> dict[str, Any]: + """Get access group (permissions) settings schema. Args: - project_name (str): Project name. - list_id (str): Entity list id. + project_name (Optional[str]): Project name used as context. Returns: - Optional[dict[str, Any]]: Entity list data or None if not found. + dict[str, Any]: Access group schema. """ con = get_server_api_connection() - return con.get_entity_list_rest( + return con.get_access_group_schema( project_name=project_name, - list_id=list_id, ) -def get_entity_list_by_id( - project_name: str, - list_id: str, - fields: Optional[Iterable[str]] = None, -) -> Optional[dict[str, Any]]: - """Get entity list by id using GraphQl. +def get_access_group( + access_group_name: str, + project_name: Optional[str] = None, +) -> dict[str, Any]: + """Get access group definition (permissions). Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. + access_group_name (str): Access group name. + project_name (Optional[str]): Project name. Studio level access + group is returned if not passed. Returns: - Optional[dict[str, Any]]: Entity list data or None if not found. + dict[str, Any]: Access group permissions. """ con = get_server_api_connection() - return con.get_entity_list_by_id( + return con.get_access_group( + access_group_name=access_group_name, project_name=project_name, - list_id=list_id, - fields=fields, ) -def create_entity_list( - project_name: str, - entity_type: EntityListEntityType, - label: str, - *, - list_type: Optional[str] = None, - access: Optional[dict[str, Any]] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[list[str]] = None, - template: Optional[dict[str, Any]] = None, - entity_list_folder_id: Optional[str] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - items: Optional[list[dict[str, Any]]] = None, - list_id: Optional[str] = None, -) -> str: - """Create entity list. +def save_access_group( + access_group_name: str, + permissions: dict[str, Any], + project_name: Optional[str] = None, +) -> None: + """Create or update access group. Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - entity_list_folder_id (Optional[str]): Entity list folder id. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. + access_group_name (str): Access group name. + permissions (dict[str, Any]): Access group permissions. + project_name (Optional[str]): Project name. Studio level access + group is saved if not passed. """ con = get_server_api_connection() - return con.create_entity_list( + return con.save_access_group( + access_group_name=access_group_name, + permissions=permissions, project_name=project_name, - entity_type=entity_type, - label=label, - list_type=list_type, - access=access, - attrib=attrib, - data=data, - tags=tags, - template=template, - entity_list_folder_id=entity_list_folder_id, - owner=owner, - active=active, - items=items, - list_id=list_id, ) -def update_entity_list( - project_name: str, - list_id: str, - *, - label: Optional[str] = None, - access: Optional[dict[str, Any]] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[list[str]] = None, - entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, - owner: Optional[str] = None, - active: Optional[bool] = None, +def delete_access_group( + access_group_name: str, + project_name: Optional[str] = None, ) -> None: - """Update entity list. + """Delete access group. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (str | None | type[NOT_SET]): New entity - list folder id. Use ``None`` to move entity list to root. - Use 'NOT_SET' to keep current folder. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. + access_group_name (str): Access group name. + project_name (Optional[str]): Project name. Studio level access + group is deleted if not passed. """ con = get_server_api_connection() - return con.update_entity_list( + return con.delete_access_group( + access_group_name=access_group_name, project_name=project_name, - list_id=list_id, - label=label, - access=access, - attrib=attrib, - data=data, - tags=tags, - entity_list_folder_id=entity_list_folder_id, - owner=owner, - active=active, ) -def delete_entity_list( - project_name: str, - list_id: str, +def set_projects_access( + access: dict[str, dict[str, list[str]]], ) -> None: - """Delete entity list from project. + """Set access of multiple users to multiple projects. + + Projects not present in the payload are not affected. Pass an empty + list to revoke access to a project. + + Example:: + + { + "alice": { + "MyProject01": ["editor"], + "MyProject02": ["artist"], + }, + "bob": { + "MyProject01": ["artist", "reviewer"], + "MyProject02": [], + }, + } Args: - project_name (str): Project name. - list_id (str): Entity list id that will be removed. + access (dict[str, dict[str, list[str]]]): Access groups by + project name by user name. """ con = get_server_api_connection() - return con.delete_entity_list( - project_name=project_name, - list_id=list_id, + return con.set_projects_access( + access=access, ) -def get_entity_list_attribute_definitions( +def get_teams( project_name: str, - list_id: str, -) -> list[EntityListAttributeDefinitionDict]: - """Get attribute definitioins on entity list. + *, + show_members: bool = False, +) -> list[dict[str, Any]]: + """Get teams in a project. Args: project_name (str): Project name. - list_id (str): Entity list id. + show_members (bool): Include team members in the response. Returns: - list[EntityListAttributeDefinitionDict]: List of attribute - definitions. + list[dict[str, Any]]: Teams with 'name', 'memberCount', + 'leaders' and optionally 'members' keys. """ con = get_server_api_connection() - return con.get_entity_list_attribute_definitions( + return con.get_teams( project_name=project_name, - list_id=list_id, + show_members=show_members, ) -def set_entity_list_attribute_definitions( +def update_teams( project_name: str, - list_id: str, - attribute_definitions: list[EntityListAttributeDefinitionDict], + teams: list[dict[str, Any]], ) -> None: - """Set attribute definitioins on entity list. + """Create or replace multiple teams in a project. + + Teams that are not in 'teams' are not affected. Args: project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (list[EntityListAttributeDefinitionDict]): - List of attribute definitions. + teams (list[dict[str, Any]]): Teams with 'name' and 'members' + keys. Member is a dictionary with 'name', 'leader' + and 'roles' keys. """ con = get_server_api_connection() - return con.set_entity_list_attribute_definitions( + return con.update_teams( project_name=project_name, - list_id=list_id, - attribute_definitions=attribute_definitions, + teams=teams, ) -def create_entity_list_item( +def save_team( project_name: str, - list_id: str, - entity_id: 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, - item_id: Optional[str] = None, -) -> str: - """Create entity list item. + team_name: str, + members: list[dict[str, Any]], +) -> None: + """Create or replace a team in a project. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - entity_id (str): Id of entity added to the list. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. - - Returns: - str: Item id. + project_name (str): Project name. + team_name (str): Team name. + members (list[dict[str, Any]]): Team members. Member is + a dictionary with 'name', 'leader' and 'roles' keys. """ con = get_server_api_connection() - return con.create_entity_list_item( + return con.save_team( project_name=project_name, - list_id=list_id, - entity_id=entity_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, - item_id=item_id, + team_name=team_name, + members=members, ) -def update_entity_list_items( +def delete_team( project_name: str, - list_id: str, - items: list[dict[str, Any]], - mode: EntityListItemMode, + team_name: str, ) -> None: - """Update items in entity list. + """Delete a team from a project. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id. - items (list[dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. + project_name (str): Project name. + team_name (str): Team name. """ con = get_server_api_connection() - return con.update_entity_list_items( + return con.delete_team( project_name=project_name, - list_id=list_id, - items=items, - mode=mode, + team_name=team_name, ) -def update_entity_list_item( +def save_team_member( project_name: str, - list_id: str, - item_id: str, + team_name: str, + member_name: str, *, - new_list_id: Optional[str] = None, - 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, + leader: bool = False, + roles: Optional[list[str]] = None, ) -> None: - """Update item in entity list. + """Add or replace a team member. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. + project_name (str): Project name. + team_name (str): Team name. + member_name (str): User name. + leader (bool): Is user a leader of the team. + roles (Optional[list[str]]): User roles in the team. """ con = get_server_api_connection() - return con.update_entity_list_item( + return con.save_team_member( project_name=project_name, - list_id=list_id, - item_id=item_id, - new_list_id=new_list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, + team_name=team_name, + member_name=member_name, + leader=leader, + roles=roles, ) -def delete_entity_list_item( +def delete_team_member( project_name: str, - list_id: str, - item_id: str, + team_name: str, + member_name: str, ) -> None: - """Delete item from entity list. + """Remove a member from a team. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. + project_name (str): Project name. + team_name (str): Team name. + member_name (str): User name. """ con = get_server_api_connection() - return con.delete_entity_list_item( + return con.delete_team_member( project_name=project_name, - list_id=list_id, - item_id=item_id, + team_name=team_name, + member_name=member_name, ) -def get_entity_list_entities( +def get_project_users( project_name: str, - entity_list_id: str, -) -> dict[str, Any]: - """Get entity list items using REST API. +) -> dict[str, list[str]]: + """Get users with explicitly set access groups on a project. + + Managers and admins are not included as they have access to all + projects. Args: project_name (str): Project name. - entity_list_id (str): Entity list id. Returns: - dict[str, Any]: Information about entities on the list. + dict[str, list[str]]: Access groups by user name. """ con = get_server_api_connection() - return con.get_entity_list_entities( + return con.get_project_users( project_name=project_name, - entity_list_id=entity_list_id, ) -def get_entity_list_folders_raw( +def set_project_user_access_groups( project_name: str, -) -> dict[str, Any]: - """Get entity list folders. + user_name: str, + access_groups: list[str], +) -> dict[str, list[str]]: + """Set access groups of a user on a project. + + Pass an empty list to remove user access to the project. Args: project_name (str): Project name. + user_name (str): User name. + access_groups (list[str]): Access group names. Returns: - dict[str, Any]: Raw output of entity list folders output. At this - moment contains only "folders" key with list of folders, - but it can be extended in the future. + dict[str, list[str]]: Access groups by user name of all users + on the project. """ con = get_server_api_connection() - return con.get_entity_list_folders_raw( + return con.set_project_user_access_groups( project_name=project_name, + user_name=user_name, + access_groups=access_groups, ) -def get_entity_list_folders( - project_name: str, -) -> list[dict[str, Any]]: - """Get entity list folders. +def get_services() -> list[dict[str, Any]]: + """Get addon services registered on server. + + Only administrators receive the services, other users get an + empty list. Returns: - list[dict[str, Any]]: List of entity list folders. + list[dict[str, Any]]: List of services. """ con = get_server_api_connection() - return con.get_entity_list_folders( - project_name=project_name, - ) + return con.get_services() -def create_entity_list_folder( - project_name: str, - label: str, - *, - parent_id: str | None = None, - color: str | None = None, - icon: str | None = None, - scope: list[EntityListScope] | None = None, - data: dict[str, Any] | None = None, - access: dict[str, Any] | None = None, - entity_list_folder_id: str | None = None, -) -> str: - """Create entity list folder. +def spawn_service( + service_name: str, + addon_name: str, + addon_version: str, + service: str, + hostname: str, + config: Optional[dict[str, Any]] = None, +) -> None: + """Spawn (create) a new addon service. + + Requires administrator privileges. Args: - project_name (str): Project name. - label (str): Folder label. - parent_id (str | None): Parent folder id. If None, the folder will - be created in root. - color (str | None): Folder color. - icon (str | None): Folder icon. - scope (list[EntityListScope] | None): Folder scope. Empty list can - be used to scope folder for all views. - data (dict[str, Any] | None): Custom data of entity list folder. - access (dict[str, Any] | None): Access control for - entity list folder. - entity_list_folder_id (str | None): Id of folder that will be - created. If None, a new id will be generated. + service_name (str): Unique name of the service. + addon_name (str): Name of addon that provides the service. + addon_version (str): Version of addon. + service (str): Name of service defined by the addon. + hostname (str): Name of host (ASH) where service should run. + config (Optional[dict[str, Any]]): Service configuration + e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', + 'storagePath' or 'registryAuth'. - Returns: - str: Created entity list folder id. + """ + con = get_server_api_connection() + return con.spawn_service( + service_name=service_name, + addon_name=addon_name, + addon_version=addon_version, + service=service, + hostname=hostname, + config=config, + ) + + +def update_service( + service_name: str, + should_run: Optional[bool] = None, + hostname: Optional[str] = None, + error: Optional[str] = None, + config: Optional[dict[str, Any]] = None, +) -> None: + """Update addon service. + + Requires administrator privileges. + + Args: + service_name (str): Name of the service. + should_run (Optional[bool]): Service should be running. + hostname (Optional[str]): Name of host where service should run. + error (Optional[str]): Error message. Ignored when service + should be running. + config (Optional[dict[str, Any]]): Service configuration + values to change e.g. 'volumes', 'ports', 'memLimit', + 'user', 'env', 'storagePath' or 'registryAuth'. """ con = get_server_api_connection() - return con.create_entity_list_folder( - project_name=project_name, - label=label, - parent_id=parent_id, - color=color, - icon=icon, - scope=scope, - data=data, - access=access, - entity_list_folder_id=entity_list_folder_id, + return con.update_service( + service_name=service_name, + should_run=should_run, + hostname=hostname, + error=error, + config=config, ) -def update_entity_list_folder( - project_name: str, - entity_list_folder_id: str, - *, - label: str | None = None, - parent_id: str | None | type[NOT_SET] = NOT_SET, - color: str | None = None, - icon: str | None = None, - scope: list[EntityListScope] | None = None, - data: dict[str, Any] | None = None, - access: dict[str, Any] | None = None, +def delete_service( + service_name: str, ) -> None: - """Update entity list folder. + """Delete addon service. + + Requires administrator privileges. Args: - project_name (str): Project name. - entity_list_folder_id (str): Folder id that will be updated. - label (str | None): New label of entity list folder. - parent_id (str | None | type[NOT_SET]): New parent id of entity - list folder. If None, the folder will be moved to root. - color (str | None): New color of entity list folder. - icon (str | None): New icon of entity list folder. - scope (list[EntityListScope] | None): New scope of entity list - folder. Empty list can be used to scope folder for all views. - data (dict[str, Any] | None): Custom data of entity list folder. - access (dict[str, Any] | None): Access control for - entity list folder. + service_name (str): Name of the service. """ con = get_server_api_connection() - return con.update_entity_list_folder( - project_name=project_name, - entity_list_folder_id=entity_list_folder_id, - label=label, - parent_id=parent_id, - color=color, - icon=icon, - scope=scope, - data=data, - access=access, + return con.delete_service( + service_name=service_name, ) -def delete_entity_list_folder( - project_name: str, - entity_list_folder_id: str, +def get_hosts() -> list[dict[str, Any]]: + """Get AYON service hosts (ASH) registered on server. + + Host is registered automatically when it sends heartbeat to server. + + Example output:: + + [ + { + "name": "my-host", + "lastSeen": "2025-01-01T00:00:00", + "health": {"cpu": 0.5, "mem": 42.0}, + } + ] + + Returns: + list[dict[str, Any]]: List of hosts. + + """ + con = get_server_api_connection() + return con.get_hosts() + + +def get_sites( + platform_name: Optional[str] = None, + hostname: Optional[str] = None, +) -> list[dict[str, Any]]: + """Get sites registered on server. + + Non-manager users receive only sites they have used. + + Example output:: + + [ + { + "id": "site-id", + "platform": "windows", + "hostname": "my-machine", + "version": "1.0.0", + "users": ["admin"], + } + ] + + Args: + platform_name (Optional[str]): Filter sites by platform name. + Possible values are 'windows', 'linux' and 'darwin'. + hostname (Optional[str]): Filter sites by machine hostname. + + Returns: + list[dict[str, Any]]: List of sites. + + """ + con = get_server_api_connection() + return con.get_sites( + platform_name=platform_name, + hostname=hostname, + ) + + +def delete_site( + site_id: str, ) -> None: - """Delete entity list folder.""" + """Unregister site from server. + + Requires administrator privileges. + + Args: + site_id (str): Site id. + + """ con = get_server_api_connection() - return con.delete_entity_list_folder( - project_name=project_name, - entity_list_folder_id=entity_list_folder_id, + return con.delete_site( + site_id=site_id, ) -def set_entity_list_folders_order( - project_name: str, - order: list[str], +def get_restart_required() -> dict[str, Any]: + """Get information whether server requires restart. + + Example output:: + + { + "required": True, + "reason": "Server restart is required", + } + + Returns: + dict[str, Any]: Restart required information with keys + 'required' and 'reason'. + + """ + con = get_server_api_connection() + return con.get_restart_required() + + +def set_restart_required( + required: bool, + reason: Optional[str] = None, ) -> None: - """Change order of entity list folders. + """Set or clear the server restart required flag. + + This notifies administrators that the server needs to be restarted. + Use 'trigger_server_restart' to actually restart the server. Args: - project_name (str): Project name. - order (list[str]): List of folder ids in desired order. + required (bool): Server requires restart. + reason (Optional[str]): Human-readable reason for the restart. """ con = get_server_api_connection() - return con.set_entity_list_folders_order( - project_name=project_name, - order=order, + return con.set_restart_required( + required=required, + reason=reason, ) -def get_thumbnail_by_id( - project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. +def get_server_config_schema() -> dict[str, Any]: + """Get JSON schema of server configuration. - 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. + Returns: + dict[str, Any]: Server configuration schema. - 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. + """ + con = get_server_api_connection() + return con.get_server_config_schema() - 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. +def update_server_config( + config: dict[str, Any], +) -> None: + """Partially update server configuration. + + Values are recursively merged into current server configuration. + Keys that are not passed remain unchanged. Use the same + structure as 'get_server_config' returns, e.g. + '{"studio_name": "My Studio"}'. + + Requires administrator privileges. + + Args: + config (dict[str, Any]): Server configuration values to change. """ con = get_server_api_connection() - return con.get_thumbnail_by_id( - project_name=project_name, - thumbnail_id=thumbnail_id, + return con.update_server_config( + config=config, ) -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. +def get_enums() -> list[dict[str, Any]]: + """Get available enum resolvers. - 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. + Example output:: - 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'. + [ + { + "name": "statuses", + "label": "Statuses", + ... + } + ] Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: List of enum resolvers information. """ 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, - ) + return con.get_enums() -def get_folder_thumbnail( - project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. +def get_enum( + enum_name: str, + project_name: Optional[str] = None, + user_name: Optional[str] = None, + context: Optional[dict[str, Any]] = None, +) -> list[dict[str, Any]]: + """Get enum items resolved by server. + + Accepted context values depend on the enum resolver, use 'get_enums' + to find out which parameters a resolver accepts. + + Example output:: + + [ + { + "value": "in_progress", + "label": "In progress", + ... + } + ] 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. + enum_name (str): Name of the enum e.g. 'statuses'. + project_name (Optional[str]): Project name for project context. + user_name (Optional[str]): Resolve enum for different user. + Only administrators can use this. + context (Optional[dict[str, Any]]): Additional context + parameters for the resolver. Lists are joined with comma. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: List of enum items. """ con = get_server_api_connection() - return con.get_folder_thumbnail( + return con.get_enum( + enum_name=enum_name, project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, + user_name=user_name, + context=context, ) -def get_task_thumbnail( +def get_reviewables( project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. + entity_type: ReviewablesEntityType, + entity_ids: Iterable[str], + *, + latest: bool = False, + latest_done: bool = False, +) -> list[dict[str, Any]]: + """Get reviewables for multiple entities of one entity type. + + Each item in output represents a version with its reviewables. 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 name. + entity_type (ReviewablesEntityType): Entity type of passed + entity ids. Supported types are 'folder', 'product', + 'version' and 'task'. + entity_ids (Iterable[str]): Entity ids for which reviewables + should be returned. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: Versions with reviewables. """ con = get_server_api_connection() - return con.get_task_thumbnail( + return con.get_reviewables( project_name=project_name, - task_id=task_id, + entity_type=entity_type, + entity_ids=entity_ids, + latest=latest, + latest_done=latest_done, ) -def get_version_thumbnail( +def get_version_reviewables( project_name: str, version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. + *, + latest: bool = False, + latest_done: bool = False, +) -> dict[str, Any]: + """Get reviewables of a version. + + Notes: + Server responds with an error if version does not have + any reviewables. 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 name. + version_id (str): Version id. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, Any]: Version information with reviewables. """ con = get_server_api_connection() - return con.get_version_thumbnail( + return con.get_version_reviewables( project_name=project_name, version_id=version_id, - thumbnail_id=thumbnail_id, + latest=latest, + latest_done=latest_done, ) -def get_workfile_thumbnail( +def get_product_reviewables( project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. + product_id: str, + *, + latest: bool = False, + latest_done: bool = False, +) -> list[dict[str, Any]]: + """Get reviewables of all versions of a product. 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 name. + product_id (str): Product id. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: Versions with reviewables. """ con = get_server_api_connection() - return con.get_workfile_thumbnail( + return con.get_product_reviewables( project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, + product_id=product_id, + latest=latest, + latest_done=latest_done, ) -def create_thumbnail( +def get_folder_reviewables( project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from passed path. + folder_id: str, + *, + latest: bool = False, + latest_done: bool = False, +) -> list[dict[str, Any]]: + """Get reviewables of all versions under a folder. 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. + folder_id (str): Folder id. + latest (bool): Return only reviewables of the latest versions. + latest_done (bool): Return only reviewables of the latest + approved versions. Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. + list[dict[str, Any]]: Versions with reviewables. """ con = get_server_api_connection() - return con.create_thumbnail( + return con.get_folder_reviewables( project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, + folder_id=folder_id, + latest=latest, + latest_done=latest_done, ) -def create_thumbnail_with_stream( +def get_task_reviewables( project_name: str, - stream: StreamType, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from byte stream. + task_id: str, + *, + latest: bool = False, + latest_done: bool = False, +) -> list[dict[str, Any]]: + """Get reviewables of all versions linked to a task. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - stream (StreamType): Thumbnail content stream. - thumbnail_id (Optional[str]): Prepared if of thumbnail. + project_name (str): Project name. + task_id (str): Task id. + latest (bool): Return only reviewables of the latest versions. + latest_done (bool): Return only reviewables of the latest + approved versions. Returns: - str: Created thumbnail id. - - Raises: - ValueError: When a thumbnail source cannot be processed. + list[dict[str, Any]]: Versions with reviewables. """ con = get_server_api_connection() - return con.create_thumbnail_with_stream( + return con.get_task_reviewables( project_name=project_name, - stream=stream, - thumbnail_id=thumbnail_id, + task_id=task_id, + latest=latest, + latest_done=latest_done, ) -def update_thumbnail( +def sort_version_reviewables( project_name: str, - thumbnail_id: str, - src_filepath: str, + version_id: str, + reviewable_ids: list[str], ) -> None: - """Change thumbnail content by id. - - Update can be also used to create new thumbnail. + """Change order of reviewables of a version. 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. + project_name (str): Project name. + version_id (str): Version id. + reviewable_ids (list[str]): Reviewable (activity) ids in + the desired order. Must contain all reviewables of + the version. """ con = get_server_api_connection() - return con.update_thumbnail( + return con.sort_version_reviewables( project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, + version_id=version_id, + reviewable_ids=reviewable_ids, ) -def update_thumbnail_from_stream( +def update_reviewable( project_name: str, - thumbnail_id: str, - stream: StreamType, + version_id: str, + reviewable_id: str, + *, + label: Optional[str] = None, ) -> None: - """Change thumbnail content by id. + """Update reviewable of a version. - Update can be also used to create new thumbnail. + At this moment only label of reviewable can be changed. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - stream (StreamType): Thumbnail content stream. + project_name (str): Project name. + version_id (str): Version id. + reviewable_id (str): Reviewable (activity) id. + label (Optional[str]): New label of reviewable. """ con = get_server_api_connection() - return con.update_thumbnail_from_stream( + return con.update_reviewable( project_name=project_name, - thumbnail_id=thumbnail_id, - stream=stream, + version_id=version_id, + reviewable_id=reviewable_id, + label=label, ) diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py index 6e81904a8..014e705e4 100644 --- a/ayon_api/_api_helpers/__init__.py +++ b/ayon_api/_api_helpers/__init__.py @@ -17,6 +17,10 @@ from .actions import ActionsAPI from .links import LinksAPI from .lists import ListsAPI +from .users import UsersAPI +from .services import ServicesAPI +from .system import SystemAPI +from .reviewables import ReviewablesAPI __all__ = ( @@ -39,4 +43,8 @@ "ActionsAPI", "LinksAPI", "ListsAPI", + "UsersAPI", + "ServicesAPI", + "SystemAPI", + "ReviewablesAPI", ) diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 9bb7bc214..fc91d4604 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -2,7 +2,7 @@ import os import typing -from typing import Optional, Any +from typing import Optional, Any, Literal from ayon_api.utils import ( RequestTypes, @@ -461,7 +461,9 @@ def get_addon_settings_schema( self, addon_name: str, addon_version: str, - project_name: Optional[str] = None + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, ) -> dict[str, Any]: """Sudio/Project settings schema of an addon. @@ -473,19 +475,26 @@ def get_addon_settings_schema( addon_version (str): Version of addon. project_name (Optional[str]): Schema for specific project or default studio schemas. + variant (Optional[str]): Settings variant used as context for + the schema. Server uses 'production' if not passed. + site_id (Optional[str]): Site id used as context for project + schema. Ignored if 'project_name' is not passed. Returns: dict[str, Any]: Schema of studio/project settings. """ args = tuple() + query_data = {"variant": variant or None} if project_name: args = (project_name, ) + query_data["site_id"] = site_id or None + query = prepare_query_string(query_data) endpoint = self.get_addon_endpoint( addon_name, addon_version, "schema", *args ) - result = self.get(endpoint) + result = self.get(f"{endpoint}{query}") result.raise_for_status() return result.data @@ -900,3 +909,532 @@ def get_addons_settings( use_site=use_site, only_values=only_values ) + + def set_addon_studio_settings( + self, + addon_name: str, + addon_version: str, + settings: dict[str, Any], + variant: Optional[str] = None, + ) -> None: + """Set studio settings of an addon. + + Server calculates studio overrides from passed settings values. + Explicit (un)pinning of fields can be done with + '__pinned_fields__' and '__unpinned_fields__' keys in settings. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + settings (dict[str, Any]): Full settings values. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + response = self.post( + f"addons/{addon_name}/{addon_version}/settings{query}", + **settings + ) + response.raise_for_status() + + def set_addon_project_settings( + self, + addon_name: str, + addon_version: str, + project_name: str, + settings: dict[str, Any], + variant: Optional[str] = None, + site_id: Optional[str] = None, + ) -> None: + """Set project settings of an addon. + + Server calculates project overrides from passed settings values. + If 'site_id' is passed then project site overrides of current user + are set instead. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + settings (dict[str, Any]): Full settings values. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which are site overrides + set. Project overrides are set if not passed. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + }) + response = self.post( + f"addons/{addon_name}/{addon_version}" + f"/settings/{project_name}{query}", + **settings + ) + response.raise_for_status() + + def get_addon_studio_overrides( + self, + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + as_version: Optional[str] = None, + ) -> dict[str, Any]: + """Get studio overrides of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + as_version (Optional[str]): Use overrides of different addon + version. + + Returns: + dict[str, Any]: Overrides information by settings path. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "variant": variant or None, + "as": as_version or None, + }) + response = self.get( + f"addons/{addon_name}/{addon_version}/overrides{query}" + ) + response.raise_for_status() + return response.data + + def modify_addon_studio_overrides( + self, + addon_name: str, + addon_version: str, + action: Literal["delete", "pin"], + path: list[str], + variant: Optional[str] = None, + ) -> None: + """Modify studio override of an addon on a settings path. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + action (Literal["delete", "pin"]): Remove override or pin + current value as override. + path (list[str]): Path to settings key. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + response = self.post( + f"addons/{addon_name}/{addon_version}/overrides{query}", + action=action, + path=path, + ) + response.raise_for_status() + + def delete_addon_studio_overrides( + self, + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + ) -> None: + """Delete all studio overrides of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + response = self.delete( + f"addons/{addon_name}/{addon_version}/overrides{query}" + ) + response.raise_for_status() + + def get_addon_project_overrides( + self, + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + as_version: Optional[str] = None, + ) -> dict[str, Any]: + """Get project overrides of an addon. + + Output contains studio and project overrides, and site overrides + if 'site_id' is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to include site overrides. + as_version (Optional[str]): Use overrides of different addon + version. + + Returns: + dict[str, Any]: Overrides information by settings path. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + "as": as_version or None, + }) + response = self.get( + f"addons/{addon_name}/{addon_version}" + f"/overrides/{project_name}{query}" + ) + response.raise_for_status() + return response.data + + def modify_addon_project_overrides( + self, + addon_name: str, + addon_version: str, + project_name: str, + action: Literal["delete", "pin"], + path: list[str], + variant: Optional[str] = None, + site_id: Optional[str] = None, + ) -> None: + """Modify project override of an addon on a settings path. + + Project site override of current user is modified if 'site_id' + is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + action (Literal["delete", "pin"]): Remove override or pin + current value as override. + path (list[str]): Path to settings key. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to modify site overrides. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + }) + response = self.post( + f"addons/{addon_name}/{addon_version}" + f"/overrides/{project_name}{query}", + action=action, + path=path, + ) + response.raise_for_status() + + def delete_addon_project_overrides( + self, + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + ) -> None: + """Delete all project overrides of an addon. + + Project site overrides of current user are deleted if 'site_id' + is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to delete site overrides. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + }) + response = self.delete( + f"addons/{addon_name}/{addon_version}" + f"/overrides/{project_name}{query}" + ) + response.raise_for_status() + + def get_addon_studio_raw_overrides( + self, + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + ) -> dict[str, Any]: + """Get raw studio overrides of an addon. + + Raw overrides are stored data without any processing. Only admins + can access them. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + Returns: + dict[str, Any]: Raw overrides data. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + response = self.get( + f"addons/{addon_name}/{addon_version}/rawOverrides{query}" + ) + response.raise_for_status() + return response.data + + def set_addon_studio_raw_overrides( + self, + addon_name: str, + addon_version: str, + overrides: dict[str, Any], + variant: Optional[str] = None, + ) -> None: + """Set raw studio overrides of an addon. + + Warnings: + Data are stored without any validation. Only admins can use + this. Use with caution. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + overrides (dict[str, Any]): Raw overrides data. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + response = self.put( + f"addons/{addon_name}/{addon_version}/rawOverrides{query}", + **overrides + ) + response.raise_for_status() + + def get_addon_project_raw_overrides( + self, + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + ) -> dict[str, Any]: + """Get raw project overrides of an addon. + + Raw overrides are stored data without any processing. Project + overrides are accessible only by admins, project site overrides + of current user are returned if 'site_id' is passed. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to get site overrides. + + Returns: + dict[str, Any]: Raw overrides data. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + }) + response = self.get( + f"addons/{addon_name}/{addon_version}" + f"/rawOverrides/{project_name}{query}" + ) + response.raise_for_status() + return response.data + + def set_addon_project_raw_overrides( + self, + addon_name: str, + addon_version: str, + project_name: str, + overrides: dict[str, Any], + variant: Optional[str] = None, + site_id: Optional[str] = None, + ) -> None: + """Set raw project overrides of an addon. + + Project site overrides of current user are set if 'site_id' + is passed. + + Warnings: + Data are stored without any validation. Use with caution. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Project name. + overrides (dict[str, Any]): Raw overrides data. + variant (Optional[str]): Name of settings variant. Used + 'default_settings_variant' by default. + site_id (Optional[str]): Site id to set site overrides. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site_id": site_id or None, + "variant": variant or None, + }) + response = self.put( + f"addons/{addon_name}/{addon_version}" + f"/rawOverrides/{project_name}{query}", + **overrides + ) + response.raise_for_status() + + def set_addon_site_settings( + self, + addon_name: str, + addon_version: str, + settings: dict[str, Any], + site_id: Optional[str] = None, + ) -> None: + """Set site settings of an addon for current user. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + settings (dict[str, Any]): Site settings values. + site_id (Optional[str]): Site id for which are settings set. + Using 'site_id' attribute if not passed. + + Raises: + ValueError: If site id is not available. + + """ + if site_id is None: + site_id = self.get_site_id() + + if not site_id: + raise ValueError("Site id is not available.") + + query = prepare_query_string({"site_id": site_id}) + response = self.put( + f"addons/{addon_name}/{addon_version}/siteSettings{query}", + **settings + ) + response.raise_for_status() + + def migrate_addon_settings( + self, + addon_name: str, + source_version: str, + target_version: str, + source_variant: Optional[str] = None, + target_variant: Optional[str] = None, + with_projects: Optional[bool] = None, + ) -> dict[str, Any]: + """Migrate settings of an addon between addon versions. + + Args: + addon_name (str): Name of addon. + source_version (str): Source addon version. + target_version (str): Target addon version. + source_variant (Optional[str]): Source settings variant. Server + uses 'production' if not passed. + target_variant (Optional[str]): Target settings variant. Server + uses 'production' if not passed. + with_projects (Optional[bool]): Migrate also project settings. + Server uses 'True' if not passed. + + Returns: + dict[str, Any]: Server response with 'events' created during + the migration. + + """ + body = { + "sourceVersion": source_version, + "targetVersion": target_version, + } + for key, value in ( + ("sourceVariant", source_variant), + ("targetVariant", target_variant), + ("withProjects", with_projects), + ): + if value is not None: + body[key] = value + + response = self.post(f"addons/{addon_name}/migrate", **body) + response.raise_for_status() + return response.data + + def migrate_settings_by_bundle( + self, + source_bundle: str, + target_bundle: str, + source_variant: str, + target_variant: str, + with_projects: bool = True, + ) -> None: + """Migrate settings of addons from source to target bundle. + + Settings of addons present in both bundles are migrated. + + Args: + source_bundle (str): Source bundle name. + target_bundle (str): Target bundle name. + source_variant (str): Source settings variant. + target_variant (str): Target settings variant. + with_projects (bool): Migrate also project settings. + + """ + response = self.post( + "migrateSettingsByBundle", + sourceBundle=source_bundle, + targetBundle=target_bundle, + sourceVariant=source_variant, + targetVariant=target_variant, + withProjects=with_projects, + ) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 03aaf77d2..e34dc8a8b 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -342,6 +342,38 @@ def delete_event(self, event_id: str) -> None: response = self.delete(f"events/{event_id}") response.raise_for_status() + def delete_events(self, events_filter: EventFilter) -> None: + """Delete events matching filter. + + Requires administrator privileges. All events are deleted if + filter does not contain any conditions. + + Example filter:: + + { + "conditions": [ + { + "key": "topic", + "value": "my.topic", + "operator": "eq", + } + ], + "operator": "and", + } + + Args: + events_filter (EventFilter): Filtering conditions to find events + to delete. For more technical specifications look to server + backed 'ayon_server.sqlfilter.QueryFilter'. + + """ + response = self.post( + "eventops", + type="delete", + filter=events_filter, + ) + response.raise_for_status() + def enroll_event_job( self, source_topic: Union[str, list[str]], diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index d10cfeb7a..26f84888d 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -493,6 +493,94 @@ def get_folder_ids_with_products( for folder in folders } + def search_folders( + self, + project_name: str, + search: Optional[str] = None, + folder_search: Optional[str] = None, + folder_filter: Optional[AdvancedFilterDict] = None, + task_search: Optional[str] = None, + task_filter: Optional[AdvancedFilterDict] = None, + ) -> list[str]: + """Search folders using text search and filters. + + Returns ids of folders matching the folder conditions, or containing + tasks matching the task conditions. + + Args: + project_name (str): Project name. + search (Optional[str]): Fulltext search applied to both + folders and tasks. + folder_search (Optional[str]): Fulltext search applied + to folders. + folder_filter (Optional[AdvancedFilterDict]): Filter applied + to folders. + task_search (Optional[str]): Fulltext search applied to tasks. + task_filter (Optional[AdvancedFilterDict]): Filter applied + to tasks. + + Returns: + list[str]: Matching folder ids. + + """ + body = { + key: value + for key, value in ( + ("search", search), + ("folderSearch", folder_search), + ("folderFilter", folder_filter), + ("taskSearch", task_search), + ("taskFilter", task_filter), + ) + if value is not None + } + response = self.post( + f"projects/{project_name}/folders/search", + **body + ) + response.raise_for_status() + return response.data["folderIds"] + + def get_entity_groups( + self, + project_name: str, + entity_type: str, + grouping_key: str, + empty: bool = False, + ) -> dict[str, Any]: + """Get groups of entities based on grouping key. + + Example output:: + + { + "groups": [ + {"value": "john.doe", "count": 42, ...}, + ... + ], + "key": "assignees", + "entityType": "task" + } + + Args: + project_name (str): Project name. + entity_type (str): Entity type, e.g. 'folder' or 'task'. + grouping_key (str): Grouping key, e.g. 'status', 'tags', + 'taskType', 'folderType', 'productType', 'productBaseType', + 'assignees' (tasks only) or 'attrib.{attribute name}'. + empty (bool): Include empty groups. + + Returns: + dict[str, Any]: Entity groups data. + + """ + query = prepare_query_string({"empty": "true" if empty else None}) + response = self.get( + f"projects/{project_name}/grouping/{entity_type}/{grouping_key}" + f"{query}" + ) + response.raise_for_status() + return response.data + def create_folder( self, project_name: str, diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 585504fb4..c1e7a6db2 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -675,3 +675,40 @@ def set_entity_list_folders_order( order=order, ) response.raise_for_status() + + def materialize_entity_list( + self, project_name: str, list_id: str + ) -> dict[str, Any]: + """Materialize entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + dict[str, Any]: Summary of materialized entity list. + + """ + response = self.post( + f"projects/{project_name}/lists/{list_id}/materialize" + ) + response.raise_for_status() + return response.data + + def set_entity_list_items_order( + self, project_name: str, list_id: str, order: list[str] + ) -> None: + """Change order of items in entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + order (list[str]): Item ids in desired order. Must contain + each item of the entity list exactly once. + + """ + response = self.post( + f"projects/{project_name}/lists/{list_id}/order", + order=order, + ) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index df602d808..2113b82d1 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -140,6 +140,85 @@ def get_build_in_anatomy_preset(self) -> AnatomyPresetDict: ) return self.get_built_in_anatomy_preset() + def get_anatomy_schema(self) -> dict[str, Any]: + """Get anatomy JSON schema. + + The schema is used to display the anatomy preset editor form. + + Returns: + dict[str, Any]: Anatomy JSON schema. + + """ + response = self.get("anatomy/schema") + response.raise_for_status() + return response.data + + def save_project_anatomy_preset( + self, preset_name: str, preset: AnatomyPresetDict + ) -> None: + """Create or update anatomy preset. + + Built-in preset '__builtin__' and primary preset reference + '__primary__' cannot be updated. + + Args: + preset_name (str): Anatomy preset name. + preset (AnatomyPresetDict): Anatomy preset values. + + """ + response = self.put(f"anatomy/presets/{preset_name}", **preset) + response.raise_for_status() + + def rename_project_anatomy_preset( + self, preset_name: str, new_preset_name: str + ) -> None: + """Rename anatomy preset. + + Args: + preset_name (str): Current anatomy preset name. + new_preset_name (str): New anatomy preset name. + + """ + response = self.post( + f"anatomy/presets/{preset_name}/rename", + name=new_preset_name, + ) + response.raise_for_status() + + def delete_project_anatomy_preset(self, preset_name: str) -> None: + """Delete anatomy preset. + + Args: + preset_name (str): Anatomy preset name. + + """ + response = self.delete(f"anatomy/presets/{preset_name}") + response.raise_for_status() + + def set_primary_anatomy_preset(self, preset_name: str) -> None: + """Set anatomy preset as primary. + + Primary preset is used as default preset for new projects. Use + preset name '_' to unset any primary preset (built-in preset + is used as default). + + Args: + preset_name (str): Anatomy preset name. + + """ + response = self.post(f"anatomy/presets/{preset_name}/primary") + response.raise_for_status() + + def unset_primary_anatomy_preset(self, preset_name: str) -> None: + """Unset primary flag of anatomy preset. + + Args: + preset_name (str): Anatomy preset name. + + """ + response = self.delete(f"anatomy/presets/{preset_name}/primary") + response.raise_for_status() + def get_rest_project( self, project_name: str ) -> Optional[ProjectDict]: @@ -540,6 +619,292 @@ def delete_project(self, project_name: str): f"Failed to delete project \"{project_name}\". {detail}" ) + def rename_project( + self, + project_name: str, + new_project_name: str, + new_project_code: Optional[str] = None, + ) -> None: + """Rename project. + + Args: + project_name (str): Current project name. + new_project_name (str): New project name. + new_project_code (Optional[str]): New project code. Project code + is unchanged if not passed. + + """ + body = {"name": new_project_name} + if new_project_code is not None: + body["code"] = new_project_code + response = self.post(f"projects/{project_name}/rename", **body) + response.raise_for_status() + + def get_project_anatomy(self, project_name: str) -> AnatomyPresetDict: + """Get project anatomy. + + Args: + project_name (str): Project name. + + Returns: + AnatomyPresetDict: Project anatomy in anatomy preset format. + + """ + response = self.get(f"projects/{project_name}/anatomy") + response.raise_for_status() + return response.data + + def set_project_anatomy( + self, project_name: str, anatomy: AnatomyPresetDict + ) -> None: + """Set project anatomy. + + Args: + project_name (str): Project name. + anatomy (AnatomyPresetDict): Anatomy data in anatomy + preset format. + + """ + response = self.post( + f"projects/{project_name}/anatomy", + **anatomy + ) + response.raise_for_status() + + def get_project_stats(self, project_name: str) -> dict[str, Any]: + """Get project statistics. + + Example output:: + + { + "counts": { + "folders": 10, + "products": 20, + "versions": 30, + "representations": 40, + "tasks": 50 + } + } + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Project statistics. + + """ + response = self.get(f"projects/{project_name}/stats") + response.raise_for_status() + return response.data + + def get_project_tags(self, project_name: str) -> dict[str, list[str]]: + """Get tags used on entities in project. + + Example output:: + + { + "folders": ["tag1"], + "tasks": [], + "products": [], + "versions": ["tag2"], + "representations": [], + "workfiles": [] + } + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Used tags by entity type (plural). + + """ + response = self.get(f"projects/{project_name}/tags") + response.raise_for_status() + return response.data + + def get_project_bundle( + self, project_name: str, variant: str = "production" + ) -> dict[str, Any]: + """Get project bundle information. + + Information contains addon versions used by project, installer + version, dependency packages and available options for each. + + Args: + project_name (str): Project name. + variant (str): Bundle variant 'production' or 'staging'. + + Returns: + dict[str, Any]: Project bundle information. + + """ + query = prepare_query_string({"variant": variant}) + response = self.get(f"projects/{project_name}/bundle{query}") + response.raise_for_status() + return response.data + + def set_project_bundle( + self, + project_name: str, + addons: dict[str, Optional[str]], + installer_version: Optional[str] = None, + dependency_packages: Optional[dict[str, Optional[str]]] = None, + variant: str = "production", + ) -> None: + """Set (freeze) project bundle. + + Args: + project_name (str): Project name. + addons (dict[str, Optional[str]]): Addon versions by addon name. + Use 'None' to disable an addon. + installer_version (Optional[str]): Installer version. + dependency_packages (Optional[dict[str, Optional[str]]]): + Dependency package names by platform name. + variant (str): Bundle variant 'production' or 'staging'. + + """ + body = {"addons": addons} + if installer_version is not None: + body["installerVersion"] = installer_version + if dependency_packages is not None: + body["dependencyPackages"] = dependency_packages + query = prepare_query_string({"variant": variant}) + response = self.post( + f"projects/{project_name}/bundle{query}", + **body + ) + response.raise_for_status() + + def unset_project_bundle( + self, project_name: str, variant: str = "production" + ) -> None: + """Unset (unfreeze) project bundle. + + Args: + project_name (str): Project name. + variant (str): Bundle variant 'production' or 'staging'. + + """ + query = prepare_query_string({"variant": variant}) + response = self.delete(f"projects/{project_name}/bundle{query}") + response.raise_for_status() + + def get_project_entity_uris( + self, + project_name: str, + entity_type: str, + entity_ids: Iterable[str], + ) -> list[dict[str, str]]: + """Get AYON URIs for project entities. + + Example output:: + + [ + { + "id": "0254c370005811ee9a740242ac130004", + "uri": "ayon+entity://demo/assets/hero" + }, + ... + ] + + Args: + project_name (str): Project name. + entity_type (str): Entity type, e.g. 'folder', 'task', + 'product', 'version', 'representation' or 'workfile'. + entity_ids (Iterable[str]): Entity ids. + + Returns: + list[dict[str, str]]: Entity URIs with entity ids. + + """ + response = self.post( + f"projects/{project_name}/uris", + entityType=entity_type, + ids=list(entity_ids), + ) + response.raise_for_status() + return response.data["uris"] + + def resolve_uris( + self, + uris: Iterable[str], + resolve_roots: bool = False, + path_only: bool = False, + site_id: Optional[str] = None, + ) -> list[dict[str, Any]]: + """Resolve AYON URIs to entities. + + Each URI starts with 'ayon://{project_name}/{path}' with optional + query arguments 'product', 'version', 'representation' or + 'task', 'workfile'. + + Example output:: + + [ + { + "uri": "ayon://demo/assets/hero?product=modelMain", + "entities": [ + { + "projectName": "demo", + "folderId": "0254c370005811ee9a740242ac130004", + "productId": "0255ce50005811ee9a740242ac130004", + "target": "product" + } + ] + }, + ... + ] + + Args: + uris (Iterable[str]): AYON URIs to resolve. + resolve_roots (bool): Resolve roots in representation paths. + Requires site id. + path_only (bool): Return only file paths of entities. + site_id (Optional[str]): Site id used to resolve roots. Site id + set on connection is used if not passed. + + Returns: + list[dict[str, Any]]: Resolved URIs. + + """ + query = prepare_query_string({ + "pathOnly": "true" if path_only else None, + "site_id": site_id, + }) + response = self.post( + f"resolve{query}", + uris=list(uris), + resolveRoots=resolve_roots, + ) + response.raise_for_status() + return response.data + + def get_project_file_info( + self, project_name: str, file_id: str + ) -> dict[str, Any]: + """Get information about project file. + + Example output:: + + { + "size": 1024, + "filename": "image.png", + "contentType": "image/png", + "mediaInfo": None + } + + Args: + project_name (str): Project name. + file_id (str): File id. + + Returns: + dict[str, Any]: File information. + + """ + response = self.get(f"projects/{project_name}/files/{file_id}/info") + response.raise_for_status() + return response.data + def get_raw_project_folders(self) -> dict[str, Any]: """Get project folders (raw data).""" response = self.get("projectFolders") @@ -684,6 +1049,30 @@ def get_project_root_overrides_by_site_id( roots = self.get_project_root_overrides(project_name) return roots.get(site_id, {}) + def set_project_root_overrides( + self, + project_name: str, + site_id: str, + roots: dict[str, Optional[str]], + ) -> None: + """Set root overrides of logged user for a site. + + Only roots defined in project anatomy are stored. Roots with empty + value are removed, and all overrides for the site are removed + if no root value is passed. + + Args: + project_name (str): Project name. + site_id (str): Site id for which overrides are set. + roots (dict[str, Optional[str]]): Root values by root name. + + """ + response = self.put( + f"projects/{project_name}/roots/{site_id}", + **roots + ) + response.raise_for_status() + def get_project_roots_for_site( self, project_name: str, site_id: Optional[str] = None ) -> dict[str, str]: diff --git a/ayon_api/_api_helpers/reviewables.py b/ayon_api/_api_helpers/reviewables.py new file mode 100644 index 000000000..6039b2d37 --- /dev/null +++ b/ayon_api/_api_helpers/reviewables.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import typing +from typing import Any, Iterable, Optional + +from ayon_api.utils import prepare_query_string + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ReviewablesEntityType + + +class ReviewablesAPI(BaseServerAPI): + def get_reviewables( + self, + project_name: str, + entity_type: ReviewablesEntityType, + entity_ids: Iterable[str], + *, + latest: bool = False, + latest_done: bool = False, + ) -> list[dict[str, Any]]: + """Get reviewables for multiple entities of one entity type. + + Each item in output represents a version with its reviewables. + + Args: + project_name (str): Project name. + entity_type (ReviewablesEntityType): Entity type of passed + entity ids. Supported types are 'folder', 'product', + 'version' and 'task'. + entity_ids (Iterable[str]): Entity ids for which reviewables + should be returned. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. + + Returns: + list[dict[str, Any]]: Versions with reviewables. + + """ + query = self._prepare_reviewables_query(latest, latest_done) + response = self.post( + f"projects/{project_name}/{entity_type}s/reviewables/list{query}", + entityIds=list(entity_ids), + ) + response.raise_for_status() + return response.data + + def get_version_reviewables( + self, + project_name: str, + version_id: str, + *, + latest: bool = False, + latest_done: bool = False, + ) -> dict[str, Any]: + """Get reviewables of a version. + + Notes: + Server responds with an error if version does not have + any reviewables. + + Args: + project_name (str): Project name. + version_id (str): Version id. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. + + Returns: + dict[str, Any]: Version information with reviewables. + + """ + return self._get_entity_reviewables( + project_name, "versions", version_id, latest, latest_done + ) + + def get_product_reviewables( + self, + project_name: str, + product_id: str, + *, + latest: bool = False, + latest_done: bool = False, + ) -> list[dict[str, Any]]: + """Get reviewables of all versions of a product. + + Args: + project_name (str): Project name. + product_id (str): Product id. + latest (bool): Return only reviewables of the latest version. + latest_done (bool): Return only reviewables of the latest + approved versions. + + Returns: + list[dict[str, Any]]: Versions with reviewables. + + """ + return self._get_entity_reviewables( + project_name, "products", product_id, latest, latest_done + ) + + def get_folder_reviewables( + self, + project_name: str, + folder_id: str, + *, + latest: bool = False, + latest_done: bool = False, + ) -> list[dict[str, Any]]: + """Get reviewables of all versions under a folder. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + latest (bool): Return only reviewables of the latest versions. + latest_done (bool): Return only reviewables of the latest + approved versions. + + Returns: + list[dict[str, Any]]: Versions with reviewables. + + """ + return self._get_entity_reviewables( + project_name, "folders", folder_id, latest, latest_done + ) + + def get_task_reviewables( + self, + project_name: str, + task_id: str, + *, + latest: bool = False, + latest_done: bool = False, + ) -> list[dict[str, Any]]: + """Get reviewables of all versions linked to a task. + + Args: + project_name (str): Project name. + task_id (str): Task id. + latest (bool): Return only reviewables of the latest versions. + latest_done (bool): Return only reviewables of the latest + approved versions. + + Returns: + list[dict[str, Any]]: Versions with reviewables. + + """ + return self._get_entity_reviewables( + project_name, "tasks", task_id, latest, latest_done + ) + + def sort_version_reviewables( + self, + project_name: str, + version_id: str, + reviewable_ids: list[str], + ) -> None: + """Change order of reviewables of a version. + + Args: + project_name (str): Project name. + version_id (str): Version id. + reviewable_ids (list[str]): Reviewable (activity) ids in + the desired order. Must contain all reviewables of + the version. + + """ + response = self.patch( + f"projects/{project_name}/versions/{version_id}/reviewables", + sort=reviewable_ids, + ) + response.raise_for_status() + + def update_reviewable( + self, + project_name: str, + version_id: str, + reviewable_id: str, + *, + label: Optional[str] = None, + ) -> None: + """Update reviewable of a version. + + At this moment only label of reviewable can be changed. + + Args: + project_name (str): Project name. + version_id (str): Version id. + reviewable_id (str): Reviewable (activity) id. + label (Optional[str]): New label of reviewable. + + """ + kwargs = {} + if label is not None: + kwargs["label"] = label + + response = self.patch( + f"projects/{project_name}/versions/{version_id}" + f"/reviewables/{reviewable_id}", + **kwargs + ) + response.raise_for_status() + + def _prepare_reviewables_query( + self, latest: bool, latest_done: bool + ) -> str: + return prepare_query_string({ + "latest": "true" if latest else None, + "latest_done": "true" if latest_done else None, + }) + + def _get_entity_reviewables( + self, + project_name: str, + entity_type: str, + entity_id: str, + latest: bool, + latest_done: bool, + ) -> Any: + query = self._prepare_reviewables_query(latest, latest_done) + response = self.get( + f"projects/{project_name}/{entity_type}/{entity_id}" + f"/reviewables{query}" + ) + response.raise_for_status() + return response.data diff --git a/ayon_api/_api_helpers/services.py b/ayon_api/_api_helpers/services.py new file mode 100644 index 000000000..a64d3c208 --- /dev/null +++ b/ayon_api/_api_helpers/services.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from typing import Optional, Any + +from .base import BaseServerAPI + + +class ServicesAPI(BaseServerAPI): + def get_services(self) -> list[dict[str, Any]]: + """Get addon services registered on server. + + Only administrators receive the services, other users get an + empty list. + + Returns: + list[dict[str, Any]]: List of services. + + """ + response = self.get("services") + response.raise_for_status() + return response.data["services"] + + def spawn_service( + self, + service_name: str, + addon_name: str, + addon_version: str, + service: str, + hostname: str, + config: Optional[dict[str, Any]] = None, + ) -> None: + """Spawn (create) a new addon service. + + Requires administrator privileges. + + Args: + service_name (str): Unique name of the service. + addon_name (str): Name of addon that provides the service. + addon_version (str): Version of addon. + service (str): Name of service defined by the addon. + hostname (str): Name of host (ASH) where service should run. + config (Optional[dict[str, Any]]): Service configuration + e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', + 'storagePath' or 'registryAuth'. + + """ + body: dict[str, Any] = { + "addonName": addon_name, + "addonVersion": addon_version, + "service": service, + "hostname": hostname, + } + if config is not None: + body["config"] = config + response = self.put(f"services/{service_name}", **body) + response.raise_for_status() + + def update_service( + self, + service_name: str, + should_run: Optional[bool] = None, + hostname: Optional[str] = None, + error: Optional[str] = None, + config: Optional[dict[str, Any]] = None, + ) -> None: + """Update addon service. + + Requires administrator privileges. + + Args: + service_name (str): Name of the service. + should_run (Optional[bool]): Service should be running. + hostname (Optional[str]): Name of host where service should run. + error (Optional[str]): Error message. Ignored when service + should be running. + config (Optional[dict[str, Any]]): Service configuration + values to change e.g. 'volumes', 'ports', 'memLimit', + 'user', 'env', 'storagePath' or 'registryAuth'. + + """ + body: dict[str, Any] = {} + if config: + body.update(config) + for key, value in ( + ("shouldRun", should_run), + ("hostname", hostname), + ("error", error), + ): + if value is not None: + body[key] = value + + response = self.patch(f"services/{service_name}", **body) + response.raise_for_status() + + def delete_service(self, service_name: str) -> None: + """Delete addon service. + + Requires administrator privileges. + + Args: + service_name (str): Name of the service. + + """ + response = self.delete(f"services/{service_name}") + response.raise_for_status() + + def get_hosts(self) -> list[dict[str, Any]]: + """Get AYON service hosts (ASH) registered on server. + + Host is registered automatically when it sends heartbeat to server. + + Example output:: + + [ + { + "name": "my-host", + "lastSeen": "2025-01-01T00:00:00", + "health": {"cpu": 0.5, "mem": 42.0}, + } + ] + + Returns: + list[dict[str, Any]]: List of hosts. + + """ + response = self.get("hosts") + response.raise_for_status() + return response.data["hosts"] diff --git a/ayon_api/_api_helpers/system.py b/ayon_api/_api_helpers/system.py new file mode 100644 index 000000000..dda245279 --- /dev/null +++ b/ayon_api/_api_helpers/system.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +from typing import Optional, Any + +from ayon_api.utils import prepare_query_string + +from .base import BaseServerAPI + + +class SystemAPI(BaseServerAPI): + def get_sites( + self, + platform_name: Optional[str] = None, + hostname: Optional[str] = None, + ) -> list[dict[str, Any]]: + """Get sites registered on server. + + Non-manager users receive only sites they have used. + + Example output:: + + [ + { + "id": "site-id", + "platform": "windows", + "hostname": "my-machine", + "version": "1.0.0", + "users": ["admin"], + } + ] + + Args: + platform_name (Optional[str]): Filter sites by platform name. + Possible values are 'windows', 'linux' and 'darwin'. + hostname (Optional[str]): Filter sites by machine hostname. + + Returns: + list[dict[str, Any]]: List of sites. + + """ + query = prepare_query_string({ + "platform": platform_name or None, + "hostname": hostname or None, + }) + response = self.get(f"system/sites{query}") + response.raise_for_status() + return response.data + + def delete_site(self, site_id: str) -> None: + """Unregister site from server. + + Requires administrator privileges. + + Args: + site_id (str): Site id. + + """ + response = self.delete(f"system/sites/{site_id}") + response.raise_for_status() + + def get_restart_required(self) -> dict[str, Any]: + """Get information whether server requires restart. + + Example output:: + + { + "required": True, + "reason": "Server restart is required", + } + + Returns: + dict[str, Any]: Restart required information with keys + 'required' and 'reason'. + + """ + response = self.get("system/restartRequired") + response.raise_for_status() + return response.data + + def set_restart_required( + self, + required: bool, + reason: Optional[str] = None, + ) -> None: + """Set or clear the server restart required flag. + + This notifies administrators that the server needs to be restarted. + Use 'trigger_server_restart' to actually restart the server. + + Args: + required (bool): Server requires restart. + reason (Optional[str]): Human-readable reason for the restart. + + """ + body: dict[str, Any] = {"required": required} + if reason is not None: + body["reason"] = reason + response = self.post("system/restartRequired", **body) + response.raise_for_status() + + def get_server_config_schema(self) -> dict[str, Any]: + """Get JSON schema of server configuration. + + Returns: + dict[str, Any]: Server configuration schema. + + """ + response = self.get("config/schema") + response.raise_for_status() + return response.data + + def update_server_config(self, config: dict[str, Any]) -> None: + """Partially update server configuration. + + Values are recursively merged into current server configuration. + Keys that are not passed remain unchanged. Use the same + structure as 'get_server_config' returns, e.g. + '{"studio_name": "My Studio"}'. + + Requires administrator privileges. + + Args: + config (dict[str, Any]): Server configuration values to change. + + """ + response = self.patch("config", **config) + response.raise_for_status() + + def get_enums(self) -> list[dict[str, Any]]: + """Get available enum resolvers. + + Example output:: + + [ + { + "name": "statuses", + "label": "Statuses", + ... + } + ] + + Returns: + list[dict[str, Any]]: List of enum resolvers information. + + """ + response = self.get("enum") + response.raise_for_status() + return response.data + + def get_enum( + self, + enum_name: str, + project_name: Optional[str] = None, + user_name: Optional[str] = None, + context: Optional[dict[str, Any]] = None, + ) -> list[dict[str, Any]]: + """Get enum items resolved by server. + + Accepted context values depend on the enum resolver, use 'get_enums' + to find out which parameters a resolver accepts. + + Example output:: + + [ + { + "value": "in_progress", + "label": "In progress", + ... + } + ] + + Args: + enum_name (str): Name of the enum e.g. 'statuses'. + project_name (Optional[str]): Project name for project context. + user_name (Optional[str]): Resolve enum for different user. + Only administrators can use this. + context (Optional[dict[str, Any]]): Additional context + parameters for the resolver. Lists are joined with comma. + + Returns: + list[dict[str, Any]]: List of enum items. + + """ + query_data: dict[str, Any] = {} + if context: + for key, value in context.items(): + if isinstance(value, bool): + value = "true" if value else "false" + elif isinstance(value, (list, tuple, set)): + value = ",".join(str(item) for item in value) + query_data[key] = value + + if project_name is not None: + query_data["project_name"] = project_name + if user_name is not None: + query_data["user"] = user_name + + query = prepare_query_string(query_data) + response = self.get(f"enum/{enum_name}{query}") + response.raise_for_status() + return response.data diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 1cae5caa5..3b4b55470 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from typing import Optional, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any, Literal from ayon_api.utils import ( prepare_list_filters, @@ -513,3 +513,32 @@ def delete_task(self, project_name: str, task_id: str) -> None: f"projects/{project_name}/tasks/{task_id}" ) response.raise_for_status() + + def assign_users_to_task( + self, + project_name: str, + task_id: str, + users: Iterable[str], + mode: Literal["add", "remove", "set"] = "add", + ) -> None: + """Change users assigned to a task. + + Notes: + Users without manager role can only assign or unassign + themselves. + + Args: + project_name (str): Project name. + task_id (str): Task id. + users (Iterable[str]): User names. + mode (Literal["add", "remove", "set"]): How passed users are + applied. 'add' adds users to current assignees, 'remove' + removes them and 'set' replaces assignees with passed users. + + """ + response = self.post( + f"projects/{project_name}/tasks/{task_id}/assign", + mode=mode, + users=list(users), + ) + response.raise_for_status() diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 9d534fd5e..7cac56f88 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -358,6 +358,176 @@ def update_thumbnail_from_stream( ) response.raise_for_status() + def set_entity_thumbnail( + self, + project_name: str, + entity_type: str, + entity_id: str, + src_filepath: str, + ) -> str: + """Upload new thumbnail for an entity. + + Created thumbnail is automatically set on the entity. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type. Supported types are 'folder', + 'task', 'version' and 'workfile'. + entity_id (str): Entity id. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + 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.") + + endpoint = self._get_entity_thumbnail_endpoint( + project_name, entity_type, entity_id + ) + mime_type = get_media_mime_type(src_filepath) + response = self.upload_file( + endpoint, + src_filepath, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + return response.json()["id"] + + def set_entity_thumbnail_from_stream( + self, + project_name: str, + entity_type: str, + entity_id: str, + stream: StreamType, + ) -> str: + """Upload new thumbnail for an entity from byte stream. + + Created thumbnail is automatically set on the entity. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type. Supported types are 'folder', + 'task', 'version' and 'workfile'. + entity_id (str): Entity id. + stream (StreamType): Thumbnail content stream. + + Returns: + str: Created thumbnail id. + + """ + endpoint = self._get_entity_thumbnail_endpoint( + project_name, entity_type, entity_id + ) + mime_type = get_media_mime_type_for_stream(stream) + response = self.upload_file_from_stream( + endpoint, + stream, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + return response.json()["id"] + + def get_project_thumbnail(self, project_name: str) -> ThumbnailContent: + """Get project thumbnail. + + Args: + project_name (str): Project name. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + response = self.raw_get(f"projects/{project_name}/thumbnail") + if ( + response.status_code == 200 + and "X-Thumbnail-Id" not in response.headers + ): + # Project skeleton thumbnail does not return thumbnail id + # header, use reserved id of project thumbnail + return ThumbnailContent( + project_name, + "0" * 32, + response.content, + response.content_type, + ) + return self._prepare_thumbnail_content(project_name, response) + + def set_project_thumbnail( + self, project_name: str, src_filepath: str + ) -> None: + """Upload project thumbnail. + + Args: + project_name (str): Project name. + 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"api/projects/{project_name}/thumbnail", + src_filepath, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + + def set_project_thumbnail_from_stream( + self, project_name: str, stream: StreamType + ) -> None: + """Upload project thumbnail from byte stream. + + Args: + project_name (str): Project name. + stream (StreamType): Thumbnail content stream. + + """ + mime_type = get_media_mime_type_for_stream(stream) + response = self.upload_file_from_stream( + f"api/projects/{project_name}/thumbnail", + stream, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + + def _get_entity_thumbnail_endpoint( + self, project_name: str, entity_type: str, entity_id: str + ) -> str: + if entity_type in ( + "folder", + "task", + "version", + "workfile", + ): + entity_type += "s" + + if entity_type not in ( + "folders", + "tasks", + "versions", + "workfiles", + ): + raise ValueError( + f"Entity type '{entity_type}' does not support thumbnails." + ) + return ( + f"api/projects/{project_name}/{entity_type}/{entity_id}/thumbnail" + ) + def _prepare_thumbnail_content( self, project_name: str, diff --git a/ayon_api/_api_helpers/users.py b/ayon_api/_api_helpers/users.py new file mode 100644 index 000000000..b4bd5c5d3 --- /dev/null +++ b/ayon_api/_api_helpers/users.py @@ -0,0 +1,592 @@ +from __future__ import annotations + +from typing import Optional, Any + +from ayon_api.utils import prepare_query_string + +from .base import BaseServerAPI + + +class UsersAPI(BaseServerAPI): + """Users, access groups, teams and project users REST endpoints.""" + + # --- Users --- + def create_user( + self, + user_name: str, + *, + password: Optional[str] = None, + api_key: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + ) -> None: + """Create new user. + + User level and access is defined in 'data', e.g. 'isAdmin', + 'isManager', 'isService', 'isGuest', 'accessGroups' + ({project_name: [access_group_name, ...]}) or + 'defaultAccessGroups' ([access_group_name, ...]). + + Args: + user_name (str): Name of the user. + password (Optional[str]): Password of the user. Service users + cannot have password. + api_key (Optional[str]): Api key for a service user. + attrib (Optional[dict[str, Any]]): User attributes. + data (Optional[dict[str, Any]]): User data. + active (Optional[bool]): User active state. + + """ + body = { + key: value + for key, value in ( + ("password", password), + ("apiKey", api_key), + ("attrib", attrib), + ("data", data), + ("active", active), + ) + if value is not None + } + response = self.put(f"users/{user_name}", **body) + response.raise_for_status() + + def update_user( + self, + user_name: str, + *, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + ) -> None: + """Update user. + + Non-manager users can change only their own attributes. + + Args: + user_name (str): Name of the user. + attrib (Optional[dict[str, Any]]): User attributes to change. + data (Optional[dict[str, Any]]): User data to change. + active (Optional[bool]): User active state. + + """ + body = { + key: value + for key, value in ( + ("attrib", attrib), + ("data", data), + ("active", active), + ) + if value is not None + } + response = self.patch(f"users/{user_name}", **body) + response.raise_for_status() + + def delete_user(self, user_name: str) -> None: + """Delete user. + + Args: + user_name (str): Name of the user. + + """ + response = self.delete(f"users/{user_name}") + response.raise_for_status() + + def rename_user(self, user_name: str, new_user_name: str) -> None: + """Rename user. + + This is a manager-only operation. Target user name must not exist. + + Args: + user_name (str): Current name of the user. + new_user_name (str): New name of the user. + + """ + response = self.post( + f"users/{user_name}/rename", name=new_user_name + ) + response.raise_for_status() + + def change_password( + self, + user_name: str, + *, + password: Optional[str] = None, + api_key: Optional[str] = None, + ) -> None: + """Change user password or service user api key. + + Only one of 'password' or 'api_key' should be passed. If both are + passed the password is changed. + + Args: + user_name (str): Name of the user. + password (Optional[str]): New password. + api_key (Optional[str]): New api key of a service user. Must be + 32 characters long hex string. + + """ + body = {} + if password is not None: + body["password"] = password + if api_key is not None: + body["apiKey"] = api_key + if not body: + raise ValueError("Password or api key must be passed.") + response = self.patch(f"users/{user_name}/password", **body) + response.raise_for_status() + + def set_user_access_groups( + self, + user_name: str, + access_groups: dict[str, list[str]], + ) -> None: + """Set user access groups on projects. + + Projects that are not in 'access_groups' are not affected. Pass + an empty list to remove access to a project. + + Args: + user_name (str): Name of the user. + access_groups (dict[str, list[str]]): Access groups by + project name. + + """ + response = self.patch( + f"users/{user_name}/accessGroups", + accessGroups=[ + {"project": project_name, "accessGroups": list(groups)} + for project_name, groups in access_groups.items() + ], + ) + response.raise_for_status() + + def get_user_sessions(self, user_name: str) -> list[dict[str, Any]]: + """Get active sessions of user. + + Args: + user_name (str): Name of the user. + + Returns: + list[dict[str, Any]]: Sessions with 'token', 'isService', + 'lastUsed' and 'clientInfo' keys. + + """ + response = self.get(f"users/{user_name}/sessions") + response.raise_for_status() + return response.data["sessions"] + + def delete_user_session(self, user_name: str, session_id: str) -> None: + """Delete user session (logout). + + Args: + user_name (str): Name of the user. + session_id (str): Session id (token). + + """ + response = self.delete(f"users/{user_name}/sessions/{session_id}") + response.raise_for_status() + + def get_user_api_keys(self, user_name: str) -> list[dict[str, Any]]: + """Get api keys of user. + + Args: + user_name (str): Name of the user. + + Returns: + list[dict[str, Any]]: Api keys with 'id', 'label', 'preview', + 'created' and 'expires' keys. + + """ + response = self.get(f"users/{user_name}/apikeys") + response.raise_for_status() + return response.data + + def create_user_api_key( + self, + user_name: str, + label: str, + key: str, + *, + expires: Optional[float] = None, + ) -> None: + """Create api key for user. + + Args: + user_name (str): Name of the user. + label (str): Label of the api key. Must be unique for the user. + key (str): The api key value. + expires (Optional[float]): Expiration timestamp. + + """ + response = self.post( + f"users/{user_name}/apikeys", + label=label, + key=key, + expires=expires, + ) + response.raise_for_status() + + def update_user_api_key( + self, + user_name: str, + api_key_id: str, + *, + label: Optional[str] = None, + expires: Optional[float] = None, + ) -> None: + """Update api key of user. + + Args: + user_name (str): Name of the user. + api_key_id (str): Api key id. + label (Optional[str]): New label of the api key. + expires (Optional[float]): New expiration timestamp. + + """ + response = self.patch( + f"users/{user_name}/apikeys/{api_key_id}", + label=label, + expires=expires, + ) + response.raise_for_status() + + def delete_user_api_key(self, user_name: str, api_key_id: str) -> None: + """Delete api key of user. + + Args: + user_name (str): Name of the user. + api_key_id (str): Api key id. + + """ + response = self.delete(f"users/{user_name}/apikeys/{api_key_id}") + response.raise_for_status() + + def get_user_permissions( + self, + user_name: Optional[str] = None, + project_name: Optional[str] = None, + ) -> dict[str, Any]: + """Get user permissions. + + Without 'user_name' are returned permissions of current user. For + current user without 'project_name' is returned user level with + studio and per-project permissions. Otherwise, are returned + studio permissions, or project permissions if 'project_name' + is passed. + + Args: + user_name (Optional[str]): Name of the user. Current user is used + if not passed. + project_name (Optional[str]): Project name. + + Returns: + dict[str, Any]: User permissions. + + """ + if user_name is None: + user_name = "me" + endpoint = f"users/{user_name}/permissions" + if project_name: + endpoint += f"/{project_name}" + response = self.get(endpoint) + response.raise_for_status() + return response.data + + # --- Access groups --- + def get_access_groups( + self, project_name: Optional[str] = None + ) -> list[dict[str, Any]]: + """Get access groups. + + Args: + project_name (Optional[str]): Project name. Studio level access + groups are returned if not passed. + + Returns: + list[dict[str, Any]]: Access groups with 'name' and + 'isProjectLevel' keys. + + """ + response = self.get(f"accessGroups/{project_name or '_'}") + response.raise_for_status() + return response.data + + def get_access_group_schema( + self, project_name: Optional[str] = None + ) -> dict[str, Any]: + """Get access group (permissions) settings schema. + + Args: + project_name (Optional[str]): Project name used as context. + + Returns: + dict[str, Any]: Access group schema. + + """ + query = prepare_query_string({"project_name": project_name or None}) + response = self.get(f"accessGroups/_schema{query}") + response.raise_for_status() + return response.data + + def get_access_group( + self, + access_group_name: str, + project_name: Optional[str] = None, + ) -> dict[str, Any]: + """Get access group definition (permissions). + + Args: + access_group_name (str): Access group name. + project_name (Optional[str]): Project name. Studio level access + group is returned if not passed. + + Returns: + dict[str, Any]: Access group permissions. + + """ + response = self.get( + f"accessGroups/{access_group_name}/{project_name or '_'}" + ) + response.raise_for_status() + return response.data + + def save_access_group( + self, + access_group_name: str, + permissions: dict[str, Any], + project_name: Optional[str] = None, + ) -> None: + """Create or update access group. + + Args: + access_group_name (str): Access group name. + permissions (dict[str, Any]): Access group permissions. + project_name (Optional[str]): Project name. Studio level access + group is saved if not passed. + + """ + response = self.put( + f"accessGroups/{access_group_name}/{project_name or '_'}", + **permissions, + ) + response.raise_for_status() + + def delete_access_group( + self, + access_group_name: str, + project_name: Optional[str] = None, + ) -> None: + """Delete access group. + + Args: + access_group_name (str): Access group name. + project_name (Optional[str]): Project name. Studio level access + group is deleted if not passed. + + """ + response = self.delete( + f"accessGroups/{access_group_name}/{project_name or '_'}" + ) + response.raise_for_status() + + def set_projects_access( + self, access: dict[str, dict[str, list[str]]] + ) -> None: + """Set access of multiple users to multiple projects. + + Projects not present in the payload are not affected. Pass an empty + list to revoke access to a project. + + Example:: + + { + "alice": { + "MyProject01": ["editor"], + "MyProject02": ["artist"], + }, + "bob": { + "MyProject01": ["artist", "reviewer"], + "MyProject02": [], + }, + } + + Args: + access (dict[str, dict[str, list[str]]]): Access groups by + project name by user name. + + """ + response = self.raw_post("access", json=access) + response.raise_for_status() + + # --- Teams --- + def get_teams( + self, + project_name: str, + *, + show_members: bool = False, + ) -> list[dict[str, Any]]: + """Get teams in a project. + + Args: + project_name (str): Project name. + show_members (bool): Include team members in the response. + + Returns: + list[dict[str, Any]]: Teams with 'name', 'memberCount', + 'leaders' and optionally 'members' keys. + + """ + query = prepare_query_string( + {"show_members": "true" if show_members else None} + ) + response = self.get(f"projects/{project_name}/teams{query}") + response.raise_for_status() + return response.data + + def update_teams( + self, + project_name: str, + teams: list[dict[str, Any]], + ) -> None: + """Create or replace multiple teams in a project. + + Teams that are not in 'teams' are not affected. + + Args: + project_name (str): Project name. + teams (list[dict[str, Any]]): Teams with 'name' and 'members' + keys. Member is a dictionary with 'name', 'leader' + and 'roles' keys. + + """ + response = self.raw_patch( + f"projects/{project_name}/teams", json=teams + ) + response.raise_for_status() + + def save_team( + self, + project_name: str, + team_name: str, + members: list[dict[str, Any]], + ) -> None: + """Create or replace a team in a project. + + Args: + project_name (str): Project name. + team_name (str): Team name. + members (list[dict[str, Any]]): Team members. Member is + a dictionary with 'name', 'leader' and 'roles' keys. + + """ + response = self.put( + f"projects/{project_name}/teams/{team_name}", + members=members, + ) + response.raise_for_status() + + def delete_team(self, project_name: str, team_name: str) -> None: + """Delete a team from a project. + + Args: + project_name (str): Project name. + team_name (str): Team name. + + """ + response = self.delete(f"projects/{project_name}/teams/{team_name}") + response.raise_for_status() + + def save_team_member( + self, + project_name: str, + team_name: str, + member_name: str, + *, + leader: bool = False, + roles: Optional[list[str]] = None, + ) -> None: + """Add or replace a team member. + + Args: + project_name (str): Project name. + team_name (str): Team name. + member_name (str): User name. + leader (bool): Is user a leader of the team. + roles (Optional[list[str]]): User roles in the team. + + """ + response = self.put( + f"projects/{project_name}/teams/{team_name}" + f"/members/{member_name}", + name=member_name, + leader=leader, + roles=list(roles or []), + ) + response.raise_for_status() + + def delete_team_member( + self, + project_name: str, + team_name: str, + member_name: str, + ) -> None: + """Remove a member from a team. + + Args: + project_name (str): Project name. + team_name (str): Team name. + member_name (str): User name. + + """ + response = self.delete( + f"projects/{project_name}/teams/{team_name}" + f"/members/{member_name}" + ) + response.raise_for_status() + + # --- Project users --- + def get_project_users(self, project_name: str) -> dict[str, list[str]]: + """Get users with explicitly set access groups on a project. + + Managers and admins are not included as they have access to all + projects. + + Args: + project_name (str): Project name. + + Returns: + dict[str, list[str]]: Access groups by user name. + + """ + response = self.get(f"projects/{project_name}/users") + response.raise_for_status() + return response.data + + def set_project_user_access_groups( + self, + project_name: str, + user_name: str, + access_groups: list[str], + ) -> dict[str, list[str]]: + """Set access groups of a user on a project. + + Pass an empty list to remove user access to the project. + + Args: + project_name (str): Project name. + user_name (str): User name. + access_groups (list[str]): Access group names. + + Returns: + dict[str, list[str]]: Access groups by user name of all users + on the project. + + """ + response = self.raw_patch( + f"projects/{project_name}/users/{user_name}", + json=list(access_groups), + ) + response.raise_for_status() + return response.data diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7cb03c4d4..c380369ab 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -94,6 +94,10 @@ ActionsAPI, LinksAPI, ListsAPI, + UsersAPI, + ServicesAPI, + SystemAPI, + ReviewablesAPI, ) if typing.TYPE_CHECKING: @@ -249,6 +253,10 @@ class ServerAPI( ActionsAPI, LinksAPI, ListsAPI, + UsersAPI, + ServicesAPI, + SystemAPI, + ReviewablesAPI, ): """Base handler of connection to server. diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 5869d1bd4..68a272676 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -51,6 +51,13 @@ "review-session", ] +ReviewablesEntityType = Literal[ + "folder", + "product", + "version", + "task", +] + EventFilterValueType = Union[ None, str, int, float, From e3d1ca83198acdefb346e41fe2ea8228f883eb05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:28:59 +0200 Subject: [PATCH 2/5] Validate server version for endpoints added after 1.14 Raise 'UnsupportedServerVersion' when a method uses an endpoint or argument that is not available on the connected server: - 1.15.0: rename_project, get_project_thumbnail, set_project_thumbnail, set_project_thumbnail_from_stream - 1.15.4: 'search' argument of search_folders - 1.15.9: update_server_config, set_entity_list_items_order - 1.16.4: get_enums, get_reviewables, 'latest' and 'latest_done' arguments of entity reviewables getters - 1.16.7: 'error' argument of update_service - 1.16.8: migrate_addon_settings rename_user uses the deprecated PATCH endpoint on servers older than 1.15.0 instead of failing. Co-Authored-By: Claude Opus 5.5 --- ayon_api/_api.py | 51 +++++++++++++++++++++++++ ayon_api/_api_helpers/bundles_addons.py | 9 +++++ ayon_api/_api_helpers/folders.py | 9 +++++ ayon_api/_api_helpers/lists.py | 9 +++++ ayon_api/_api_helpers/projects.py | 9 +++++ ayon_api/_api_helpers/reviewables.py | 33 ++++++++++++++++ ayon_api/_api_helpers/services.py | 16 ++++++++ ayon_api/_api_helpers/system.py | 17 +++++++++ ayon_api/_api_helpers/thumbnails.py | 23 +++++++++++ ayon_api/_api_helpers/users.py | 12 ++++-- 10 files changed, 185 insertions(+), 3 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 32f1f9792..80b6c3524 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4057,6 +4057,9 @@ def migrate_addon_settings( dict[str, Any]: Server response with 'events' created during the migration. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.8. + """ con = get_server_api_connection() return con.migrate_addon_settings( @@ -5112,6 +5115,9 @@ def rename_project( new_project_code (Optional[str]): New project code. Project code is unchanged if not passed. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ con = get_server_api_connection() return con.rename_project( @@ -6069,6 +6075,10 @@ def search_folders( Returns: list[str]: Matching folder ids. + Raises: + UnsupportedServerVersion: 'search' is used with server version + lower than 1.15.4. + """ con = get_server_api_connection() return con.search_folders( @@ -9652,6 +9662,9 @@ def set_entity_list_items_order( order (list[str]): Item ids in desired order. Must contain each item of the entity list exactly once. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.9. + """ con = get_server_api_connection() return con.set_entity_list_items_order( @@ -10022,6 +10035,9 @@ def get_project_thumbnail( ThumbnailContent: Thumbnail content wrapper. Does not have to be valid. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ con = get_server_api_connection() return con.get_project_thumbnail( @@ -10041,6 +10057,7 @@ def set_project_thumbnail( Raises: ValueError: When thumbnail source cannot be processed. + UnsupportedServerVersion: Server version is lower than 1.15.0. """ con = get_server_api_connection() @@ -10060,6 +10077,9 @@ def set_project_thumbnail_from_stream( project_name (str): Project name. stream (StreamType): Thumbnail content stream. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ con = get_server_api_connection() return con.set_project_thumbnail_from_stream( @@ -10717,6 +10737,7 @@ def spawn_service( config (Optional[dict[str, Any]]): Service configuration e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', 'storagePath' or 'registryAuth'. + 'registryAuth' is ignored by AYON server older than 1.16.7. """ con = get_server_api_connection() @@ -10750,6 +10771,11 @@ def update_service( config (Optional[dict[str, Any]]): Service configuration values to change e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', 'storagePath' or 'registryAuth'. + 'registryAuth' is ignored by AYON server older than 1.16.7. + + Raises: + UnsupportedServerVersion: 'error' is used with server version + lower than 1.16.7. """ con = get_server_api_connection() @@ -10921,6 +10947,9 @@ def update_server_config( Args: config (dict[str, Any]): Server configuration values to change. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.9. + """ con = get_server_api_connection() return con.update_server_config( @@ -10944,6 +10973,9 @@ def get_enums() -> list[dict[str, Any]]: Returns: list[dict[str, Any]]: List of enum resolvers information. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.4. + """ con = get_server_api_connection() return con.get_enums() @@ -11017,6 +11049,9 @@ def get_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.4. + """ con = get_server_api_connection() return con.get_reviewables( @@ -11051,6 +11086,10 @@ def get_version_reviewables( Returns: dict[str, Any]: Version information with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ con = get_server_api_connection() return con.get_version_reviewables( @@ -11080,6 +11119,10 @@ def get_product_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ con = get_server_api_connection() return con.get_product_reviewables( @@ -11109,6 +11152,10 @@ def get_folder_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ con = get_server_api_connection() return con.get_folder_reviewables( @@ -11138,6 +11185,10 @@ def get_task_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ con = get_server_api_connection() return con.get_task_reviewables( diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index fc91d4604..a38449f88 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -4,6 +4,7 @@ import typing from typing import Optional, Any, Literal +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import ( RequestTypes, prepare_query_string, @@ -1392,7 +1393,15 @@ def migrate_addon_settings( dict[str, Any]: Server response with 'events' created during the migration. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.8. + """ + if self.get_server_version_tuple() < (1, 16, 8): + raise UnsupportedServerVersion( + "Function 'migrate_addon_settings' is supported" + " for AYON server 1.16.8 and above." + ) body = { "sourceVersion": source_version, "targetVersion": target_version, diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index 26f84888d..47920ed68 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -522,7 +522,16 @@ def search_folders( Returns: list[str]: Matching folder ids. + Raises: + UnsupportedServerVersion: 'search' is used with server version + lower than 1.15.4. + """ + if search and self.get_server_version_tuple() < (1, 15, 4): + raise UnsupportedServerVersion( + "Argument 'search' of 'search_folders' is supported" + " for AYON server 1.15.4 and above." + ) body = { key: value for key, value in ( diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index c1e7a6db2..99dfec895 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -4,6 +4,7 @@ import typing from typing import Optional, Iterable, Any, Generator +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import NOT_SET, create_entity_id from ayon_api.graphql_queries import entity_lists_graphql_query @@ -706,7 +707,15 @@ def set_entity_list_items_order( order (list[str]): Item ids in desired order. Must contain each item of the entity list exactly once. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.9. + """ + if self.get_server_version_tuple() < (1, 15, 9): + raise UnsupportedServerVersion( + "Function 'set_entity_list_items_order' is supported" + " for AYON server 1.15.9 and above." + ) response = self.post( f"projects/{project_name}/lists/{list_id}/order", order=order, diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 2113b82d1..c68adf603 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -11,6 +11,7 @@ PROJECT_NAME_REGEX, DEFAULT_PRODUCT_TYPE_FIELDS, ) +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import prepare_query_string, fill_own_attribs from ayon_api.graphql_queries import projects_graphql_query @@ -633,7 +634,15 @@ def rename_project( new_project_code (Optional[str]): New project code. Project code is unchanged if not passed. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ + if self.get_server_version_tuple() < (1, 15, 0): + raise UnsupportedServerVersion( + "Function 'rename_project' is supported" + " for AYON server 1.15.0 and above." + ) body = {"name": new_project_name} if new_project_code is not None: body["code"] = new_project_code diff --git a/ayon_api/_api_helpers/reviewables.py b/ayon_api/_api_helpers/reviewables.py index 6039b2d37..2e8199ea2 100644 --- a/ayon_api/_api_helpers/reviewables.py +++ b/ayon_api/_api_helpers/reviewables.py @@ -3,6 +3,7 @@ import typing from typing import Any, Iterable, Optional +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import prepare_query_string from .base import BaseServerAPI @@ -39,7 +40,15 @@ def get_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.4. + """ + if self.get_server_version_tuple() < (1, 16, 4): + raise UnsupportedServerVersion( + "Function 'get_reviewables' is supported" + " for AYON server 1.16.4 and above." + ) query = self._prepare_reviewables_query(latest, latest_done) response = self.post( f"projects/{project_name}/{entity_type}s/reviewables/list{query}", @@ -72,6 +81,10 @@ def get_version_reviewables( Returns: dict[str, Any]: Version information with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ return self._get_entity_reviewables( project_name, "versions", version_id, latest, latest_done @@ -97,6 +110,10 @@ def get_product_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ return self._get_entity_reviewables( project_name, "products", product_id, latest, latest_done @@ -122,6 +139,10 @@ def get_folder_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ return self._get_entity_reviewables( project_name, "folders", folder_id, latest, latest_done @@ -147,6 +168,10 @@ def get_task_reviewables( Returns: list[dict[str, Any]]: Versions with reviewables. + Raises: + UnsupportedServerVersion: 'latest' or 'latest_done' is used + with server version lower than 1.16.4. + """ return self._get_entity_reviewables( project_name, "tasks", task_id, latest, latest_done @@ -207,6 +232,14 @@ def update_reviewable( def _prepare_reviewables_query( self, latest: bool, latest_done: bool ) -> str: + if ( + (latest or latest_done) + and self.get_server_version_tuple() < (1, 16, 4) + ): + raise UnsupportedServerVersion( + "Arguments 'latest' and 'latest_done' are supported" + " for AYON server 1.16.4 and above." + ) return prepare_query_string({ "latest": "true" if latest else None, "latest_done": "true" if latest_done else None, diff --git a/ayon_api/_api_helpers/services.py b/ayon_api/_api_helpers/services.py index a64d3c208..82a74b39d 100644 --- a/ayon_api/_api_helpers/services.py +++ b/ayon_api/_api_helpers/services.py @@ -2,6 +2,8 @@ from typing import Optional, Any +from ayon_api.exceptions import UnsupportedServerVersion + from .base import BaseServerAPI @@ -42,6 +44,7 @@ def spawn_service( config (Optional[dict[str, Any]]): Service configuration e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', 'storagePath' or 'registryAuth'. + 'registryAuth' is ignored by AYON server older than 1.16.7. """ body: dict[str, Any] = { @@ -76,8 +79,21 @@ def update_service( config (Optional[dict[str, Any]]): Service configuration values to change e.g. 'volumes', 'ports', 'memLimit', 'user', 'env', 'storagePath' or 'registryAuth'. + 'registryAuth' is ignored by AYON server older than 1.16.7. + + Raises: + UnsupportedServerVersion: 'error' is used with server version + lower than 1.16.7. """ + if ( + error is not None + and self.get_server_version_tuple() < (1, 16, 7) + ): + raise UnsupportedServerVersion( + "Argument 'error' of 'update_service' is supported" + " for AYON server 1.16.7 and above." + ) body: dict[str, Any] = {} if config: body.update(config) diff --git a/ayon_api/_api_helpers/system.py b/ayon_api/_api_helpers/system.py index dda245279..6b809834d 100644 --- a/ayon_api/_api_helpers/system.py +++ b/ayon_api/_api_helpers/system.py @@ -2,6 +2,7 @@ from typing import Optional, Any +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import prepare_query_string from .base import BaseServerAPI @@ -122,7 +123,15 @@ def update_server_config(self, config: dict[str, Any]) -> None: Args: config (dict[str, Any]): Server configuration values to change. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.9. + """ + if self.get_server_version_tuple() < (1, 15, 9): + raise UnsupportedServerVersion( + "Function 'update_server_config' is supported" + " for AYON server 1.15.9 and above." + ) response = self.patch("config", **config) response.raise_for_status() @@ -142,7 +151,15 @@ def get_enums(self) -> list[dict[str, Any]]: Returns: list[dict[str, Any]]: List of enum resolvers information. + Raises: + UnsupportedServerVersion: Server version is lower than 1.16.4. + """ + if self.get_server_version_tuple() < (1, 16, 4): + raise UnsupportedServerVersion( + "Function 'get_enums' is supported" + " for AYON server 1.16.4 and above." + ) response = self.get("enum") response.raise_for_status() return response.data diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 7cac56f88..6aafc3416 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -5,6 +5,7 @@ import typing from typing import Optional +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import ( get_media_mime_type_for_stream, get_media_mime_type, @@ -444,7 +445,15 @@ def get_project_thumbnail(self, project_name: str) -> ThumbnailContent: ThumbnailContent: Thumbnail content wrapper. Does not have to be valid. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ + if self.get_server_version_tuple() < (1, 15, 0): + raise UnsupportedServerVersion( + "Function 'get_project_thumbnail' is supported" + " for AYON server 1.15.0 and above." + ) response = self.raw_get(f"projects/{project_name}/thumbnail") if ( response.status_code == 200 @@ -471,8 +480,14 @@ def set_project_thumbnail( Raises: ValueError: When thumbnail source cannot be processed. + UnsupportedServerVersion: Server version is lower than 1.15.0. """ + if self.get_server_version_tuple() < (1, 15, 0): + raise UnsupportedServerVersion( + "Function 'set_project_thumbnail' is supported" + " for AYON server 1.15.0 and above." + ) if not os.path.exists(src_filepath): raise ValueError("Entered filepath does not exist.") @@ -494,7 +509,15 @@ def set_project_thumbnail_from_stream( project_name (str): Project name. stream (StreamType): Thumbnail content stream. + Raises: + UnsupportedServerVersion: Server version is lower than 1.15.0. + """ + if self.get_server_version_tuple() < (1, 15, 0): + raise UnsupportedServerVersion( + "Function 'set_project_thumbnail_from_stream' is supported" + " for AYON server 1.15.0 and above." + ) mime_type = get_media_mime_type_for_stream(stream) response = self.upload_file_from_stream( f"api/projects/{project_name}/thumbnail", diff --git a/ayon_api/_api_helpers/users.py b/ayon_api/_api_helpers/users.py index b4bd5c5d3..268e05ca4 100644 --- a/ayon_api/_api_helpers/users.py +++ b/ayon_api/_api_helpers/users.py @@ -103,9 +103,15 @@ def rename_user(self, user_name: str, new_user_name: str) -> None: new_user_name (str): New name of the user. """ - response = self.post( - f"users/{user_name}/rename", name=new_user_name - ) + if self.get_server_version_tuple() < (1, 15, 0): + # Older servers have only the deprecated PATCH endpoint + response = self.patch( + f"users/{user_name}/rename", newName=new_user_name + ) + else: + response = self.post( + f"users/{user_name}/rename", name=new_user_name + ) response.raise_for_status() def change_password( From ab06567b049b2fde8f9b15fbdea84416c33aeb82 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:42:26 +0200 Subject: [PATCH 3/5] Simplify thumbnails getter --- ayon_api/_api_helpers/thumbnails.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 6aafc3416..2fc045459 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -530,25 +530,18 @@ def set_project_thumbnail_from_stream( def _get_entity_thumbnail_endpoint( self, project_name: str, entity_type: str, entity_id: str ) -> str: - if entity_type in ( + if entity_type not in ( "folder", "task", "version", "workfile", - ): - entity_type += "s" - - if entity_type not in ( - "folders", - "tasks", - "versions", - "workfiles", ): raise ValueError( f"Entity type '{entity_type}' does not support thumbnails." ) + return ( - f"api/projects/{project_name}/{entity_type}/{entity_id}/thumbnail" + f"api/projects/{project_name}/{entity_type}s/{entity_id}/thumbnail" ) def _prepare_thumbnail_content( From 4fc06a09253d02701221deae85acca265742cd85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:48:09 +0200 Subject: [PATCH 4/5] Add tests for new server endpoint methods Tests for users, access groups, teams, addon settings, projects, anatomy presets, folders search, thumbnails, services, sites, system, events deletion, reviewables, task assignment and entity lists run against a real AYON server, like the other server tests. Server version validation is tested without a server. Fixes found while writing the tests: - 'delete_events' refuses a filter without conditions, which would delete all events. - Settings, overrides, anatomy, roots, access group and server config payloads are sent as JSON body instead of keyword arguments, so any key name works. - 'set_project_thumbnail' and 'set_project_thumbnail_from_stream' return thumbnail id like entity thumbnail setters. - Docstrings of 'set_project_root_overrides' and 'get_version_reviewables'. Co-Authored-By: Claude Opus 5.5 --- ayon_api/_api.py | 25 +- ayon_api/_api_helpers/bundles_addons.py | 20 +- ayon_api/_api_helpers/events.py | 11 +- ayon_api/_api_helpers/projects.py | 16 +- ayon_api/_api_helpers/reviewables.py | 2 +- ayon_api/_api_helpers/system.py | 2 +- ayon_api/_api_helpers/thumbnails.py | 12 +- ayon_api/_api_helpers/users.py | 4 +- tests/test_addon_settings_api.py | 1062 +++++++++++++++++++++++ tests/test_projects_api.py | 726 ++++++++++++++++ tests/test_review_tasks_lists_api.py | 575 ++++++++++++ tests/test_server_version_validation.py | 348 ++++++++ tests/test_system_api.py | 531 ++++++++++++ tests/test_users_api.py | 670 ++++++++++++++ 14 files changed, 3970 insertions(+), 34 deletions(-) create mode 100644 tests/test_addon_settings_api.py create mode 100644 tests/test_projects_api.py create mode 100644 tests/test_review_tasks_lists_api.py create mode 100644 tests/test_server_version_validation.py create mode 100644 tests/test_system_api.py create mode 100644 tests/test_users_api.py diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 80b6c3524..7c0bf357e 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4377,8 +4377,7 @@ def delete_events( ) -> None: """Delete events matching filter. - Requires administrator privileges. All events are deleted if - filter does not contain any conditions. + Requires administrator privileges. Example filter:: @@ -4398,6 +4397,10 @@ def delete_events( to delete. For more technical specifications look to server backed 'ayon_server.sqlfilter.QueryFilter'. + Raises: + ValueError: Filter does not contain any conditions. Server would + delete all events. + """ con = get_server_api_connection() return con.delete_events( @@ -5564,9 +5567,9 @@ def set_project_root_overrides( ) -> None: """Set root overrides of logged user for a site. - Only roots defined in project anatomy are stored. Roots with empty - value are removed, and all overrides for the site are removed - if no root value is passed. + Roots defined in project anatomy with empty value are removed, + and all overrides for the site are removed if no root value + is left. Args: project_name (str): Project name. @@ -10048,13 +10051,16 @@ def get_project_thumbnail( def set_project_thumbnail( project_name: str, src_filepath: str, -) -> None: +) -> str: """Upload project thumbnail. Args: project_name (str): Project name. src_filepath (str): Filepath to thumbnail which should be uploaded. + Returns: + str: Thumbnail id of project thumbnail. + Raises: ValueError: When thumbnail source cannot be processed. UnsupportedServerVersion: Server version is lower than 1.15.0. @@ -10070,13 +10076,16 @@ def set_project_thumbnail( def set_project_thumbnail_from_stream( project_name: str, stream: StreamType, -) -> None: +) -> str: """Upload project thumbnail from byte stream. Args: project_name (str): Project name. stream (StreamType): Thumbnail content stream. + Returns: + str: Thumbnail id of project thumbnail. + Raises: UnsupportedServerVersion: Server version is lower than 1.15.0. @@ -11074,7 +11083,7 @@ def get_version_reviewables( Notes: Server responds with an error if version does not have - any reviewables. + any reviewables and 'latest' or 'latest_done' is used. Args: project_name (str): Project name. diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index a38449f88..09804daf9 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -936,9 +936,9 @@ def set_addon_studio_settings( variant = self.get_default_settings_variant() query = prepare_query_string({"variant": variant or None}) - response = self.post( + response = self.raw_post( f"addons/{addon_name}/{addon_version}/settings{query}", - **settings + json=settings, ) response.raise_for_status() @@ -975,10 +975,10 @@ def set_addon_project_settings( "site_id": site_id or None, "variant": variant or None, }) - response = self.post( + response = self.raw_post( f"addons/{addon_name}/{addon_version}" f"/settings/{project_name}{query}", - **settings + json=settings, ) response.raise_for_status() @@ -1247,9 +1247,9 @@ def set_addon_studio_raw_overrides( variant = self.get_default_settings_variant() query = prepare_query_string({"variant": variant or None}) - response = self.put( + response = self.raw_put( f"addons/{addon_name}/{addon_version}/rawOverrides{query}", - **overrides + json=overrides, ) response.raise_for_status() @@ -1327,10 +1327,10 @@ def set_addon_project_raw_overrides( "site_id": site_id or None, "variant": variant or None, }) - response = self.put( + response = self.raw_put( f"addons/{addon_name}/{addon_version}" f"/rawOverrides/{project_name}{query}", - **overrides + json=overrides, ) response.raise_for_status() @@ -1361,9 +1361,9 @@ def set_addon_site_settings( raise ValueError("Site id is not available.") query = prepare_query_string({"site_id": site_id}) - response = self.put( + response = self.raw_put( f"addons/{addon_name}/{addon_version}/siteSettings{query}", - **settings + json=settings, ) response.raise_for_status() diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index e34dc8a8b..f1535afd4 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -345,8 +345,7 @@ def delete_event(self, event_id: str) -> None: def delete_events(self, events_filter: EventFilter) -> None: """Delete events matching filter. - Requires administrator privileges. All events are deleted if - filter does not contain any conditions. + Requires administrator privileges. Example filter:: @@ -366,7 +365,15 @@ def delete_events(self, events_filter: EventFilter) -> None: to delete. For more technical specifications look to server backed 'ayon_server.sqlfilter.QueryFilter'. + Raises: + ValueError: Filter does not contain any conditions. Server would + delete all events. + """ + if not events_filter or not events_filter.get("conditions"): + raise ValueError( + "Filter without conditions would delete all events." + ) response = self.post( "eventops", type="delete", diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index c68adf603..faf364e0f 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -167,7 +167,7 @@ def save_project_anatomy_preset( preset (AnatomyPresetDict): Anatomy preset values. """ - response = self.put(f"anatomy/presets/{preset_name}", **preset) + response = self.raw_put(f"anatomy/presets/{preset_name}", json=preset) response.raise_for_status() def rename_project_anatomy_preset( @@ -674,9 +674,9 @@ def set_project_anatomy( preset format. """ - response = self.post( + response = self.raw_post( f"projects/{project_name}/anatomy", - **anatomy + json=anatomy, ) response.raise_for_status() @@ -1066,9 +1066,9 @@ def set_project_root_overrides( ) -> None: """Set root overrides of logged user for a site. - Only roots defined in project anatomy are stored. Roots with empty - value are removed, and all overrides for the site are removed - if no root value is passed. + Roots defined in project anatomy with empty value are removed, + and all overrides for the site are removed if no root value + is left. Args: project_name (str): Project name. @@ -1076,9 +1076,9 @@ def set_project_root_overrides( roots (dict[str, Optional[str]]): Root values by root name. """ - response = self.put( + response = self.raw_put( f"projects/{project_name}/roots/{site_id}", - **roots + json=roots, ) response.raise_for_status() diff --git a/ayon_api/_api_helpers/reviewables.py b/ayon_api/_api_helpers/reviewables.py index 2e8199ea2..d36b6b552 100644 --- a/ayon_api/_api_helpers/reviewables.py +++ b/ayon_api/_api_helpers/reviewables.py @@ -69,7 +69,7 @@ def get_version_reviewables( Notes: Server responds with an error if version does not have - any reviewables. + any reviewables and 'latest' or 'latest_done' is used. Args: project_name (str): Project name. diff --git a/ayon_api/_api_helpers/system.py b/ayon_api/_api_helpers/system.py index 6b809834d..8ff21b0f2 100644 --- a/ayon_api/_api_helpers/system.py +++ b/ayon_api/_api_helpers/system.py @@ -132,7 +132,7 @@ def update_server_config(self, config: dict[str, Any]) -> None: "Function 'update_server_config' is supported" " for AYON server 1.15.9 and above." ) - response = self.patch("config", **config) + response = self.raw_patch("config", json=config) response.raise_for_status() def get_enums(self) -> list[dict[str, Any]]: diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 2fc045459..aa8a40854 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -471,13 +471,16 @@ def get_project_thumbnail(self, project_name: str) -> ThumbnailContent: def set_project_thumbnail( self, project_name: str, src_filepath: str - ) -> None: + ) -> str: """Upload project thumbnail. Args: project_name (str): Project name. src_filepath (str): Filepath to thumbnail which should be uploaded. + Returns: + str: Thumbnail id of project thumbnail. + Raises: ValueError: When thumbnail source cannot be processed. UnsupportedServerVersion: Server version is lower than 1.15.0. @@ -499,16 +502,20 @@ def set_project_thumbnail( headers={"Content-Type": mime_type}, ) response.raise_for_status() + return response.json()["id"] def set_project_thumbnail_from_stream( self, project_name: str, stream: StreamType - ) -> None: + ) -> str: """Upload project thumbnail from byte stream. Args: project_name (str): Project name. stream (StreamType): Thumbnail content stream. + Returns: + str: Thumbnail id of project thumbnail. + Raises: UnsupportedServerVersion: Server version is lower than 1.15.0. @@ -526,6 +533,7 @@ def set_project_thumbnail_from_stream( headers={"Content-Type": mime_type}, ) response.raise_for_status() + return response.json()["id"] def _get_entity_thumbnail_endpoint( self, project_name: str, entity_type: str, entity_id: str diff --git a/ayon_api/_api_helpers/users.py b/ayon_api/_api_helpers/users.py index 268e05ca4..411c6f2a5 100644 --- a/ayon_api/_api_helpers/users.py +++ b/ayon_api/_api_helpers/users.py @@ -373,9 +373,9 @@ def save_access_group( group is saved if not passed. """ - response = self.put( + response = self.raw_put( f"accessGroups/{access_group_name}/{project_name or '_'}", - **permissions, + json=permissions, ) response.raise_for_status() diff --git a/tests/test_addon_settings_api.py b/tests/test_addon_settings_api.py new file mode 100644 index 000000000..ad40d84c0 --- /dev/null +++ b/tests/test_addon_settings_api.py @@ -0,0 +1,1062 @@ +"""Tests of addon settings API (overrides, raw overrides, migration). + +To run use: pytest --envfile {environment path}. +Make sure you have set AYON_SERVER_URL and AYON_API_KEY in your environment. +The tests expect an admin user. + +Tests use an addon from the production bundle that has a settings model. +Studio level writes are done only on a unique, throwaway settings variant +(not 'production' or 'staging') and the raw studio overrides of that +variant are restored in fixture teardown. Project level writes are done +on the test project created by 'project_entity_fixture'. + +""" + +import copy +import uuid + +import pytest + +from ayon_api import ( + delete_addon_project_overrides, + delete_addon_studio_overrides, + delete_site, + get_addon_project_overrides, + get_addon_project_raw_overrides, + get_addon_project_settings, + get_addon_settings_schema, + get_addon_site_settings, + get_addon_site_settings_schema, + get_addon_studio_overrides, + get_addon_studio_raw_overrides, + get_addon_studio_settings, + get_addons_info, + get_bundles, + get_client_version, + get_info, + get_server_version_tuple, + get_site_id, + get_sites, + migrate_addon_settings, + migrate_settings_by_bundle, + modify_addon_project_overrides, + modify_addon_studio_overrides, + set_addon_project_raw_overrides, + set_addon_project_settings, + set_addon_site_settings, + set_addon_studio_raw_overrides, + set_addon_studio_settings, + set_client_version, + set_site_id, + exceptions, +) + + +PREFERRED_ADDONS = ("core",) + + +# --- Helpers --- +def _unique_name(prefix="ayon-api-test"): + return f"{prefix}-{uuid.uuid4().hex[:12]}" + + +def _get_value(data, path): + for key in path: + if not isinstance(data, dict) or key not in data: + raise KeyError(key) + data = data[key] + return data + + +def _has_path(data, path): + try: + _get_value(data, path) + except KeyError: + return False + return True + + +def _set_value(data, path, value): + for key in path[:-1]: + data = data.setdefault(key, {}) + data[path[-1]] = value + + +def _build_overrides(path, value): + overrides = {} + _set_value(overrides, path, value) + return overrides + + +def _override_key(path): + return "root_" + "_".join(path) + + +def _resolve_ref(schema, prop): + """Resolve '$ref' (directly or via 'allOf') of a schema property.""" + ref = prop.get("$ref") + if ref is None: + all_of = prop.get("allOf") + if all_of and len(all_of) == 1: + ref = all_of[0].get("$ref") + if not ref or not ref.startswith("#/definitions/"): + return None + return schema.get("definitions", {}).get(ref.split("/")[-1]) + + +def _find_bool_path(schema, required_scopes=("studio", "project")): + """Find path to a boolean settings field that is not inside a group. + + Group fields are stored as a whole in overrides, which would make + assertions on override paths ambiguous. + + """ + def _crawl(model_schema, path, depth): + if depth > 5 or model_schema.get("isgroup"): + return None + properties = model_schema.get("properties") or {} + sub_models = [] + for name, prop in properties.items(): + scope = prop.get("scope") or ["studio", "project"] + if any(s not in scope for s in required_scopes): + continue + if prop.get("disabled") or prop.get("conditionalEnum"): + continue + if prop.get("type") == "boolean": + return [*path, name] + sub_schema = _resolve_ref(schema, prop) + if sub_schema is not None: + sub_models.append((name, sub_schema)) + + for name, sub_schema in sub_models: + result = _crawl(sub_schema, [*path, name], depth + 1) + if result: + return result + return None + + return _crawl(schema, [], 0) + + +def _get_production_bundle(): + bundles = get_bundles() + production_name = bundles.get("productionBundle") + if not production_name: + return None + for bundle in bundles["bundles"]: + if bundle["name"] == production_name: + return bundle + return None + + +def _iter_bundle_addons(bundle): + addons = { + name: version + for name, version in bundle.get("addons", {}).items() + if version + } + ordered = [name for name in PREFERRED_ADDONS if name in addons] + ordered.extend(sorted(name for name in addons if name not in ordered)) + for name in ordered: + yield name, addons[name] + + +# --- Fixtures --- +@pytest.fixture(scope="module") +def production_bundle(): + bundle = _get_production_bundle() + if bundle is None: + pytest.skip("Server does not have a production bundle.") + return bundle + + +@pytest.fixture(scope="module") +def settings_addon(production_bundle): + """Addon from production bundle with a usable settings model. + + Returns: + dict[str, Any]: 'name', 'version' of the addon and 'path' to + a boolean settings field usable on studio and project level. + + """ + for addon_name, addon_version in _iter_bundle_addons(production_bundle): + try: + schema = get_addon_settings_schema(addon_name, addon_version) + except exceptions.HTTPRequestError: + continue + if not schema or not schema.get("properties"): + continue + path = _find_bool_path(schema) + if not path: + continue + settings = get_addon_studio_settings( + addon_name, addon_version, variant="production" + ) + if not _has_path(settings, path): + continue + if not isinstance(_get_value(settings, path), bool): + continue + return { + "name": addon_name, + "version": addon_version, + "path": path, + } + pytest.skip( + "No addon in production bundle has settings with a boolean field." + ) + + +@pytest.fixture +def studio_variant(settings_addon): + """Unique settings variant used for studio level writes. + + Raw studio overrides of the variant are stored before the test and + restored after it (there should not be any). + + """ + variant = _unique_name() + name = settings_addon["name"] + version = settings_addon["version"] + original = get_addon_studio_raw_overrides( + name, version, variant=variant + ) + try: + yield variant + finally: + current = get_addon_studio_raw_overrides( + name, version, variant=variant + ) + if current != original: + if original: + set_addon_studio_raw_overrides( + name, version, original, variant=variant + ) + else: + delete_addon_studio_overrides(name, version, variant=variant) + + +@pytest.fixture +def project_addon(settings_addon, project_entity_fixture): + """Settings addon with the test project name. + + Project overrides of the addon on the test project are restored + after the test. + + """ + project_name = project_entity_fixture["name"] + name = settings_addon["name"] + version = settings_addon["version"] + original = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + try: + yield {**settings_addon, "project_name": project_name} + finally: + current = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + if current != original: + if original: + set_addon_project_raw_overrides( + name, + version, + project_name, + original, + variant="production", + ) + else: + delete_addon_project_overrides( + name, version, project_name, variant="production" + ) + + +@pytest.fixture +def project_site_id(project_addon): + """Unique site id used for project site overrides. + + Project site overrides are stored only in the test project (which is + deleted at the end of the session) but are removed after the test too. + + """ + site_id = _unique_name() + try: + yield site_id + finally: + delete_addon_project_overrides( + project_addon["name"], + project_addon["version"], + project_addon["project_name"], + site_id=site_id, + ) + + +@pytest.fixture +def site_settings_addon(production_bundle): + """Addon from production bundle that has site settings.""" + for addon_name, addon_version in _iter_bundle_addons(production_bundle): + try: + schema = get_addon_site_settings_schema(addon_name, addon_version) + except exceptions.HTTPRequestError: + continue + if schema and schema.get("properties"): + return { + "name": addon_name, + "version": addon_version, + "schema": schema, + } + pytest.skip("No addon in production bundle has site settings.") + + +@pytest.fixture +def registered_site(): + """Register a unique site id on server using the connection. + + Site id (and client version, which is required for site registration) + is set on the connection and 'get_info' registers the site. Site is + unregistered and connection values are restored after the test. + Unregistering the site removes its site settings on server. + + """ + site_id = _unique_name() + orig_site_id = get_site_id() + orig_client_version = get_client_version() + registered = False + try: + set_site_id(site_id) + if not orig_client_version: + set_client_version("0.0.0") + get_info() + registered = any(site["id"] == site_id for site in get_sites()) + if not registered: + pytest.skip("Unable to register a test site on server.") + yield site_id + finally: + set_site_id(orig_site_id) + set_client_version(orig_client_version) + if registered: + delete_site(site_id) + + +@pytest.fixture +def min_server_1_16_8(): + if get_server_version_tuple() < (1, 16, 8): + pytest.skip("Server version is lower than 1.16.8.") + + +# --- Schema --- +def test_get_addon_settings_schema_variant(settings_addon, studio_variant): + """Studio schema accepts 'variant' argument. + + Verifies: + - Schema for production and custom variant is returned. + - Both schemas have the same top level properties. + + """ + name = settings_addon["name"] + version = settings_addon["version"] + default_schema = get_addon_settings_schema(name, version) + production_schema = get_addon_settings_schema( + name, version, variant="production" + ) + variant_schema = get_addon_settings_schema( + name, version, variant=studio_variant + ) + for schema in (default_schema, production_schema, variant_schema): + assert isinstance(schema, dict) + assert "properties" in schema + assert "title" in schema + + assert ( + set(production_schema["properties"]) + == set(variant_schema["properties"]) + == set(default_schema["properties"]) + ) + + +def test_get_addon_settings_schema_project_variant_site( + project_addon, project_site_id +): + """Project schema accepts 'variant' and 'site_id' arguments.""" + name = project_addon["name"] + version = project_addon["version"] + project_name = project_addon["project_name"] + + studio_schema = get_addon_settings_schema(name, version) + project_schema = get_addon_settings_schema( + name, version, project_name, variant="production" + ) + site_schema = get_addon_settings_schema( + name, + version, + project_name, + variant="production", + site_id=project_site_id, + ) + for schema in (project_schema, site_schema): + assert isinstance(schema, dict) + assert set(schema["properties"]) == set(studio_schema["properties"]) + + # Site id is ignored without project name + studio_site_schema = get_addon_settings_schema( + name, version, site_id=project_site_id + ) + assert set(studio_site_schema["properties"]) == set( + studio_schema["properties"] + ) + + +# --- Studio settings --- +def test_set_addon_studio_settings(settings_addon, studio_variant): + """Set full studio settings and check calculated overrides. + + Verifies: + - Changed value is returned by studio settings getter. + - Only changed value is stored in raw overrides. + - Overrides listing contains the changed field on studio level. + + """ + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + settings = get_addon_studio_settings(name, version, variant=studio_variant) + new_value = not _get_value(settings, path) + new_settings = copy.deepcopy(settings) + _set_value(new_settings, path, new_value) + + set_addon_studio_settings( + name, version, new_settings, variant=studio_variant + ) + + updated = get_addon_studio_settings(name, version, variant=studio_variant) + assert _get_value(updated, path) is new_value + + raw = get_addon_studio_raw_overrides(name, version, variant=studio_variant) + assert _get_value(raw, path) is new_value + + overrides = get_addon_studio_overrides( + name, version, variant=studio_variant + ) + item = overrides[_override_key(path)] + assert item["path"] == path + assert item["value"] is new_value + assert item["level"] == "studio" + + # Production settings were not affected + production = get_addon_studio_settings( + name, version, variant="production" + ) + assert _get_value(production, path) is _get_value(settings, path) + + +def test_set_addon_studio_settings_explicit_pin( + settings_addon, studio_variant +): + """Unchanged value is stored when pinned with '__pinned_fields__'.""" + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + settings = get_addon_studio_settings(name, version, variant=studio_variant) + value = _get_value(settings, path) + payload = copy.deepcopy(settings) + payload["__pinned_fields__"] = [path] + set_addon_studio_settings(name, version, payload, variant=studio_variant) + + raw = get_addon_studio_raw_overrides(name, version, variant=studio_variant) + assert _get_value(raw, path) is value + + +def test_get_addon_studio_overrides_as_version( + settings_addon, studio_variant +): + """Overrides with 'as_version' of the same version are equal.""" + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + settings = get_addon_studio_settings(name, version, variant=studio_variant) + new_value = not _get_value(settings, path) + set_addon_studio_raw_overrides( + name, + version, + _build_overrides(path, new_value), + variant=studio_variant, + ) + + overrides = get_addon_studio_overrides( + name, version, variant=studio_variant + ) + as_version_overrides = get_addon_studio_overrides( + name, version, variant=studio_variant, as_version=version + ) + assert overrides == as_version_overrides + assert overrides[_override_key(path)]["value"] is new_value + + +def test_modify_addon_studio_overrides(settings_addon, studio_variant): + """Remove and pin studio override on a settings path. + + Verifies: + - 'delete' action removes the override and default value is used. + - 'pin' action stores current value as override. + + """ + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + default_value = _get_value( + get_addon_studio_settings(name, version, variant=studio_variant), + path, + ) + set_addon_studio_raw_overrides( + name, + version, + _build_overrides(path, not default_value), + variant=studio_variant, + ) + + modify_addon_studio_overrides( + name, version, "delete", path, variant=studio_variant + ) + raw = get_addon_studio_raw_overrides(name, version, variant=studio_variant) + assert not _has_path(raw, path) + settings = get_addon_studio_settings(name, version, variant=studio_variant) + assert _get_value(settings, path) is default_value + overrides = get_addon_studio_overrides( + name, version, variant=studio_variant + ) + assert _override_key(path) not in overrides + + modify_addon_studio_overrides( + name, version, "pin", path, variant=studio_variant + ) + raw = get_addon_studio_raw_overrides(name, version, variant=studio_variant) + assert _get_value(raw, path) is default_value + overrides = get_addon_studio_overrides( + name, version, variant=studio_variant + ) + assert overrides[_override_key(path)]["value"] is default_value + + +def test_delete_addon_studio_overrides(settings_addon, studio_variant): + """Delete all studio overrides of the variant.""" + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + default_value = _get_value( + get_addon_studio_settings(name, version, variant=studio_variant), + path, + ) + set_addon_studio_raw_overrides( + name, + version, + _build_overrides(path, not default_value), + variant=studio_variant, + ) + assert get_addon_studio_raw_overrides( + name, version, variant=studio_variant + ) + + delete_addon_studio_overrides(name, version, variant=studio_variant) + + assert get_addon_studio_raw_overrides( + name, version, variant=studio_variant + ) == {} + assert get_addon_studio_overrides( + name, version, variant=studio_variant + ) == {} + settings = get_addon_studio_settings(name, version, variant=studio_variant) + assert _get_value(settings, path) is default_value + + +def test_studio_raw_overrides_roundtrip(settings_addon, studio_variant): + """Raw studio overrides are stored and returned as they are.""" + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + assert get_addon_studio_raw_overrides( + name, version, variant=studio_variant + ) == {} + + default_value = _get_value( + get_addon_studio_settings(name, version, variant=studio_variant), + path, + ) + overrides = _build_overrides(path, not default_value) + set_addon_studio_raw_overrides( + name, version, overrides, variant=studio_variant + ) + + assert get_addon_studio_raw_overrides( + name, version, variant=studio_variant + ) == overrides + settings = get_addon_studio_settings(name, version, variant=studio_variant) + assert _get_value(settings, path) is (not default_value) + + +# --- Project settings --- +def test_set_addon_project_settings(project_addon): + """Set full project settings and check calculated overrides. + + Verifies: + - Changed value is returned by project settings getter. + - Changed value is stored in raw project overrides. + - Overrides listing contains the field on project level. + + """ + name = project_addon["name"] + version = project_addon["version"] + path = project_addon["path"] + project_name = project_addon["project_name"] + + settings = get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ) + new_value = not _get_value(settings, path) + new_settings = copy.deepcopy(settings) + _set_value(new_settings, path, new_value) + + set_addon_project_settings( + name, version, project_name, new_settings, variant="production" + ) + + updated = get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ) + assert _get_value(updated, path) is new_value + + raw = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + assert _get_value(raw, path) is new_value + + overrides = get_addon_project_overrides( + name, version, project_name, variant="production" + ) + item = overrides[_override_key(path)] + assert item["path"] == path + assert item["value"] is new_value + assert item["level"] == "project" + + +def test_modify_addon_project_overrides(project_addon): + """Remove and pin project override on a settings path.""" + name = project_addon["name"] + version = project_addon["version"] + path = project_addon["path"] + project_name = project_addon["project_name"] + + delete_addon_project_overrides( + name, version, project_name, variant="production" + ) + studio_value = _get_value( + get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ), + path, + ) + set_addon_project_raw_overrides( + name, + version, + project_name, + _build_overrides(path, not studio_value), + variant="production", + ) + + modify_addon_project_overrides( + name, version, project_name, "delete", path, variant="production" + ) + raw = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + assert not _has_path(raw, path) + settings = get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ) + assert _get_value(settings, path) is studio_value + + modify_addon_project_overrides( + name, version, project_name, "pin", path, variant="production" + ) + raw = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + assert _get_value(raw, path) is studio_value + overrides = get_addon_project_overrides( + name, version, project_name, variant="production" + ) + item = overrides[_override_key(path)] + assert item["level"] == "project" + assert item["value"] is studio_value + + +def test_project_raw_overrides_and_delete(project_addon): + """Raw project overrides roundtrip and deletion of all overrides.""" + name = project_addon["name"] + version = project_addon["version"] + path = project_addon["path"] + project_name = project_addon["project_name"] + + delete_addon_project_overrides( + name, version, project_name, variant="production" + ) + assert get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) == {} + + studio_value = _get_value( + get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ), + path, + ) + overrides = _build_overrides(path, not studio_value) + set_addon_project_raw_overrides( + name, version, project_name, overrides, variant="production" + ) + assert get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) == overrides + settings = get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ) + assert _get_value(settings, path) is (not studio_value) + + delete_addon_project_overrides( + name, version, project_name, variant="production" + ) + assert get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) == {} + settings = get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ) + assert _get_value(settings, path) is studio_value + + +def test_project_site_overrides(project_addon, project_site_id): + """Project site overrides of current user with 'site_id' argument. + + Verifies: + - Raw site overrides are separated from project overrides. + - Overrides listing with site id contains the site level override. + - Pin/delete actions and deletion of all site overrides. + + """ + name = project_addon["name"] + version = project_addon["version"] + path = project_addon["path"] + project_name = project_addon["project_name"] + site_id = project_site_id + + assert get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) == {} + project_raw = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + + project_value = _get_value( + get_addon_project_settings( + name, version, project_name, variant="production", use_site=False + ), + path, + ) + overrides = _build_overrides(path, not project_value) + set_addon_project_raw_overrides( + name, version, project_name, overrides, site_id=site_id + ) + assert get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) == overrides + # Project overrides are not affected + assert get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) == project_raw + + site_settings = get_addon_project_settings( + name, version, project_name, variant="production", site_id=site_id + ) + assert _get_value(site_settings, path) is (not project_value) + + listing = get_addon_project_overrides( + name, version, project_name, variant="production", site_id=site_id + ) + item = listing[_override_key(path)] + assert item["level"] == "site" + assert item["value"] is (not project_value) + + # Site overrides are not listed without site id + listing = get_addon_project_overrides( + name, version, project_name, variant="production" + ) + item = listing.get(_override_key(path)) + assert item is None or item["level"] != "site" + + modify_addon_project_overrides( + name, version, project_name, "delete", path, site_id=site_id + ) + raw = get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) + assert not _has_path(raw, path) + + modify_addon_project_overrides( + name, version, project_name, "pin", path, site_id=site_id + ) + raw = get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) + assert _get_value(raw, path) is project_value + + delete_addon_project_overrides( + name, version, project_name, site_id=site_id + ) + assert get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) == {} + + +def test_set_addon_project_settings_site(project_addon, project_site_id): + """Set full project settings as site overrides with 'site_id'.""" + name = project_addon["name"] + version = project_addon["version"] + path = project_addon["path"] + project_name = project_addon["project_name"] + site_id = project_site_id + + project_raw = get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) + settings = get_addon_project_settings( + name, version, project_name, variant="production", site_id=site_id + ) + new_value = not _get_value(settings, path) + new_settings = copy.deepcopy(settings) + _set_value(new_settings, path, new_value) + + set_addon_project_settings( + name, + version, + project_name, + new_settings, + variant="production", + site_id=site_id, + ) + + raw = get_addon_project_raw_overrides( + name, version, project_name, site_id=site_id + ) + assert _get_value(raw, path) is new_value + assert get_addon_project_raw_overrides( + name, version, project_name, variant="production" + ) == project_raw + + updated = get_addon_project_settings( + name, version, project_name, variant="production", site_id=site_id + ) + assert _get_value(updated, path) is new_value + + +# --- Site settings --- +def test_set_addon_site_settings(site_settings_addon, registered_site): + """Set site settings of current user for a registered test site. + + Verifies: + - Site settings of unused site are defaults. + - Changed value is returned after set. + - Site settings of different site are not affected. + + """ + name = site_settings_addon["name"] + version = site_settings_addon["version"] + schema = site_settings_addon["schema"] + site_id = registered_site + + defaults = get_addon_site_settings(name, version, site_id=site_id) + assert isinstance(defaults, dict) + # Site id from connection is used by default + assert get_addon_site_settings(name, version) == defaults + + new_settings = copy.deepcopy(defaults) + path = _find_bool_path(schema, required_scopes=()) + if path and isinstance(_get_value(defaults, path), bool): + _set_value(new_settings, path, not _get_value(defaults, path)) + else: + path = None + + set_addon_site_settings(name, version, new_settings, site_id=site_id) + assert get_addon_site_settings( + name, version, site_id=site_id + ) == new_settings + if path is not None: + assert new_settings != defaults + other_site = _unique_name() + assert get_addon_site_settings( + name, version, site_id=other_site + ) != new_settings + + # Site id from connection is used if not passed + set_addon_site_settings(name, version, defaults) + assert get_addon_site_settings( + name, version, site_id=site_id + ) == defaults + + +def test_set_addon_site_settings_without_site_id(): + """ValueError is raised if site id is not available.""" + orig_site_id = get_site_id() + try: + set_site_id(None) + with pytest.raises(ValueError): + set_addon_site_settings("addon", "1.0.0", {}) + finally: + set_site_id(orig_site_id) + + +# --- Migration --- +def test_migrate_addon_settings_same_version( + settings_addon, min_server_1_16_8 +): + """Server refuses migration to the same addon version.""" + with pytest.raises(exceptions.HTTPRequestError): + migrate_addon_settings( + settings_addon["name"], + settings_addon["version"], + settings_addon["version"], + with_projects=False, + ) + + +def test_migrate_addon_settings( + settings_addon, studio_variant, min_server_1_16_8 +): + """Migrate studio settings from other installed addon version. + + Both source and target variant is the unique test variant and project + settings are not migrated, so production settings are not affected. + Source version does not have overrides for the variant, so target + overrides are replaced (removed) by the migration. + + """ + name = settings_addon["name"] + version = settings_addon["version"] + path = settings_addon["path"] + + other_version = None + for addon in get_addons_info(details=False)["addons"]: + if addon["name"] != name: + continue + for addon_version in addon["versions"]: + if addon_version != version: + other_version = addon_version + break + if other_version is None: + pytest.skip(f"Addon '{name}' does not have other version installed.") + + assert get_addon_studio_raw_overrides( + name, other_version, variant=studio_variant + ) == {} + + default_value = _get_value( + get_addon_studio_settings(name, version, variant=studio_variant), + path, + ) + set_addon_studio_raw_overrides( + name, + version, + _build_overrides(path, not default_value), + variant=studio_variant, + ) + + result = migrate_addon_settings( + name, + other_version, + version, + source_variant=studio_variant, + target_variant=studio_variant, + with_projects=False, + ) + assert isinstance(result, dict) + events = result["events"] + assert isinstance(events, list) + assert len(events) == 1 + summary = events[0]["summary"] + assert summary["variant"] == studio_variant + assert summary["source_version"] == other_version + assert summary["addon_version"] == version + + # Converted (empty) source overrides replaced the target overrides + assert get_addon_studio_raw_overrides( + name, version, variant=studio_variant + ) != _build_overrides(path, not default_value) + # Source overrides stay untouched + assert get_addon_studio_raw_overrides( + name, other_version, variant=studio_variant + ) == {} + + +def test_migrate_settings_by_bundle_same_bundle( + settings_addon, production_bundle +): + """Migrate production bundle settings to itself without projects. + + Source and target are the same bundle and variant, so the migration + does not change any data (overrides are compared with themselves). + + """ + name = settings_addon["name"] + version = settings_addon["version"] + original = get_addon_studio_raw_overrides( + name, version, variant="production" + ) + try: + migrate_settings_by_bundle( + production_bundle["name"], + production_bundle["name"], + "production", + "production", + with_projects=False, + ) + assert get_addon_studio_raw_overrides( + name, version, variant="production" + ) == original + finally: + current = get_addon_studio_raw_overrides( + name, version, variant="production" + ) + if current != original and original: + set_addon_studio_raw_overrides( + name, version, original, variant="production" + ) + + +def test_migrate_settings_by_bundle_invalid(production_bundle): + """Invalid migration requests are refused before any change. + + Verifies: + - Custom variant must match the bundle name. + - Unknown bundle is refused. + + """ + bundle_name = production_bundle["name"] + with pytest.raises(exceptions.HTTPRequestError): + migrate_settings_by_bundle( + bundle_name, + bundle_name, + "production", + _unique_name(), + with_projects=False, + ) + + with pytest.raises(exceptions.HTTPRequestError): + migrate_settings_by_bundle( + _unique_name(), + bundle_name, + "production", + "production", + with_projects=False, + ) diff --git a/tests/test_projects_api.py b/tests/test_projects_api.py new file mode 100644 index 000000000..0d621a85b --- /dev/null +++ b/tests/test_projects_api.py @@ -0,0 +1,726 @@ +"""Tests of project related server API methods. + +Covers anatomy presets, project anatomy, project bundle, root overrides, +entity URIs, project files, folder search, entity grouping and entity +and project thumbnails. + +To run use: pytest --envfile {environment path}. +Make sure you have set AYON_SERVER_URL and AYON_API_KEY in your +environment. Tests expect to run as an admin user. + +""" +import os +import uuid + +import pytest + +from ayon_api import ( + create_folder, + create_project, + create_task, + delete_folder, + delete_project, + delete_project_anatomy_preset, + delete_project_file, + delete_task, + get_anatomy_schema, + get_built_in_anatomy_preset, + get_bundles, + get_entity_groups, + get_folder_by_id, + get_folder_thumbnail, + get_project, + get_project_anatomy, + get_project_anatomy_preset, + get_project_anatomy_presets, + get_project_bundle, + get_project_entity_uris, + get_project_file_info, + get_project_root_overrides, + get_project_stats, + get_project_tags, + get_project_thumbnail, + get_server_version_tuple, + get_task_by_id, + get_task_thumbnail, + rename_project, + rename_project_anatomy_preset, + resolve_uris, + save_project_anatomy_preset, + search_folders, + set_entity_thumbnail, + set_entity_thumbnail_from_stream, + set_primary_anatomy_preset, + set_project_anatomy, + set_project_bundle, + set_project_root_overrides, + set_project_thumbnail, + set_project_thumbnail_from_stream, + unset_primary_anatomy_preset, + unset_project_bundle, + upload_project_file, +) +from ayon_api.exceptions import HTTPRequestError + +from .conftest import project_entity_fixture, project_name_fixture + + +RESOURCES_DIR = os.path.join(os.path.dirname(__file__), "resources") +IMAGE_PATH = os.path.join(RESOURCES_DIR, "ayon-symbol.png") +PROJECT_THUMBNAIL_ID = "0" * 32 + + +def _unique_suffix() -> str: + return uuid.uuid4().hex[:12] + + +def _normalize_id(entity_id): + if entity_id is None: + return None + return entity_id.replace("-", "") + + +def _require_server_version(version): + if get_server_version_tuple() < version: + version_str = ".".join(str(part) for part in version) + pytest.skip(f"Requires AYON server {version_str} or newer.") + + +def _get_primary_preset_name(): + for preset in get_project_anatomy_presets(): + if preset.get("primary"): + return preset["name"] + return None + + +def _get_preset_names(): + return {preset["name"] for preset in get_project_anatomy_presets()} + + +# --- Fixtures --- +@pytest.fixture +def temp_project(): + """Temporary project created and deleted by the test. + + Yields a dictionary with 'name' and 'code' keys. A test that renames + the project must update the 'name' key so the teardown removes + the correct project. + + """ + suffix = _unique_suffix() + holder = { + "name": f"AYONApiTmp_{suffix}", + "code": f"tmp{suffix[:8]}", + } + create_project(holder["name"], holder["code"]) + try: + yield holder + finally: + if get_project(holder["name"]): + delete_project(holder["name"]) + + +@pytest.fixture +def anatomy_preset_names(): + """Unique anatomy preset names, the presets are removed on teardown. + + Original primary preset is restored on teardown. + + """ + suffix = _unique_suffix() + names = [f"test_preset_{suffix}", f"test_preset_renamed_{suffix}"] + orig_primary = _get_primary_preset_name() + try: + yield names + finally: + for name in names: + delete_project_anatomy_preset(name) + + current_primary = _get_primary_preset_name() + if current_primary != orig_primary: + # Preset name '_' unsets any primary preset + set_primary_anatomy_preset(orig_primary or "_") + + +@pytest.fixture +def test_hierarchy(project_entity_fixture): + """Folder with a task in the shared test project. + + Folder has a unique tag. Both entities are removed on teardown. + + """ + project_name = project_entity_fixture["name"] + suffix = _unique_suffix() + folder_name = f"projects_api_folder_{suffix}" + task_name = f"projects_api_task_{suffix}" + tag = f"projects_api_tag_{suffix}" + task_type = project_entity_fixture["taskTypes"][0]["name"] + + folder_id = create_folder( + project_name, + folder_name, + folder_type="Folder", + tags=[tag], + ) + task_id = None + try: + task_id = create_task( + project_name, + task_name, + task_type, + folder_id, + ) + yield { + "project_name": project_name, + "folder_id": folder_id, + "folder_name": folder_name, + "task_id": task_id, + "task_name": task_name, + "task_type": task_type, + "tag": tag, + } + finally: + if task_id is not None: + delete_task(project_name, task_id) + delete_folder(project_name, folder_id, force=True) + + +# --- Anatomy --- +def test_get_anatomy_schema(): + """Anatomy schema is a JSON schema describing the anatomy model.""" + schema = get_anatomy_schema() + assert isinstance(schema, dict) + properties = schema.get("properties") + assert isinstance(properties, dict) + for key in ("roots", "templates", "attributes", "statuses"): + assert key in properties + + +def test_anatomy_preset_lifecycle(anatomy_preset_names): + """Save, rename, set/unset primary and delete an anatomy preset. + + Verifies: + - Saved preset is listed and its values are stored. + - Renamed preset is listed only under the new name. + - Preset can be marked as primary and the flag can be removed. + - Deleted preset is no longer listed. + + """ + preset_name, new_preset_name = anatomy_preset_names + + preset = get_built_in_anatomy_preset() + root_path = f"C:/ayon_api_test/{preset_name}" + preset["roots"][0]["windows"] = root_path + + save_project_anatomy_preset(preset_name, preset) + assert preset_name in _get_preset_names() + stored = get_project_anatomy_preset(preset_name) + assert stored["roots"][0]["windows"] == root_path + + # Update existing preset + updated_root_path = f"{root_path}_updated" + preset["roots"][0]["windows"] = updated_root_path + save_project_anatomy_preset(preset_name, preset) + stored = get_project_anatomy_preset(preset_name) + assert stored["roots"][0]["windows"] == updated_root_path + + rename_project_anatomy_preset(preset_name, new_preset_name) + preset_names = _get_preset_names() + assert preset_name not in preset_names + assert new_preset_name in preset_names + stored = get_project_anatomy_preset(new_preset_name) + assert stored["roots"][0]["windows"] == updated_root_path + + set_primary_anatomy_preset(new_preset_name) + assert _get_primary_preset_name() == new_preset_name + primary = get_project_anatomy_preset() + assert primary["roots"][0]["windows"] == updated_root_path + + unset_primary_anatomy_preset(new_preset_name) + assert _get_primary_preset_name() is None + + delete_project_anatomy_preset(new_preset_name) + assert new_preset_name not in _get_preset_names() + + +def test_rename_missing_anatomy_preset_fails(anatomy_preset_names): + """Renaming a preset that does not exist raises an error.""" + preset_name, new_preset_name = anatomy_preset_names + with pytest.raises(HTTPRequestError): + rename_project_anatomy_preset(preset_name, new_preset_name) + + +def test_save_builtin_anatomy_preset_fails(): + """Built-in preset cannot be overridden.""" + preset = get_built_in_anatomy_preset() + with pytest.raises(HTTPRequestError): + save_project_anatomy_preset("__builtin__", preset) + + +# --- Project --- +def test_rename_project(temp_project): + """Rename a temporary project including its code.""" + _require_server_version((1, 15, 0)) + + old_name = temp_project["name"] + suffix = _unique_suffix() + new_name = f"AYONApiTmpRenamed_{suffix}" + new_code = f"ren{suffix[:8]}" + + rename_project(old_name, new_name, new_code) + temp_project["name"] = new_name + + assert get_project(old_name) is None + project = get_project(new_name) + assert project is not None + assert project["name"] == new_name + assert project["code"] == new_code + + # Rename without code change keeps the code + newer_name = f"AYONApiTmpRenamed2_{suffix}" + rename_project(new_name, newer_name) + temp_project["name"] = newer_name + + project = get_project(newer_name) + assert project is not None + assert project["code"] == new_code + + +def test_get_project_anatomy(project_entity_fixture): + """Project anatomy is returned in anatomy preset format.""" + project_name = project_entity_fixture["name"] + anatomy = get_project_anatomy(project_name) + + for key in ("roots", "templates", "attributes", "statuses"): + assert key in anatomy + + root_names = {root["name"] for root in anatomy["roots"]} + assert root_names == set(project_entity_fixture["config"]["roots"]) + + +def test_set_project_anatomy(temp_project): + """Changed project anatomy is stored on the project.""" + project_name = temp_project["name"] + anatomy = get_project_anatomy(project_name) + + new_fps = 37.0 + if anatomy["attributes"].get("fps") == new_fps: + new_fps = 41.0 + anatomy["attributes"]["fps"] = new_fps + + root_path = f"C:/ayon_api_test/{project_name}" + anatomy["roots"][0]["windows"] = root_path + root_name = anatomy["roots"][0]["name"] + + set_project_anatomy(project_name, anatomy) + + stored = get_project_anatomy(project_name) + assert stored["attributes"]["fps"] == new_fps + assert stored["roots"][0]["windows"] == root_path + + project = get_project(project_name) + assert project["attrib"]["fps"] == new_fps + assert project["config"]["roots"][root_name]["windows"] == root_path + + +def test_get_project_stats(test_hierarchy): + """Project statistics contain counts of entities.""" + project_name = test_hierarchy["project_name"] + stats = get_project_stats(project_name) + + counts = stats["counts"] + assert set(counts) == { + "folders", + "products", + "versions", + "representations", + "tasks", + } + for value in counts.values(): + assert isinstance(value, int) + assert counts["folders"] >= 1 + assert counts["tasks"] >= 1 + + +def test_get_project_tags(test_hierarchy): + """Tags used on project entities are returned by entity type.""" + project_name = test_hierarchy["project_name"] + tags = get_project_tags(project_name) + + assert set(tags) == { + "folders", + "tasks", + "products", + "versions", + "representations", + "workfiles", + } + assert test_hierarchy["tag"] in tags["folders"] + assert test_hierarchy["tag"] not in tags["tasks"] + + +def test_project_bundle(temp_project): + """Freeze and unfreeze project bundle of a temporary project. + + An empty addons mapping is used so no addon settings are changed. + + """ + project_name = temp_project["name"] + variant = "production" + bundle_name = f"__project__{project_name}__{variant}" + has_production_bundle = bool(get_bundles().get("productionBundle")) + + if has_production_bundle: + bundle_info = get_project_bundle(project_name, variant=variant) + for key in ( + "addons", + "addonMetadata", + "installerOptions", + "dependencyPackageOptions", + ): + assert key in bundle_info + else: + # Information can't be received without production bundle + with pytest.raises(HTTPRequestError): + get_project_bundle(project_name, variant=variant) + + try: + set_project_bundle(project_name, {}, variant=variant) + + project = get_project(project_name) + assert project["data"]["bundle"][variant] == bundle_name + + if has_production_bundle: + bundle_info = get_project_bundle(project_name, variant=variant) + # All addons are disabled by the frozen project bundle + for addon_version in bundle_info["addons"].values(): + assert addon_version == "__disable__" + + finally: + unset_project_bundle(project_name, variant=variant) + + project = get_project(project_name) + assert variant not in project["data"].get("bundle", {}) + + +def test_set_project_root_overrides(project_entity_fixture): + """Set and remove root overrides of a site for the logged user. + + Uses a unique site id that is not used by any real site. + + """ + project_name = project_entity_fixture["name"] + root_name = next(iter(project_entity_fixture["config"]["roots"])) + site_id = f"ayon-api-test-site-{_unique_suffix()}" + root_path = f"C:/ayon_api_test/{site_id}" + + try: + set_project_root_overrides( + project_name, site_id, {root_name: root_path} + ) + overrides = get_project_root_overrides(project_name) + assert overrides[site_id] == {root_name: root_path} + + # Empty value removes the root override, which removes all + # overrides of the site + set_project_root_overrides(project_name, site_id, {root_name: None}) + overrides = get_project_root_overrides(project_name) + assert site_id not in overrides + + finally: + set_project_root_overrides(project_name, site_id, {}) + + assert site_id not in get_project_root_overrides(project_name) + + +def test_get_project_entity_uris(test_hierarchy): + """URIs of folder and task are built from their path.""" + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + task_id = test_hierarchy["task_id"] + folder_uri = ( + f"ayon+entity://{project_name}/{test_hierarchy['folder_name']}" + ) + task_uri = f"{folder_uri}?task={test_hierarchy['task_name']}" + + folder_uris = get_project_entity_uris( + project_name, "folder", [folder_id] + ) + assert len(folder_uris) == 1 + assert _normalize_id(folder_uris[0]["id"]) == folder_id + assert folder_uris[0]["uri"] == folder_uri + + task_uris = get_project_entity_uris(project_name, "task", {task_id}) + assert len(task_uris) == 1 + assert _normalize_id(task_uris[0]["id"]) == task_id + assert task_uris[0]["uri"] == task_uri + + assert get_project_entity_uris(project_name, "folder", []) == [] + + +def test_resolve_uris(test_hierarchy): + """Resolve folder and task URIs back to entity ids.""" + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + task_id = test_hierarchy["task_id"] + folder_uri = f"ayon://{project_name}/{test_hierarchy['folder_name']}" + task_uri = f"{folder_uri}?task={test_hierarchy['task_name']}" + missing_uri = f"{folder_uri}_missing" + + result = resolve_uris([folder_uri, task_uri, missing_uri]) + assert [item["uri"] for item in result] == [ + folder_uri, task_uri, missing_uri + ] + folder_result, task_result, missing_result = result + + assert not folder_result.get("error") + assert len(folder_result["entities"]) == 1 + folder_entity = folder_result["entities"][0] + assert folder_entity["projectName"] == project_name + assert folder_entity["target"] == "folder" + assert _normalize_id(folder_entity["folderId"]) == folder_id + + assert not task_result.get("error") + assert len(task_result["entities"]) == 1 + task_entity = task_result["entities"][0] + assert task_entity["target"] == "task" + assert _normalize_id(task_entity["folderId"]) == folder_id + assert _normalize_id(task_entity["taskId"]) == task_id + + assert missing_result["entities"] == [] + + path_only_result = resolve_uris([folder_uri], path_only=True) + assert len(path_only_result) == 1 + assert len(path_only_result[0]["entities"]) == 1 + # Only file path is returned which is not available for folders + assert "folderId" not in path_only_result[0]["entities"][0] + + +def test_resolve_uris_invalid(): + """Invalid URI and unknown project are reported as errors.""" + missing_project = f"AYONApiMissing_{_unique_suffix()}" + invalid_uri = "invalid://project/path" + missing_uri = f"ayon://{missing_project}/folder" + + result = resolve_uris([invalid_uri, missing_uri]) + assert len(result) == 2 + invalid_result, missing_result = result + assert invalid_result["uri"] == invalid_uri + assert invalid_result["error"] + assert missing_result["uri"] == missing_uri + assert missing_project in missing_result["error"] + + +def test_get_project_file_info(project_entity_fixture): + """Uploaded project file information is returned.""" + project_name = project_entity_fixture["name"] + filename = f"projects_api_{_unique_suffix()}.png" + + response = upload_project_file( + project_name, + IMAGE_PATH, + filename=filename, + content_type="image/png", + ) + file_id = response.json()["id"] + try: + file_info = get_project_file_info(project_name, file_id) + assert file_info["size"] == os.path.getsize(IMAGE_PATH) + assert file_info["filename"] == filename + assert file_info["contentType"] == "image/png" + + finally: + delete_project_file(project_name, file_id) + + with pytest.raises(HTTPRequestError): + get_project_file_info(project_name, file_id) + + +# --- Folders --- +def test_search_folders(test_hierarchy): + """Search folders by text and by filters on folders and tasks.""" + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + folder_name = test_hierarchy["folder_name"] + task_name = test_hierarchy["task_name"] + + assert search_folders( + project_name, folder_search=folder_name + ) == [folder_id] + + assert search_folders(project_name, task_search=task_name) == [folder_id] + + folder_filter = { + "conditions": [ + {"key": "name", "value": folder_name, "operator": "eq"}, + ], + "operator": "and", + } + assert search_folders( + project_name, folder_filter=folder_filter + ) == [folder_id] + + task_filter = { + "conditions": [ + {"key": "name", "value": task_name, "operator": "eq"}, + ], + "operator": "and", + } + assert search_folders(project_name, task_filter=task_filter) == [ + folder_id + ] + + # Folder name does not match task conditions + assert search_folders(project_name, task_search=folder_name) == [] + assert search_folders( + project_name, folder_search=f"{folder_name}_missing" + ) == [] + + +def test_search_folders_global_search(test_hierarchy): + """Global search matches folders and folders of matching tasks.""" + _require_server_version((1, 15, 4)) + + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + + assert search_folders( + project_name, search=test_hierarchy["folder_name"] + ) == [folder_id] + assert search_folders( + project_name, search=test_hierarchy["task_name"] + ) == [folder_id] + + +def test_get_entity_groups(test_hierarchy, project_entity_fixture): + """Group folders by folder type and tasks by task type.""" + project_name = test_hierarchy["project_name"] + + result = get_entity_groups(project_name, "folder", "folderType") + assert result["key"] == "folderType" + assert result["entityType"] == "folder" + groups_by_value = {group["value"]: group for group in result["groups"]} + assert groups_by_value["Folder"]["count"] >= 1 + for group in result["groups"]: + assert group["count"] > 0 + + result = get_entity_groups( + project_name, "folder", "folderType", empty=True + ) + group_values = {group["value"] for group in result["groups"]} + folder_type_names = { + folder_type["name"] + for folder_type in project_entity_fixture["folderTypes"] + } + assert folder_type_names <= group_values + + result = get_entity_groups(project_name, "task", "taskType") + assert result["entityType"] == "task" + groups_by_value = {group["value"]: group for group in result["groups"]} + assert groups_by_value[test_hierarchy["task_type"]]["count"] >= 1 + + +def test_get_entity_groups_invalid_key(project_entity_fixture): + """Invalid grouping key raises an error.""" + with pytest.raises(HTTPRequestError): + get_entity_groups( + project_entity_fixture["name"], "folder", "invalidKey" + ) + + +# --- Thumbnails --- +def test_set_entity_thumbnail(test_hierarchy): + """Upload thumbnail of a folder from a file path.""" + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + + thumbnail_id = set_entity_thumbnail( + project_name, "folder", folder_id, IMAGE_PATH + ) + assert thumbnail_id + + folder = get_folder_by_id( + project_name, folder_id, fields={"id", "thumbnailId"} + ) + assert _normalize_id(folder["thumbnailId"]) == thumbnail_id + + thumbnail = get_folder_thumbnail(project_name, folder_id) + assert thumbnail.is_valid + assert _normalize_id(thumbnail.id) == thumbnail_id + assert thumbnail.content + + +def test_set_entity_thumbnail_from_stream(test_hierarchy): + """Upload thumbnail of a task from a byte stream.""" + project_name = test_hierarchy["project_name"] + task_id = test_hierarchy["task_id"] + + with open(IMAGE_PATH, "rb") as stream: + thumbnail_id = set_entity_thumbnail_from_stream( + project_name, "task", task_id, stream + ) + assert thumbnail_id + + task = get_task_by_id( + project_name, task_id, fields={"id", "thumbnailId"} + ) + assert _normalize_id(task["thumbnailId"]) == thumbnail_id + + thumbnail = get_task_thumbnail(project_name, task_id) + assert thumbnail.is_valid + assert _normalize_id(thumbnail.id) == thumbnail_id + assert thumbnail.content + + +def test_set_entity_thumbnail_invalid(test_hierarchy): + """Invalid inputs are refused before any request is sent.""" + project_name = test_hierarchy["project_name"] + folder_id = test_hierarchy["folder_id"] + + with pytest.raises(ValueError): + set_entity_thumbnail( + project_name, + "folder", + folder_id, + os.path.join(RESOURCES_DIR, "missing-image.png"), + ) + + with pytest.raises(ValueError): + set_entity_thumbnail( + project_name, "product", folder_id, IMAGE_PATH + ) + + +def test_project_thumbnail(temp_project): + """Set project thumbnail from file path and from stream.""" + _require_server_version((1, 15, 0)) + + project_name = temp_project["name"] + + # Project without thumbnail returns placeholder which is not valid + thumbnail = get_project_thumbnail(project_name) + assert not thumbnail.is_valid + + thumbnail_id = set_project_thumbnail(project_name, IMAGE_PATH) + assert thumbnail_id == PROJECT_THUMBNAIL_ID + thumbnail = get_project_thumbnail(project_name) + assert thumbnail.is_valid + assert thumbnail.id == PROJECT_THUMBNAIL_ID + assert thumbnail.content + assert thumbnail.content_type == "image/png" + + with open(IMAGE_PATH, "rb") as stream: + thumbnail_id = set_project_thumbnail_from_stream( + project_name, stream + ) + assert thumbnail_id == PROJECT_THUMBNAIL_ID + thumbnail = get_project_thumbnail(project_name) + assert thumbnail.is_valid + assert thumbnail.id == PROJECT_THUMBNAIL_ID + assert thumbnail.content + + with pytest.raises(ValueError): + set_project_thumbnail( + project_name, + os.path.join(RESOURCES_DIR, "missing-image.png"), + ) diff --git a/tests/test_review_tasks_lists_api.py b/tests/test_review_tasks_lists_api.py new file mode 100644 index 000000000..4b28efed9 --- /dev/null +++ b/tests/test_review_tasks_lists_api.py @@ -0,0 +1,575 @@ +"""Tests of reviewables, task assignees and entity list order endpoints. + +Tests run against a real AYON server and use the test project created +by 'project_entity_fixture'. Every entity created by the tests is +removed at the end. + +To run use: pytest --envfile {environment path}. +Make sure you have set AYON_SERVER_URL and AYON_API_KEY in +your environment. + +""" +import os +import uuid + +import pytest + +from ayon_api import ( + assign_users_to_task, + create_entity_list, + create_entity_list_item, + create_folder, + create_product, + create_task, + create_version, + delete_entity_list, + delete_folder, + get_entity_list_rest, + get_folder_reviewables, + get_product_reviewables, + get_reviewables, + get_server_version_tuple, + get_task_by_id, + get_task_reviewables, + get_user, + get_version_reviewables, + materialize_entity_list, + set_entity_list_items_order, + sort_version_reviewables, + update_reviewable, + upload_reviewable, +) +from ayon_api.exceptions import HTTPRequestError + +from .conftest import project_entity_fixture # noqa: F401 + +REVIEWABLE_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "resources", + "ayon-symbol.png", +) + + +def _unique_name(prefix): + return f"{prefix}_{uuid.uuid4().hex[:8]}" + + +def _find_status(project_entity, done): + """Find project status usable on versions. + + Args: + project_entity (dict[str, Any]): Project entity. + done (bool): Look for status with 'done' state. + + Returns: + Optional[str]: Status name or None if not found. + + """ + for status in project_entity["statuses"]: + scope = status.get("scope") + if scope and "version" not in scope: + continue + if (status.get("state") == "done") is done: + return status["name"] + return None + + +def _upload(project_name, version_id, label): + response = upload_reviewable( + project_name, version_id, REVIEWABLE_PATH, label=label + ) + response.raise_for_status() + return response.json() + + +def _get_filtered(func, *args, **kwargs): + """Call single entity reviewables getter with 'latest' filter. + + AYON server 1.16.x builds SQL condition 'versions.id = $1AND ...' + (missing space) for single entity endpoints with 'latest' or + 'latest_done', which PostgreSQL 15+ refuses. Mark the test as + expected failure in that case instead of failing on server bug. + + """ + try: + return func(*args, **kwargs) + except HTTPRequestError as exc: + response = exc.response + if response is not None and response.status_code >= 500: + pytest.xfail( + "Server fails on 'latest' filter of single entity" + f" reviewables endpoint: {exc}" + ) + raise + + +def _reviewables_by_version_id(versions): + return { + version["id"]: version["reviewables"] + for version in versions + } + + +@pytest.fixture +def folder_fixture(project_entity_fixture): # noqa: F811 + """Create a folder in test project and delete it after test.""" + project_name = project_entity_fixture["name"] + folder_id = create_folder( + project_name, + _unique_name("review_test"), + folder_type=project_entity_fixture["folderTypes"][0]["name"], + ) + try: + yield folder_id + finally: + delete_folder(project_name, folder_id, force=True) + + +@pytest.fixture +def task_fixture(project_entity_fixture, folder_fixture): # noqa: F811 + """Create a task without assignees under the test folder.""" + return create_task( + project_entity_fixture["name"], + _unique_name("task"), + project_entity_fixture["taskTypes"][0]["name"], + folder_fixture, + ) + + +@pytest.fixture +def reviewables_fixture( + project_entity_fixture, # noqa: F811 + folder_fixture, + task_fixture, +): + """Create product with 2 versions and upload reviewables to them. + + Version 1 has 'done' status and two reviewables, version 2 has + a not 'done' status and one reviewable. Version 3 lives in other + product and has no reviewables. + + """ + if get_server_version_tuple() < (1, 16, 4): + pytest.skip("Reviewables endpoints require AYON server 1.16.4+") + + project_name = project_entity_fixture["name"] + done_status = _find_status(project_entity_fixture, True) + not_done_status = _find_status(project_entity_fixture, False) + + product_id = create_product( + project_name, _unique_name("review"), "review", folder_fixture + ) + version_1_id = create_version( + project_name, + 1, + product_id, + task_id=task_fixture, + status=done_status, + ) + version_2_id = create_version( + project_name, + 2, + product_id, + task_id=task_fixture, + status=not_done_status, + ) + empty_product_id = create_product( + project_name, _unique_name("empty"), "review", folder_fixture + ) + empty_version_id = create_version(project_name, 1, empty_product_id) + + v1_reviewables = [ + _upload(project_name, version_1_id, "first"), + _upload(project_name, version_1_id, "second"), + ] + v2_reviewables = [ + _upload(project_name, version_2_id, "third"), + ] + return { + "project_name": project_name, + "folder_id": folder_fixture, + "task_id": task_fixture, + "product_id": product_id, + "version_1_id": version_1_id, + "version_2_id": version_2_id, + "empty_product_id": empty_product_id, + "empty_version_id": empty_version_id, + "done_status": done_status, + "v1_activity_ids": [r["activityId"] for r in v1_reviewables], + "v2_activity_ids": [r["activityId"] for r in v2_reviewables], + } + + +def test_upload_reviewable_response(reviewables_fixture): + """Upload of reviewable returns activity ids used by other tests.""" + activity_ids = ( + reviewables_fixture["v1_activity_ids"] + + reviewables_fixture["v2_activity_ids"] + ) + assert len(set(activity_ids)) == 3 + assert all(activity_ids) + + +def test_get_version_reviewables(reviewables_fixture): + """Version reviewables contain version info and its reviewables.""" + project_name = reviewables_fixture["project_name"] + version_id = reviewables_fixture["version_1_id"] + + version = get_version_reviewables(project_name, version_id) + assert version["id"] == version_id + assert version["name"] == "v001" + assert version["productId"] == reviewables_fixture["product_id"] + assert version["productType"] == "review" + + reviewables = version["reviewables"] + assert {r["activityId"] for r in reviewables} == set( + reviewables_fixture["v1_activity_ids"] + ) + for reviewable in reviewables: + assert reviewable["fileId"] + assert reviewable["filename"] == "ayon-symbol.png" + assert reviewable["mimetype"] == "image/png" + assert reviewable["author"]["name"] == get_user()["name"] + assert {r["label"] for r in reviewables} == {"first", "second"} + + +def test_get_version_reviewables_without_reviewables(reviewables_fixture): + """Version without reviewables returns empty 'reviewables'.""" + project_name = reviewables_fixture["project_name"] + version_id = reviewables_fixture["empty_version_id"] + + version = get_version_reviewables(project_name, version_id) + assert version["id"] == version_id + assert version["reviewables"] == [] + + # With 'latest' the server uses inner join and the version is not + # found -> server responds with error + with pytest.raises(HTTPRequestError): + get_version_reviewables(project_name, version_id, latest=True) + + +def test_get_product_reviewables(reviewables_fixture): + """Product reviewables contain all versions and filter by latest.""" + project_name = reviewables_fixture["project_name"] + product_id = reviewables_fixture["product_id"] + version_1_id = reviewables_fixture["version_1_id"] + version_2_id = reviewables_fixture["version_2_id"] + + by_id = _reviewables_by_version_id( + get_product_reviewables(project_name, product_id) + ) + assert set(by_id) == {version_1_id, version_2_id} + assert len(by_id[version_1_id]) == 2 + assert len(by_id[version_2_id]) == 1 + + latest = _get_filtered( + get_product_reviewables, project_name, product_id, latest=True + ) + assert [v["id"] for v in latest] == [version_2_id] + + if reviewables_fixture["done_status"] is None: + pytest.skip("Project does not have status with 'done' state") + + latest_done = _get_filtered( + get_product_reviewables, project_name, product_id, latest_done=True + ) + assert [v["id"] for v in latest_done] == [version_1_id] + + +def test_get_folder_reviewables(reviewables_fixture): + """Folder reviewables contain versions of all products in folder.""" + project_name = reviewables_fixture["project_name"] + folder_id = reviewables_fixture["folder_id"] + version_1_id = reviewables_fixture["version_1_id"] + version_2_id = reviewables_fixture["version_2_id"] + + by_id = _reviewables_by_version_id( + get_folder_reviewables(project_name, folder_id) + ) + assert set(by_id) == { + version_1_id, + version_2_id, + reviewables_fixture["empty_version_id"], + } + assert by_id[reviewables_fixture["empty_version_id"]] == [] + + # Latest version of the empty product has no reviewables, so it is + # not in the output + latest = _get_filtered( + get_folder_reviewables, project_name, folder_id, latest=True + ) + assert [v["id"] for v in latest] == [version_2_id] + + if reviewables_fixture["done_status"] is None: + pytest.skip("Project does not have status with 'done' state") + + latest_done = _get_filtered( + get_folder_reviewables, project_name, folder_id, latest_done=True + ) + assert [v["id"] for v in latest_done] == [version_1_id] + + +def test_get_task_reviewables(reviewables_fixture): + """Task reviewables contain only versions linked to the task.""" + project_name = reviewables_fixture["project_name"] + task_id = reviewables_fixture["task_id"] + version_1_id = reviewables_fixture["version_1_id"] + version_2_id = reviewables_fixture["version_2_id"] + + by_id = _reviewables_by_version_id( + get_task_reviewables(project_name, task_id) + ) + assert set(by_id) == {version_1_id, version_2_id} + + latest = _get_filtered( + get_task_reviewables, project_name, task_id, latest=True + ) + assert [v["id"] for v in latest] == [version_2_id] + + +@pytest.mark.parametrize( + "entity_type", ["version", "product", "folder", "task"] +) +def test_get_reviewables(reviewables_fixture, entity_type): + """Batch reviewables endpoint works for all supported entity types.""" + project_name = reviewables_fixture["project_name"] + version_1_id = reviewables_fixture["version_1_id"] + version_2_id = reviewables_fixture["version_2_id"] + entity_ids = { + "version": [version_1_id, version_2_id], + "product": [reviewables_fixture["product_id"]], + "folder": [reviewables_fixture["folder_id"]], + "task": [reviewables_fixture["task_id"]], + }[entity_type] + + by_id = _reviewables_by_version_id( + get_reviewables(project_name, entity_type, entity_ids) + ) + assert {version_1_id, version_2_id} <= set(by_id) + assert {r["activityId"] for r in by_id[version_1_id]} == set( + reviewables_fixture["v1_activity_ids"] + ) + assert {r["activityId"] for r in by_id[version_2_id]} == set( + reviewables_fixture["v2_activity_ids"] + ) + + # Iterable (not only list) of entity ids is accepted + latest = get_reviewables( + project_name, + entity_type, + iter(entity_ids), + latest=entity_type != "version", + ) + latest_ids = {v["id"] for v in latest} + if entity_type == "version": + assert latest_ids == {version_1_id, version_2_id} + else: + assert latest_ids == {version_2_id} + + +def test_get_reviewables_empty_ids(reviewables_fixture): + """Batch reviewables endpoint with unknown ids returns empty list.""" + result = get_reviewables( + reviewables_fixture["project_name"], + "version", + [uuid.uuid4().hex], + ) + assert result == [] + + +def test_sort_version_reviewables(reviewables_fixture): + """Order of reviewables is changed and invalid order is refused.""" + project_name = reviewables_fixture["project_name"] + version_id = reviewables_fixture["version_1_id"] + + current_order = [ + r["activityId"] + for r in get_version_reviewables( + project_name, version_id + )["reviewables"] + ] + assert len(current_order) == 2 + + for new_order in ( + list(reversed(current_order)), + list(current_order), + ): + sort_version_reviewables(project_name, version_id, new_order) + version = get_version_reviewables(project_name, version_id) + assert [ + r["activityId"] for r in version["reviewables"] + ] == new_order + + # All reviewables of the version must be passed + with pytest.raises(HTTPRequestError): + sort_version_reviewables( + project_name, version_id, current_order[:1] + ) + + # Reviewables of other versions are not allowed + with pytest.raises(HTTPRequestError): + sort_version_reviewables( + project_name, + version_id, + current_order + reviewables_fixture["v2_activity_ids"], + ) + + +def test_update_reviewable(reviewables_fixture): + """Label of reviewable can be changed.""" + project_name = reviewables_fixture["project_name"] + version_id = reviewables_fixture["version_2_id"] + activity_id = reviewables_fixture["v2_activity_ids"][0] + new_label = _unique_name("label") + + update_reviewable( + project_name, version_id, activity_id, label=new_label + ) + version = get_version_reviewables(project_name, version_id) + labels = { + r["activityId"]: r["label"] + for r in version["reviewables"] + } + assert labels == {activity_id: new_label} + + # Nothing to change -> no error and label stays + update_reviewable(project_name, version_id, activity_id) + version = get_version_reviewables(project_name, version_id) + assert version["reviewables"][0]["label"] == new_label + + # Empty label is refused by server + with pytest.raises(HTTPRequestError): + update_reviewable(project_name, version_id, activity_id, label="") + + +def _get_assignees(project_name, task_id): + task = get_task_by_id( + project_name, task_id, fields={"id", "assignees"} + ) + return set(task["assignees"]) + + +def test_assign_users_to_task( + project_entity_fixture, # noqa: F811 + task_fixture, +): + """Current user is added, removed and set as task assignee.""" + project_name = project_entity_fixture["name"] + task_id = task_fixture + username = get_user()["name"] + + assert _get_assignees(project_name, task_id) == set() + + # Default mode is 'add' + assign_users_to_task(project_name, task_id, [username]) + assert _get_assignees(project_name, task_id) == {username} + + # Adding already assigned user does not change anything + assign_users_to_task(project_name, task_id, [username], mode="add") + assert _get_assignees(project_name, task_id) == {username} + + assign_users_to_task(project_name, task_id, [username], mode="remove") + assert _get_assignees(project_name, task_id) == set() + + # Removing not assigned user does not change anything + assign_users_to_task(project_name, task_id, [username], mode="remove") + assert _get_assignees(project_name, task_id) == set() + + # Iterable (not only list) of users is accepted + assign_users_to_task( + project_name, task_id, iter([username]), mode="set" + ) + assert _get_assignees(project_name, task_id) == {username} + + assign_users_to_task(project_name, task_id, [], mode="set") + assert _get_assignees(project_name, task_id) == set() + + +@pytest.fixture +def entity_list_fixture( + project_entity_fixture, # noqa: F811 + folder_fixture, +): + """Create entity list of folders with 3 items. + + Yields: + tuple[str, str, list[str]]: Project name, list id and item ids + in their initial order. + + """ + project_name = project_entity_fixture["name"] + folder_type = project_entity_fixture["folderTypes"][0]["name"] + folder_ids = [ + create_folder( + project_name, + _unique_name("list_item"), + folder_type=folder_type, + parent_id=folder_fixture, + ) + for _ in range(3) + ] + list_id = create_entity_list( + project_name, "folder", _unique_name("test_list") + ) + try: + item_ids = [ + create_entity_list_item( + project_name, list_id, folder_id, position=idx + ) + for idx, folder_id in enumerate(folder_ids) + ] + yield project_name, list_id, item_ids + finally: + delete_entity_list(project_name, list_id) + + +def _get_list_item_ids_in_order(project_name, list_id): + entity_list = get_entity_list_rest(project_name, list_id) + items = sorted(entity_list["items"], key=lambda i: i["position"]) + return [item["id"] for item in items] + + +def test_set_entity_list_items_order(entity_list_fixture): + """Order of entity list items is changed.""" + if get_server_version_tuple() < (1, 15, 9): + pytest.skip("Items order endpoint requires AYON server 1.15.9+") + + project_name, list_id, item_ids = entity_list_fixture + assert _get_list_item_ids_in_order(project_name, list_id) == item_ids + + new_order = [item_ids[2], item_ids[0], item_ids[1]] + set_entity_list_items_order(project_name, list_id, new_order) + assert _get_list_item_ids_in_order(project_name, list_id) == new_order + + # Order must contain each item exactly once + for invalid_order in ( + new_order[:2], + new_order + [new_order[0]], + new_order[:2] + [uuid.uuid4().hex], + ): + with pytest.raises(HTTPRequestError): + set_entity_list_items_order( + project_name, list_id, invalid_order + ) + assert _get_list_item_ids_in_order(project_name, list_id) == new_order + + +def test_materialize_entity_list(entity_list_fixture): + """Materialize entity list returns list summary. + + Materialization is not implemented on some server versions + (e.g. 1.16.8 responds with 501), in that case the test is skipped. + + """ + project_name, list_id, item_ids = entity_list_fixture + try: + summary = materialize_entity_list(project_name, list_id) + except HTTPRequestError as exc: + if exc.response is not None and exc.response.status_code == 501: + pytest.skip("Materialize is not implemented on server") + raise + + assert summary["id"] == list_id + assert summary["entityType"] == "folder" + assert "label" in summary + assert summary["count"] >= len(item_ids) diff --git a/tests/test_server_version_validation.py b/tests/test_server_version_validation.py new file mode 100644 index 000000000..5a09f7152 --- /dev/null +++ b/tests/test_server_version_validation.py @@ -0,0 +1,348 @@ +"""Tests of server version validation of version dependent methods. + +Methods using endpoints or arguments that were added in newer AYON server +versions raise 'UnsupportedServerVersion' when the connected server is older. +These tests patch the server version of a 'ServerAPI' instance and replace +all request methods so no request can reach any server. + +These tests do NOT need a running AYON server. + +""" +import io + +import pytest + +from ayon_api import ServerAPI +from ayon_api.exceptions import UnsupportedServerVersion + +# Minimal PNG signature so mime type detection recognizes the content +PNG_CONTENT = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16 + +# Methods that send a request to server (or would fetch server info) +REQUEST_METHODS = ( + "get_info", + "get", + "post", + "put", + "patch", + "delete", + "raw_get", + "raw_post", + "raw_put", + "raw_patch", + "raw_delete", + "query_graphql", + "upload_file", + "upload_file_from_stream", + "_upload_file", + "_do_rest_request", +) + + +class RequestSent(Exception): + """Raised by patched request methods instead of sending a request.""" + def __init__(self, method_name, args, kwargs): + super().__init__(method_name) + self.method_name = method_name + self.call_args = args + self.call_kwargs = kwargs + + +def _version_below(threshold): + """Version tuple just below the threshold version.""" + major, minor, patch = threshold + if patch > 0: + return major, minor, patch - 1, "", "" + return major, minor - 1, 99, "", "" + + +def _version_at(threshold): + major, minor, patch = threshold + return major, minor, patch, "", "" + + +def _create_blocker(method_name): + def _blocker(*args, **kwargs): + raise RequestSent(method_name, args, kwargs) + return _blocker + + +def _create_api(server_version): + """Create 'ServerAPI' that cannot send any request. + + Args: + server_version (ServerVersion): Version returned by + 'get_server_version_tuple'. + + Returns: + ServerAPI: Server api object. + + """ + api = ServerAPI( + "http://127.0.0.1:1", + site_id=None, + create_session=False, + ) + api.get_server_version_tuple = lambda: server_version + for method_name in REQUEST_METHODS: + setattr(api, method_name, _create_blocker(method_name)) + return api + + +@pytest.fixture +def thumbnail_path(tmp_path): + path = tmp_path / "thumbnail.png" + path.write_bytes(PNG_CONTENT) + return str(path) + + +# Each item contains test id, minimum server version and callback that +# calls the gated method with gated argument. +GATED_CALLS = [ + ( + "rename_project", + (1, 15, 0), + lambda api, path: api.rename_project("old_name", "new_name"), + ), + ( + "get_project_thumbnail", + (1, 15, 0), + lambda api, path: api.get_project_thumbnail("project"), + ), + ( + "set_project_thumbnail", + (1, 15, 0), + lambda api, path: api.set_project_thumbnail("project", path), + ), + ( + "set_project_thumbnail_from_stream", + (1, 15, 0), + lambda api, path: api.set_project_thumbnail_from_stream( + "project", io.BytesIO(PNG_CONTENT) + ), + ), + ( + "search_folders-search", + (1, 15, 4), + lambda api, path: api.search_folders("project", search="text"), + ), + ( + "update_server_config", + (1, 15, 9), + lambda api, path: api.update_server_config({"studio_name": "x"}), + ), + ( + "set_entity_list_items_order", + (1, 15, 9), + lambda api, path: api.set_entity_list_items_order( + "project", "list_id", ["item_id"] + ), + ), + ( + "get_enums", + (1, 16, 4), + lambda api, path: api.get_enums(), + ), + ( + "get_reviewables", + (1, 16, 4), + lambda api, path: api.get_reviewables( + "project", "version", ["version_id"] + ), + ), + ( + "get_version_reviewables-latest", + (1, 16, 4), + lambda api, path: api.get_version_reviewables( + "project", "version_id", latest=True + ), + ), + ( + "get_version_reviewables-latest_done", + (1, 16, 4), + lambda api, path: api.get_version_reviewables( + "project", "version_id", latest_done=True + ), + ), + ( + "get_product_reviewables-latest", + (1, 16, 4), + lambda api, path: api.get_product_reviewables( + "project", "product_id", latest=True + ), + ), + ( + "get_folder_reviewables-latest_done", + (1, 16, 4), + lambda api, path: api.get_folder_reviewables( + "project", "folder_id", latest_done=True + ), + ), + ( + "get_task_reviewables-latest", + (1, 16, 4), + lambda api, path: api.get_task_reviewables( + "project", "task_id", latest=True + ), + ), + ( + "update_service-error", + (1, 16, 7), + lambda api, path: api.update_service( + "service", should_run=False, error="Failed" + ), + ), + ( + "migrate_addon_settings", + (1, 16, 8), + lambda api, path: api.migrate_addon_settings( + "addon", "1.0.0", "1.0.1" + ), + ), +] + +# Calls of gated methods without the gated argument, they must work +# with server older than the version required by the argument. +UNGATED_ARG_CALLS = [ + ( + "search_folders-no-search", + (1, 15, 4), + lambda api: api.search_folders("project", folder_search="text"), + ), + ( + "get_version_reviewables", + (1, 16, 4), + lambda api: api.get_version_reviewables("project", "version_id"), + ), + ( + "get_product_reviewables", + (1, 16, 4), + lambda api: api.get_product_reviewables("project", "product_id"), + ), + ( + "get_folder_reviewables", + (1, 16, 4), + lambda api: api.get_folder_reviewables("project", "folder_id"), + ), + ( + "get_task_reviewables", + (1, 16, 4), + lambda api: api.get_task_reviewables("project", "task_id"), + ), + ( + "update_service-no-error", + (1, 16, 7), + lambda api: api.update_service("service", should_run=False), + ), +] + + +def _gated_params(calls): + return [ + pytest.param(threshold, func, id=test_id) + for test_id, threshold, func in calls + ] + + +@pytest.mark.parametrize("threshold, func", _gated_params(GATED_CALLS)) +def test_unsupported_version_raises(threshold, func, thumbnail_path): + """Gated method raises before any request on older server. + + Verifies: + - 'UnsupportedServerVersion' is raised when server version is + just below the required version. + - No request method is called (blocked request methods would + raise 'RequestSent' instead). + + """ + api = _create_api(_version_below(threshold)) + with pytest.raises(UnsupportedServerVersion): + func(api, thumbnail_path) + + +@pytest.mark.parametrize("threshold, func", _gated_params(GATED_CALLS)) +def test_supported_version_sends_request(threshold, func, thumbnail_path): + """Gated method sends request on server with the required version. + + Verifies: + - Version check does not raise when server version equals + the required version and request is sent. + + """ + api = _create_api(_version_at(threshold)) + with pytest.raises(RequestSent): + func(api, thumbnail_path) + + +@pytest.mark.parametrize( + "threshold, func", _gated_params(UNGATED_ARG_CALLS) +) +def test_ungated_arguments_on_older_server(threshold, func): + """Method without gated argument works on older server. + + Verifies: + - Request is sent when the gated argument is not used even if + server version is below the version required by the argument. + + """ + api = _create_api(_version_below(threshold)) + with pytest.raises(RequestSent): + func(api) + + +class _FakeResponse: + def raise_for_status(self): + pass + + +def _create_rename_user_api(server_version): + api = _create_api(server_version) + calls = [] + + def _create_recorder(method_name): + def _recorder(entrypoint, **kwargs): + calls.append((method_name, entrypoint, kwargs)) + return _FakeResponse() + return _recorder + + api.post = _create_recorder("post") + api.patch = _create_recorder("patch") + return api, calls + + +@pytest.mark.parametrize( + "server_version", + [(1, 14, 99, "", ""), (1, 14, 0, "", ""), (1, 13, 5, "", "")] +) +def test_rename_user_old_server_uses_patch(server_version): + """'rename_user' uses deprecated PATCH endpoint before 1.15.0. + + Verifies: + - Single PATCH request is sent to 'users/{name}/rename'. + - New name is passed in 'newName' key. + + """ + api, calls = _create_rename_user_api(server_version) + api.rename_user("old_user", "new_user") + assert calls == [ + ("patch", "users/old_user/rename", {"newName": "new_user"}) + ] + + +@pytest.mark.parametrize( + "server_version", + [(1, 15, 0, "", ""), (1, 15, 1, "", ""), (1, 16, 8, "", "")] +) +def test_rename_user_new_server_uses_post(server_version): + """'rename_user' uses POST endpoint from 1.15.0. + + Verifies: + - Single POST request is sent to 'users/{name}/rename'. + - New name is passed in 'name' key. + + """ + api, calls = _create_rename_user_api(server_version) + api.rename_user("old_user", "new_user") + assert calls == [ + ("post", "users/old_user/rename", {"name": "new_user"}) + ] diff --git a/tests/test_system_api.py b/tests/test_system_api.py new file mode 100644 index 000000000..18434861d --- /dev/null +++ b/tests/test_system_api.py @@ -0,0 +1,531 @@ +"""Tests of system, services and events related server API. + +Tests run against a real AYON server and require an admin user. + +To run use: pytest --envfile {environment path}. +Make sure you have set AYON_SERVER_URL and AYON_API_KEY in your environment. + +All entities created by the tests use unique names and are removed after +the test. Server-wide state that is changed by a test is restored. + +""" +import time +import uuid + +import pytest + +from ayon_api import ( + delete_event, + delete_events, + delete_service, + delete_site, + dispatch_event, + get_addons_info, + get_enum, + get_enums, + get_events, + get_hosts, + get_project, + get_restart_required, + get_server_api_connection, + get_server_config, + get_server_config_schema, + get_server_version_tuple, + get_services, + get_sites, + get_user, + set_restart_required, + spawn_service, + update_server_config, + update_service, + exceptions, +) + +TEST_PREFIX = "ayon-api-test" + + +def _unique_name(prefix=TEST_PREFIX): + return f"{prefix}-{uuid.uuid4().hex[:12]}" + + +def _topic_filter(topic): + """Events filter matching single topic. + + Never returns empty filter, empty filter would delete all events. + + """ + assert topic + return { + "conditions": [ + { + "key": "topic", + "value": topic, + "operator": "eq", + } + ], + "operator": "and", + } + + +def _require_server_version(version, func_name): + if get_server_version_tuple() < version: + version_str = ".".join(str(part) for part in version) + pytest.skip( + f"'{func_name}' requires AYON server {version_str} or newer" + ) + + +def _get_service(service_name): + for service in get_services(): + if service["name"] == service_name: + return service + return None + + +def _find_addon_service(): + """Find addon version that defines a service with an image. + + Returns: + Optional[tuple[str, str, str]]: Addon name, addon version and + service name. + + """ + for addon in get_addons_info(details=True)["addons"]: + for version, version_info in addon["versions"].items(): + services = version_info.get("services") or {} + for service_name, service_def in services.items(): + if isinstance(service_def, dict) and service_def.get("image"): + return addon["name"], version, service_name + return None + + +# --- Services --- +def test_get_services(): + """Tests 'get_services' returns services in expected shape. + + Verifies: + - Result is a list. + - Each service contains name, host, addon and run state keys. + + """ + services = get_services() + assert isinstance(services, list) + for service in services: + for key in ( + "name", + "hostname", + "addonName", + "addonVersion", + "service", + "shouldRun", + "isRunning", + "data", + ): + assert key in service + + +def test_get_hosts(): + """Tests 'get_hosts' returns hosts in expected shape. + + Verifies: + - Result is a list. + - Each host contains name, last seen time and health information. + + """ + hosts = get_hosts() + assert isinstance(hosts, list) + for host in hosts: + assert isinstance(host["name"], str) + assert "lastSeen" in host + assert set(host["health"]) >= {"cpu", "mem"} + + +@pytest.fixture +def addon_service(): + """Addon service definition usable for spawning a service. + + Server allows spawning only services defined by installed addon, + the test is skipped if there is no such addon on server. + + """ + addon_service = _find_addon_service() + if addon_service is None: + pytest.skip("Server does not have any addon defining a service") + return addon_service + + +@pytest.fixture +def spawned_service_name(): + """Unique service name, the service is deleted on teardown.""" + service_name = _unique_name() + yield service_name + if _get_service(service_name) is not None: + delete_service(service_name) + + +def test_spawn_update_delete_service(addon_service, spawned_service_name): + """Tests spawn, update and delete of addon service. + + Service is spawned on a non-existent hostname so no service host + will ever run it, and it is stopped right after spawn. + + Verifies: + - Spawned service is listed with passed hostname and addon info. + - 'update_service' changes 'shouldRun', 'hostname' and config. + - 'update_service' sets 'error' (server 1.16.7+). + - Deleted service is not listed anymore. + + """ + addon_name, addon_version, service = addon_service + service_name = spawned_service_name + hostname = _unique_name(f"{TEST_PREFIX}-host") + spawn_service( + service_name, + addon_name, + addon_version, + service, + hostname, + ) + # Stop the service immediately + update_service(service_name, should_run=False) + + service_data = _get_service(service_name) + assert service_data is not None + assert service_data["hostname"] == hostname + assert service_data["addonName"] == addon_name + assert service_data["addonVersion"] == addon_version + assert service_data["service"] == service + assert service_data["shouldRun"] is False + + new_hostname = _unique_name(f"{TEST_PREFIX}-host") + env = {"AYON_API_TEST": service_name} + update_service( + service_name, + hostname=new_hostname, + config={"env": env}, + ) + service_data = _get_service(service_name) + assert service_data["hostname"] == new_hostname + assert service_data["shouldRun"] is False + assert service_data["data"]["env"] == env + + if get_server_version_tuple() >= (1, 16, 7): + error_msg = "AYON api test error" + update_service(service_name, should_run=False, error=error_msg) + service_data = _get_service(service_name) + assert service_data["data"]["error"] == error_msg + + delete_service(service_name) + assert _get_service(service_name) is None + + +# --- Sites --- +@pytest.fixture +def temp_site(): + """Register temporary site on server. + + Server registers a site when '/api/info' is requested with site + headers 'x-ayon-site-id', 'x-ayon-platform', 'x-ayon-hostname' + and 'x-ayon-version'. Site is deleted on teardown if still exists. + + """ + site_id = _unique_name() + site_info = { + "id": site_id, + "platform": "linux", + "hostname": _unique_name(f"{TEST_PREFIX}-host"), + "version": "0.0.0", + } + con = get_server_api_connection() + headers = con.get_headers() + headers.update({ + "x-ayon-site-id": site_info["id"], + "x-ayon-platform": site_info["platform"], + "x-ayon-hostname": site_info["hostname"], + "x-ayon-version": site_info["version"], + }) + response = con.raw_get("info", headers=headers) + response.raise_for_status() + + yield site_info + + if any(site["id"] == site_id for site in get_sites()): + delete_site(site_id) + + +def test_get_sites(temp_site): + """Tests 'get_sites' with and without filters. + + Verifies: + - Registered site is listed with platform, hostname, version + and the current user. + - Filter by hostname returns only the registered site. + - Filter by different platform does not return the site. + + """ + sites = get_sites() + assert isinstance(sites, list) + site_by_id = {site["id"]: site for site in sites} + assert temp_site["id"] in site_by_id + site = site_by_id[temp_site["id"]] + assert site["platform"] == temp_site["platform"] + assert site["hostname"] == temp_site["hostname"] + assert site["version"] == temp_site["version"] + assert get_user()["name"] in site["users"] + + sites = get_sites(hostname=temp_site["hostname"]) + assert [site["id"] for site in sites] == [temp_site["id"]] + + sites = get_sites( + platform_name=temp_site["platform"], + hostname=temp_site["hostname"], + ) + assert [site["id"] for site in sites] == [temp_site["id"]] + + sites = get_sites( + platform_name="windows", + hostname=temp_site["hostname"], + ) + assert sites == [] + + +def test_delete_site(temp_site): + """Tests 'delete_site' unregisters site. + + Verifies: + - Deleted site is not listed anymore. + - Deleting a non-existent site raises an error. + + """ + site_id = temp_site["id"] + delete_site(site_id) + assert all(site["id"] != site_id for site in get_sites()) + + with pytest.raises(exceptions.HTTPRequestError): + delete_site(site_id) + + +# --- Restart required --- +def test_get_restart_required(): + """Tests 'get_restart_required' response shape.""" + result = get_restart_required() + assert isinstance(result["required"], bool) + if result["required"]: + assert isinstance(result["reason"], str) + + +def test_set_restart_required(): + """Tests set and clear of server restart required flag. + + Test is skipped if server already requires restart, so the original + flag is not changed. + + Verifies: + - Flag is set after 'set_restart_required(True)'. + - Flag is cleared after 'set_restart_required(False)'. + + """ + original = get_restart_required() + if original["required"]: + pytest.skip("Server already requires restart, not changing the flag") + + try: + set_restart_required(True, reason="AYON api test") + # Server ignores restart required events younger than 2 seconds + time.sleep(2.5) + result = get_restart_required() + assert result["required"] is True + assert isinstance(result["reason"], str) + + finally: + set_restart_required(False) + + assert get_restart_required()["required"] is False + + +# --- Server config --- +def test_get_server_config_schema(): + """Tests 'get_server_config_schema' returns JSON schema. + + Verifies: + - Schema has object properties containing 'studio_name' which + is also available in server config. + + """ + schema = get_server_config_schema() + assert isinstance(schema, dict) + assert schema["title"] == "Server Configuration" + assert "studio_name" in schema["properties"] + assert "studio_name" in get_server_config() + + +def test_update_server_config(): + """Tests partial update of server config. + + Studio name is changed and restored afterwards. + + Verifies: + - Studio name is changed. + - Other config values are unchanged. + - Original studio name is restored. + + """ + _require_server_version((1, 15, 9), "update_server_config") + + original_config = get_server_config() + original_name = original_config["studio_name"] + new_name = f"AYON api test {uuid.uuid4().hex[:8]}" + try: + update_server_config({"studio_name": new_name}) + config = get_server_config() + assert config["studio_name"] == new_name + + config.pop("studio_name") + expected = dict(original_config) + expected.pop("studio_name") + assert config == expected + + finally: + update_server_config({"studio_name": original_name}) + + assert get_server_config()["studio_name"] == original_name + + +# --- Enums --- +def test_get_enums(): + """Tests 'get_enums' lists enum resolvers. + + Verifies: + - Each resolver has name, label and accepted params. + - Built-in resolvers are available. + + """ + _require_server_version((1, 16, 4), "get_enums") + + enums = get_enums() + assert isinstance(enums, list) + for enum_info in enums: + assert isinstance(enum_info["name"], str) + assert isinstance(enum_info["label"], str) + assert isinstance(enum_info["acceptedParams"], dict) + + enums_by_name = {enum_info["name"]: enum_info for enum_info in enums} + assert {"folderTypes", "taskTypes", "statuses"} <= set(enums_by_name) + assert "project_name" in enums_by_name["folderTypes"]["acceptedParams"] + + +def test_get_enum(project_entity_fixture): + """Tests 'get_enum' with project, user and context parameters. + + Verifies: + - Items without project have value and label. + - Project folder types match folder types of the project. + - Resolving for user (admin only) works. + - Context parameters are passed to the resolver. + - Invalid context value raises an error. + + """ + project_name = project_entity_fixture["name"] + project_entity = get_project(project_name) + + items = get_enum("folderTypes") + assert isinstance(items, list) + for item in items: + assert "value" in item + assert isinstance(item["label"], str) + + items = get_enum("folderTypes", project_name=project_name) + expected_folder_types = { + folder_type["name"] + for folder_type in project_entity["folderTypes"] + } + assert {item["value"] for item in items} == expected_folder_types + + items = get_enum( + "folderTypes", + project_name=project_name, + user_name=get_user()["name"], + ) + assert {item["value"] for item in items} == expected_folder_types + + expected_statuses = { + status["name"] + for status in project_entity["statuses"] + if "task" in (status.get("scope") or []) + } + items = get_enum( + "statuses", + project_name=project_name, + context={"entity_type": "task"}, + ) + assert {item["value"] for item in items} == expected_statuses + + with pytest.raises(exceptions.HTTPRequestError): + get_enum( + "statuses", + project_name=project_name, + context={"entity_type": "invalid"}, + ) + + +# --- Events --- +@pytest.fixture +def event_topics(): + """Two unique event topics, events are deleted on teardown.""" + topics = [ + f"ayon_api.test.delete_events.{uuid.uuid4().hex}" + for _ in range(2) + ] + yield topics + for event in list(get_events(topics=topics, fields={"id"})): + delete_event(event["id"]) + + +def _get_event_ids(topic): + return { + event["id"] + for event in get_events(topics=[topic], fields={"id"}) + } + + +def test_delete_events(event_topics): + """Tests 'delete_events' removes only events matching filter. + + Verifies: + - Events with filtered topic are deleted. + - Events with other topic are kept. + + """ + topic_to_delete, topic_to_keep = event_topics + created_ids = {} + for topic in event_topics: + created_ids[topic] = { + dispatch_event( + topic, + sender="ayon_api_tests", + description=f"AYON api test event {idx}", + )["id"] + for idx in range(3) + } + + for topic in event_topics: + assert _get_event_ids(topic) == created_ids[topic] + + delete_events(_topic_filter(topic_to_delete)) + + assert _get_event_ids(topic_to_delete) == set() + assert _get_event_ids(topic_to_keep) == created_ids[topic_to_keep] + + +@pytest.mark.parametrize( + "events_filter", + [ + None, + {}, + {"conditions": [], "operator": "and"}, + ], +) +def test_delete_events_without_conditions_raises(events_filter): + """Tests 'delete_events' refuses filter that would delete all events.""" + with pytest.raises(ValueError): + delete_events(events_filter) diff --git a/tests/test_users_api.py b/tests/test_users_api.py new file mode 100644 index 000000000..94a1ffdb8 --- /dev/null +++ b/tests/test_users_api.py @@ -0,0 +1,670 @@ +"""Tests of users, access groups, teams and project users endpoints. + +Tests are running against a real AYON server and expect that the +connection is authorized as an admin user. + +To run use: pytest --envfile {environment path}. +Make sure you have set AYON_SERVER_URL and AYON_API_KEY in your +environment. + +All users, access groups and teams created by the tests have unique names +and are removed at the end of each test. + +""" + +import time +import uuid + +import pytest + +from ayon_api import ( + ServerAPI, + get_base_url, + get_user, + create_user, + update_user, + delete_user, + rename_user, + change_password, + set_user_access_groups, + get_user_sessions, + delete_user_session, + get_user_api_keys, + create_user_api_key, + update_user_api_key, + delete_user_api_key, + get_user_permissions, + get_access_groups, + get_access_group_schema, + get_access_group, + save_access_group, + delete_access_group, + set_projects_access, + get_teams, + update_teams, + save_team, + delete_team, + save_team_member, + delete_team_member, + get_project_users, + set_project_user_access_groups, +) +from ayon_api.exceptions import AuthenticationError, HTTPRequestError + +from .conftest import project_entity_fixture + + +def _unique_name(prefix): + return f"{prefix}{uuid.uuid4().hex[:12]}" + + +def _new_password(): + # Admin is not affected by password complexity check, but use + # a strong password anyway + return f"Ayon_{uuid.uuid4().hex}.9" + + +def _user_exists(user_name): + try: + return get_user(user_name) is not None + except HTTPRequestError: + return False + + +def _safe_delete_user(user_name): + if _user_exists(user_name): + delete_user(user_name) + + +@pytest.fixture +def temp_user(): + """Create a temporary non-manager user with a password. + + Yields: + dict[str, str]: Mutable dictionary with 'name' and 'password'. + Tests that rename the user must update the 'name' so + the teardown removes the right user. + + """ + user_info = { + "name": _unique_name("ayonapitest"), + "password": _new_password(), + } + create_user( + user_info["name"], + password=user_info["password"], + attrib={"fullName": "AYON API Test User"}, + data={"isAdmin": False, "isManager": False}, + active=True, + ) + try: + yield user_info + finally: + _safe_delete_user(user_info["name"]) + + +@pytest.fixture +def temp_access_group(): + """Create a temporary studio level access group. + + Yields: + str: Name of the access group. + + """ + access_group_name = _unique_name("ayonapitest_ag_") + save_access_group(access_group_name, {"read": {"enabled": False}}) + try: + yield access_group_name + finally: + studio_names = { + access_group["name"] + for access_group in get_access_groups() + } + if access_group_name in studio_names: + delete_access_group(access_group_name) + + +def _login(user_name, password): + """Login to server as a different user. + + Returns: + ServerAPI: Connection authorized as the user. + + """ + con = ServerAPI(get_base_url()) + con.login(user_name, password) + return con + + +def _get_team(project_name, team_name, show_members=True): + for team in get_teams(project_name, show_members=show_members): + if team["name"] == team_name: + return team + return None + + +def _safe_delete_team(project_name, team_name): + if _get_team(project_name, team_name, show_members=False) is not None: + delete_team(project_name, team_name) + + +# --- Users --- +def test_create_update_delete_user(): + """Create a user, update attributes and active state, then delete.""" + user_name = _unique_name("ayonapitest") + create_user( + user_name, + password=_new_password(), + attrib={"fullName": "Before Update"}, + active=True, + ) + try: + user = get_user(user_name) + assert user["name"] == user_name + assert user["attrib"]["fullName"] == "Before Update" + assert user["active"] is True + + # Creating already existing user fails + with pytest.raises(HTTPRequestError): + create_user(user_name, password=_new_password()) + + update_user( + user_name, + attrib={"fullName": "After Update"}, + active=False, + ) + user = get_user(user_name) + assert user["attrib"]["fullName"] == "After Update" + assert user["active"] is False + + delete_user(user_name) + assert not _user_exists(user_name) + finally: + _safe_delete_user(user_name) + + +def test_create_service_user_with_api_key(): + """Service users can be created with api key, and key can change. + + Service users must not have password. + """ + user_name = _unique_name("ayonapitestsvc") + create_user( + user_name, + api_key=uuid.uuid4().hex, + data={"isService": True}, + ) + try: + user = get_user(user_name) + assert user["name"] == user_name + assert user["data"].get("isService") is True + + change_password(user_name, api_key=uuid.uuid4().hex) + + # Password can't be set on service user on creation + invalid_name = _unique_name("ayonapitestsvc") + try: + with pytest.raises(HTTPRequestError): + create_user( + invalid_name, + password=_new_password(), + data={"isService": True}, + ) + finally: + _safe_delete_user(invalid_name) + finally: + _safe_delete_user(user_name) + + +def test_rename_user(temp_user): + """Rename user and validate the old name does not exist anymore.""" + old_name = temp_user["name"] + new_name = _unique_name("ayonapitestrenamed") + rename_user(old_name, new_name) + # Make sure the fixture deletes the renamed user + temp_user["name"] = new_name + + assert not _user_exists(old_name) + user = get_user(new_name) + assert user["name"] == new_name + assert user["attrib"]["fullName"] == "AYON API Test User" + + +def test_change_password(temp_user): + """Change password and validate login works only with the new one.""" + user_name = temp_user["name"] + new_password = _new_password() + + with pytest.raises(ValueError): + change_password(user_name) + + change_password(user_name, password=new_password) + temp_user["password"] = new_password + + con = _login(user_name, new_password) + try: + assert con.get_user()["name"] == user_name + finally: + con.logout() + + with pytest.raises(AuthenticationError): + _login(user_name, "invalid_" + new_password) + + +def test_set_user_access_groups( + project_entity_fixture, temp_user, temp_access_group +): + """Set and remove access groups of a user on a project.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + + set_user_access_groups( + user_name, {project_name: [temp_access_group]} + ) + user = get_user(user_name) + assert user["data"]["accessGroups"][project_name] == [ + temp_access_group + ] + assert get_project_users(project_name)[user_name] == [ + temp_access_group + ] + + # Empty list removes access to the project + set_user_access_groups(user_name, {project_name: []}) + user = get_user(user_name) + assert project_name not in user["data"].get("accessGroups", {}) + assert user_name not in get_project_users(project_name) + + +def test_user_sessions(temp_user): + """Login as temp user, list its sessions and delete the session.""" + user_name = temp_user["name"] + con = _login(user_name, temp_user["password"]) + try: + token = con.access_token + sessions = get_user_sessions(user_name) + assert isinstance(sessions, list) + sessions_by_token = { + session["token"]: session + for session in sessions + } + assert token in sessions_by_token + session = sessions_by_token[token] + assert session["isService"] is False + assert isinstance(session["lastUsed"], (int, float)) + + delete_user_session(user_name, token) + tokens = { + session["token"] + for session in get_user_sessions(user_name) + } + assert token not in tokens + + # Deleting not existing session fails + with pytest.raises(HTTPRequestError): + delete_user_session(user_name, token) + finally: + # Session was already removed on server, avoid logout request + con.logout(soft=True) + + +@pytest.fixture +def temp_user_with_api_keys(): + """Create a temporary user with initialized 'apiKeys' in data. + + Server (1.16.8) api key endpoints fail with 500 if user does not have + 'apiKeys' set in data, because 'data->'apiKeys'' is NULL and is + iterated. The key is pre-filled on creation to work around it. + + Yields: + str: Name of the user. + + """ + user_name = _unique_name("ayonapitest") + create_user( + user_name, + password=_new_password(), + data={"apiKeys": []}, + ) + try: + yield user_name + finally: + _safe_delete_user(user_name) + + +def test_user_api_keys(temp_user_with_api_keys): + """Create, list, update and delete api key of a user.""" + user_name = temp_user_with_api_keys + assert get_user_api_keys(user_name) == [] + + key = uuid.uuid4().hex + label = _unique_name("label_") + expires = time.time() + 3600 + create_user_api_key(user_name, label, key, expires=expires) + + api_keys = get_user_api_keys(user_name) + assert len(api_keys) == 1 + api_key = api_keys[0] + assert api_key["label"] == label + assert api_key["expires"] == pytest.approx(expires) + assert api_key["preview"] == f"{key[:4]}*****{key[-4:]}" + assert isinstance(api_key["created"], (int, float)) + api_key_id = api_key["id"] + + # Label must be unique + with pytest.raises(HTTPRequestError): + create_user_api_key(user_name, label, uuid.uuid4().hex) + + new_label = _unique_name("label_") + new_expires = expires + 3600 + update_user_api_key( + user_name, api_key_id, label=new_label, expires=new_expires + ) + api_key = get_user_api_keys(user_name)[0] + assert api_key["id"] == api_key_id + assert api_key["label"] == new_label + assert api_key["expires"] == pytest.approx(new_expires) + + # Update only label, expiration stays unchanged + update_user_api_key(user_name, api_key_id, label=label) + api_key = get_user_api_keys(user_name)[0] + assert api_key["label"] == label + assert api_key["expires"] == pytest.approx(new_expires) + + delete_user_api_key(user_name, api_key_id) + assert get_user_api_keys(user_name) == [] + + +def test_get_user_permissions( + project_entity_fixture, temp_user, temp_access_group +): + """Get permissions of current user and of other user.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + + my_permissions = get_user_permissions() + assert my_permissions["user_level"] in ("admin", "manager") + + my_project_permissions = get_user_permissions( + project_name=project_name + ) + assert "read" in my_project_permissions + assert "project" in my_project_permissions + + studio_permissions = get_user_permissions(user_name) + assert "studio" in studio_permissions + + # User without access to the project can't have project permissions + with pytest.raises(HTTPRequestError): + get_user_permissions(user_name, project_name) + + set_user_access_groups( + user_name, {project_name: [temp_access_group]} + ) + try: + project_permissions = get_user_permissions(user_name, project_name) + assert "read" in project_permissions + finally: + set_user_access_groups(user_name, {project_name: []}) + + +# --- Access groups --- +def test_access_group_schema(project_entity_fixture): + """Access group schema is a settings schema with properties.""" + schema = get_access_group_schema() + assert isinstance(schema, dict) + assert "read" in schema["properties"] + + project_schema = get_access_group_schema( + project_entity_fixture["name"] + ) + assert "read" in project_schema["properties"] + + +def test_studio_access_group(): + """Create, update, read and delete studio level access group.""" + access_group_name = _unique_name("ayonapitest_ag_") + save_access_group(access_group_name, {"read": {"enabled": True}}) + try: + access_groups = { + access_group["name"]: access_group + for access_group in get_access_groups() + } + assert access_group_name in access_groups + assert access_groups[access_group_name]["isProjectLevel"] is False + + access_group = get_access_group(access_group_name) + assert access_group["read"]["enabled"] is True + + # Save existing access group updates it + save_access_group(access_group_name, {"read": {"enabled": False}}) + access_group = get_access_group(access_group_name) + assert access_group["read"]["enabled"] is False + + delete_access_group(access_group_name) + names = { + access_group["name"] + for access_group in get_access_groups() + } + assert access_group_name not in names + with pytest.raises(HTTPRequestError): + get_access_group(access_group_name) + finally: + names = { + access_group["name"] + for access_group in get_access_groups() + } + if access_group_name in names: + delete_access_group(access_group_name) + + +def test_project_access_group(project_entity_fixture, temp_access_group): + """Project level override of a studio access group.""" + project_name = project_entity_fixture["name"] + + access_groups = { + access_group["name"]: access_group + for access_group in get_access_groups(project_name) + } + assert access_groups[temp_access_group]["isProjectLevel"] is False + + save_access_group( + temp_access_group, + {"read": {"enabled": True}}, + project_name=project_name, + ) + try: + access_groups = { + access_group["name"]: access_group + for access_group in get_access_groups(project_name) + } + assert access_groups[temp_access_group]["isProjectLevel"] is True + + project_group = get_access_group(temp_access_group, project_name) + assert project_group["read"]["enabled"] is True + # Studio access group is not affected + studio_group = get_access_group(temp_access_group) + assert studio_group["read"]["enabled"] is False + finally: + delete_access_group(temp_access_group, project_name) + + access_groups = { + access_group["name"]: access_group + for access_group in get_access_groups(project_name) + } + assert access_groups[temp_access_group]["isProjectLevel"] is False + # Project access group falls back to studio data + project_group = get_access_group(temp_access_group, project_name) + assert project_group["read"]["enabled"] is False + + # Studio access group still exists + assert temp_access_group in { + access_group["name"] + for access_group in get_access_groups() + } + + +def test_set_projects_access( + project_entity_fixture, temp_user, temp_access_group +): + """Set and revoke project access of a user in bulk.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + + set_projects_access({user_name: {project_name: [temp_access_group]}}) + try: + assert get_project_users(project_name)[user_name] == [ + temp_access_group + ] + user = get_user(user_name) + assert user["data"]["accessGroups"][project_name] == [ + temp_access_group + ] + finally: + set_projects_access({user_name: {project_name: []}}) + + assert user_name not in get_project_users(project_name) + + +# --- Project users --- +def test_project_users( + project_entity_fixture, temp_user, temp_access_group +): + """Set access groups of a user on a project via project endpoint.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + + project_users = get_project_users(project_name) + assert isinstance(project_users, dict) + assert user_name not in project_users + + result = set_project_user_access_groups( + project_name, user_name, [temp_access_group] + ) + try: + assert result[user_name] == [temp_access_group] + assert get_project_users(project_name) == result + finally: + result = set_project_user_access_groups( + project_name, user_name, [] + ) + assert user_name not in result + assert user_name not in get_project_users(project_name) + + +# --- Teams --- +def test_teams(project_entity_fixture, temp_user): + """Create team, modify its members and delete it.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + team_name = _unique_name("team_") + + try: + save_team( + project_name, + team_name, + [{"name": user_name, "leader": True, "roles": ["lead"]}], + ) + team = _get_team(project_name, team_name) + assert team is not None + assert team["memberCount"] == 1 + assert team["members"] == [ + {"name": user_name, "leader": True, "roles": ["lead"]} + ] + assert [leader["name"] for leader in team["leaders"]] == [ + user_name + ] + + # Members are not returned without 'show_members' + team = _get_team(project_name, team_name, show_members=False) + assert team.get("members") is None + assert team["memberCount"] == 1 + + # Replace member + save_team_member( + project_name, + team_name, + user_name, + leader=False, + roles=["artist", "compositor"], + ) + team = _get_team(project_name, team_name) + assert team["memberCount"] == 1 + assert team["members"] == [ + { + "name": user_name, + "leader": False, + "roles": ["artist", "compositor"], + } + ] + assert team["leaders"] == [] + + delete_team_member(project_name, team_name, user_name) + team = _get_team(project_name, team_name) + assert team["memberCount"] == 0 + assert team["members"] == [] + + # Saving member to not existing team fails + with pytest.raises(HTTPRequestError): + save_team_member( + project_name, _unique_name("team_"), user_name + ) + + delete_team(project_name, team_name) + assert _get_team(project_name, team_name) is None + finally: + _safe_delete_team(project_name, team_name) + + +def test_update_teams(project_entity_fixture, temp_user): + """Create and replace multiple teams at once.""" + project_name = project_entity_fixture["name"] + user_name = temp_user["name"] + team_names = [_unique_name("team_"), _unique_name("team_")] + + try: + update_teams( + project_name, + [ + {"name": team_names[0], "members": []}, + { + "name": team_names[1], + "members": [ + {"name": user_name, "leader": False, "roles": []} + ], + }, + ], + ) + teams_by_name = { + team["name"]: team + for team in get_teams(project_name, show_members=True) + } + assert teams_by_name[team_names[0]]["memberCount"] == 0 + assert teams_by_name[team_names[1]]["memberCount"] == 1 + + # Replace only first team, second must stay unchanged + update_teams( + project_name, + [ + { + "name": team_names[0], + "members": [ + {"name": user_name, "leader": True, "roles": []} + ], + }, + ], + ) + teams_by_name = { + team["name"]: team + for team in get_teams(project_name, show_members=True) + } + first_team = teams_by_name[team_names[0]] + assert first_team["memberCount"] == 1 + assert first_team["members"][0]["leader"] is True + assert teams_by_name[team_names[1]]["memberCount"] == 1 + finally: + for team_name in team_names: + _safe_delete_team(project_name, team_name) + + names = {team["name"] for team in get_teams(project_name)} + assert not names & set(team_names) From cb949cd7ee371d6ff04166f1133f5cd5ca53efa1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:16:17 +0200 Subject: [PATCH 5/5] remove entity type validation In future more entity types might support thumbnails using the same endpoint format. It is also private method there is no reason validate it. --- ayon_api/_api_helpers/thumbnails.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index aa8a40854..13b2f1e8f 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -538,16 +538,6 @@ def set_project_thumbnail_from_stream( def _get_entity_thumbnail_endpoint( self, project_name: str, entity_type: str, entity_id: str ) -> str: - if entity_type not in ( - "folder", - "task", - "version", - "workfile", - ): - raise ValueError( - f"Entity type '{entity_type}' does not support thumbnails." - ) - return ( f"api/projects/{project_name}/{entity_type}s/{entity_id}/thumbnail" )