From 045fdce481b32a97954fc39312c16e82e008574d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:49:16 +0200 Subject: [PATCH 1/6] added bulk creation of links --- ayon_api/__init__.py | 2 + ayon_api/_api.py | 37 +++++++++++++++- ayon_api/_api_helpers/links.py | 80 +++++++++++++++++++++++++++++++++- ayon_api/typing.py | 10 ++++- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 2f04c2800..aef55d7e4 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -275,6 +275,7 @@ delete_link_type, make_sure_link_type_exists, create_link, + create_links, delete_link, get_entities_links, get_folders_links, @@ -594,6 +595,7 @@ "delete_link_type", "make_sure_link_type_exists", "create_link", + "create_links", "delete_link", "get_entities_links", "get_folders_links", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index b0c6e02dd..08ed230ff 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -53,6 +53,7 @@ BackgroundOperationTask, LinkDirection, CreateLinkData, + CreateLinkResponseData, EventFilter, EventStatus, EnrollEventData, @@ -7504,7 +7505,7 @@ def create_link( output_type: str, link_name: Optional[str] = None, data: Optional[dict[str, Any]] = None, -) -> CreateLinkData: +) -> CreateLinkResponseData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -7527,7 +7528,7 @@ def create_link( with the link. Returns: - CreateLinkData: Information about link. + CreateLinkResponseData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -7546,6 +7547,38 @@ def create_link( ) +def create_links( + project_name: str, + links: list[dict[str, Any]], +) -> None: + """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[dict[str, Any]]): List of link data. + + Raises: + ValueError: Link data is invalid. + + """ + con = get_server_api_connection() + return con.create_links( + project_name=project_name, + links=links, + ) + + def delete_link( project_name: str, link_id: str, diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index a50b21817..a83e1f44d 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -200,7 +200,7 @@ def create_link( output_type: str, link_name: Optional[str] = None, data: Optional[dict[str, Any]] = None, - ) -> CreateLinkData: + ) -> CreateLinkResponseData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -223,7 +223,7 @@ def create_link( with the link. Returns: - CreateLinkData: Information about link. + CreateLinkResponseData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -249,6 +249,59 @@ def create_link( response.raise_for_status() return response.data + def create_links( + self, + project_name: str, + links: list[dict[str, Any]], + ) -> None: + """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[dict[str, Any]]): List of link data. + + Raises: + ValueError: Link data is invalid. + + """ + if not links: + return + + for link in links: + self._validate_link_data(link) + + if self.get_server_version_tuple() < (1, 15, 8): + for link in links: + link_type, in_type, out_type = link["linkType"].split("|") + self.create_link( + project_name, + link_type, + link["input"], + in_type, + link["output"], + out_type, + link_name=link.get("name") or None, + data=link.get("data") or None, + ) + return + + response = self.post( + f"projects/{project_name}/links/bulk", + links=links + ) + response.raise_for_status() + def delete_link(self, project_name: str, link_id: str) -> None: """Remove link by id. @@ -619,6 +672,29 @@ def get_representation_links( project_name, [representation_id], link_types, link_direction )[representation_id] + def _validate_link_data(self, link_data: dict[str, Any]) -> None: + """Validate link data before sending to server. + + Args: + link_data (dict[str, Any]): Link data to validate. + + Raises: + ValueError: Link data is invalid. + + """ + required_keys = {"input", "output", "linkType"} + missing_keys = required_keys - link_data.keys() + if missing_keys: + mk = ", ".join((f"'{key}'" for key in missing_keys)) + raise ValueError(f"Missing required keys in link data {mk}") + + link_type_parts = link_data["linkType"].split("|") + if len(link_type_parts) != 3: + raise ValueError( + f"Invalid linkType format: {link_data['linkType']}. " + "Expected format: 'link_type|input_type|output_type'" + ) + def _prepare_link_filters( self, filters: dict[str, Any], diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 37bb92f51..368d522ca 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -127,10 +127,18 @@ class BackgroundOperationTask(TypedDict): LinkDirection = Literal["in", "out"] -class CreateLinkData(TypedDict): +class CreateLinkResponseData(TypedDict): id: str +class CreateLinkData(TypedDict): + input: str + output: str + linkType: str + data: dict[str, Any] | None = None + name: str | None = None + + class AttributeEnumItemDict(TypedDict): value: str | int | float | bool label: str From a7119826a8a4ef38a7ad083339ec2b7fc0608f95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:59:20 +0200 Subject: [PATCH 2/6] use 'CreateLinkData' --- ayon_api/_api.py | 4 ++-- ayon_api/_api_helpers/links.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 159a0f03f..dfa178e88 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7549,7 +7549,7 @@ def create_link( def create_links( project_name: str, - links: list[dict[str, Any]], + links: list[CreateLinkData], ) -> None: """Create multiple links in a single request. @@ -7566,7 +7566,7 @@ def create_links( Args: project_name (str): Project where links are created. - links (list[dict[str, Any]]): List of link data. + links (list[CreateLinkData]): List of link data. Raises: ValueError: Link data is invalid. diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index a83e1f44d..5cdec5d99 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -252,7 +252,7 @@ def create_link( def create_links( self, project_name: str, - links: list[dict[str, Any]], + links: list[CreateLinkData], ) -> None: """Create multiple links in a single request. @@ -269,7 +269,7 @@ def create_links( Args: project_name (str): Project where links are created. - links (list[dict[str, Any]]): List of link data. + links (list[CreateLinkData]): List of link data. Raises: ValueError: Link data is invalid. From 139a3d0b519464899e6c33f31bc71db50eb596f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:01:32 +0200 Subject: [PATCH 3/6] add missing import --- ayon_api/_api_helpers/links.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 5cdec5d99..913ad8ab4 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -15,7 +15,11 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from ayon_api.typing import LinkDirection, CreateLinkData + from ayon_api.typing import ( + LinkDirection, + CreateLinkData, + CreateLinkResponseData, + ) class LinksAPI(BaseServerAPI): From 13aeccb27598fdde004ca466ecd85e3f0dedd2a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:52:23 +0200 Subject: [PATCH 4/6] use 'NotRequired' --- ayon_api/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 368d522ca..00155dcc5 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -135,8 +135,8 @@ class CreateLinkData(TypedDict): input: str output: str linkType: str - data: dict[str, Any] | None = None - name: str | None = None + data: NotRequired[dict[str, Any] | None] + name: NotRequired[str | None] class AttributeEnumItemDict(TypedDict): From a643361fb5ce83031514782513ae5e436669b59e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:53:32 +0200 Subject: [PATCH 5/6] better error message --- ayon_api/_api_helpers/links.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 913ad8ab4..ea42db170 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -689,8 +689,8 @@ def _validate_link_data(self, link_data: dict[str, Any]) -> None: required_keys = {"input", "output", "linkType"} missing_keys = required_keys - link_data.keys() if missing_keys: - mk = ", ".join((f"'{key}'" for key in missing_keys)) - raise ValueError(f"Missing required keys in link data {mk}") + mk = ", ".join(f"'{key}'" for key in sorted(missing_keys)) + raise ValueError(f"Missing required keys in link data: {mk}") link_type_parts = link_data["linkType"].split("|") if len(link_type_parts) != 3: From 9943761dcc02a1f5d1d3074c093ba0a9751ce139 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:54:35 +0200 Subject: [PATCH 6/6] better validation --- ayon_api/_api_helpers/links.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index ea42db170..5dd6136fc 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -692,11 +692,17 @@ def _validate_link_data(self, link_data: dict[str, Any]) -> None: mk = ", ".join(f"'{key}'" for key in sorted(missing_keys)) raise ValueError(f"Missing required keys in link data: {mk}") - link_type_parts = link_data["linkType"].split("|") + link_type = link_data["linkType"] + if not isinstance(link_type, str): + raise ValueError( + f"Invalid linkType type: {type(link_type)}. Expected 'str'" + ) + + link_type_parts = link_type.split("|") if len(link_type_parts) != 3: raise ValueError( - f"Invalid linkType format: {link_data['linkType']}. " - "Expected format: 'link_type|input_type|output_type'" + f"Invalid linkType format: {link_type}. Expected format:" + " 'link_type|input_type|output_type'" ) def _prepare_link_filters(