From 5a13e676982f650fdcff52c60389535f5c469fa9 Mon Sep 17 00:00:00 2001 From: Narek Mkhitaryan Date: Mon, 6 Jul 2026 17:34:36 +0400 Subject: [PATCH 1/4] upload_annotations support integration for mm --- src/superannotate/__init__.py | 2 +- .../lib/app/interface/sdk_interface.py | 30 +++++++++ .../lib/core/usecases/annotations.py | 10 ++- .../lib/infrastructure/controller.py | 2 + .../annotations/test_upload_annotations.py | 66 +++++++++++++++++++ 5 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index bc903a88..001bd1f4 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -2,7 +2,7 @@ import os import sys -__version__ = "4.5.6dev1" +__version__ = "4.5.7dev1" os.environ.update({"sa_version": __version__}) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index ee940303..a0a76c3b 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -3702,6 +3702,7 @@ def upload_annotations( keep_status: bool | None = None, *, data_spec: Literal["default", "multimodal"] = "default", + integration: str | None = None, ): """Uploads a list of annotation dictionaries to the specified SuperAnnotate project or folder. @@ -3723,6 +3724,11 @@ def upload_annotations( compact and modality-specific data representation. :type data_spec: str, optional + :param integration: The name of an existing integration on the SuperAnnotate platform, + used to access external URLs in the annotations. Only supported for Multimodal projects + and only applies to items being newly created — it has no effect on existing items. + :type integration: str, optional + :return: A dictionary containing the results of the upload, categorized into successfully uploaded, failed, and skipped annotations. :rtype: dict @@ -3759,6 +3765,17 @@ def upload_annotations( keep_status=True, data_spec='multimodal' ) + + Example Usage with private URLs signed via an integration:: + + # Upload annotations with private URLs using integration + sa_client.upload_annotations( + project="project1/folder1", + annotations=annotations, + keep_status=True, + data_spec="multimodal", + integration="AWS Main Bucket" + ) """ if keep_status is not None: warnings.warn( @@ -3768,6 +3785,18 @@ def upload_annotations( ) ) project, folder = self.controller.get_project_folder(project) + integration_entity = None + if integration: + if data_spec != "multimodal" or project.type != ProjectType.MULTIMODAL: + raise AppException( + "Integration is only supported for Multimodal projects" + ) + for i in self.controller.integrations.list().data: + if i.name == integration: + integration_entity = i + break + else: + raise AppException("Integration not found") response = self.controller.annotations.upload_multiple( project=project, folder=folder, @@ -3775,6 +3804,7 @@ def upload_annotations( keep_status=keep_status, user=self.controller.current_user, output_format=data_spec, + integration=integration_entity, ) if response.errors: raise AppException(response.errors) diff --git a/src/superannotate/lib/core/usecases/annotations.py b/src/superannotate/lib/core/usecases/annotations.py index 3b5d6f48..fd4ab841 100644 --- a/src/superannotate/lib/core/usecases/annotations.py +++ b/src/superannotate/lib/core/usecases/annotations.py @@ -30,6 +30,7 @@ from lib.core.entities import ConfigEntity from lib.core.entities import FolderEntity from lib.core.entities import ImageEntity +from lib.core.entities import IntegrationEntity from lib.core.entities import ProjectEntity from lib.core.entities import UserEntity from lib.core.exceptions import AppException @@ -1745,6 +1746,7 @@ def __init__( user: UserEntity, keep_status: bool = False, transform_version: str = None, + integration: IntegrationEntity = None, ): super().__init__(reporter) self._project = project @@ -1758,6 +1760,7 @@ def __init__( self._transform_version = ( "llmJsonV2" if transform_version is None else transform_version ) + self._integration = integration self._category_name_to_id_map = {} @property @@ -1895,7 +1898,12 @@ def attach_items( project=self._project, folder=folder, attachments=[ - AttachmentEntity(name=item_name, url="") for item_name in item_names + AttachmentEntity( + name=item_name, + url="", + integration_id=self._integration.id if self._integration else None, + ) + for item_name in item_names ], service_provider=self._service_provider, ).execute() diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index c14cd69e..d55673fc 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -1432,6 +1432,7 @@ def upload_multiple( keep_status: bool, user: UserEntity, output_format: str = None, + integration: IntegrationEntity = None, ): if project.type == ProjectType.MULTIMODAL and output_format == "multimodal": use_case = usecases.UploadMultiModalAnnotationsUseCase( @@ -1443,6 +1444,7 @@ def upload_multiple( keep_status=keep_status, user=user, transform_version="llmJsonV2", + integration=integration, ) else: use_case = usecases.UploadAnnotationsUseCase( diff --git a/tests/integration/annotations/test_upload_annotations.py b/tests/integration/annotations/test_upload_annotations.py index a86e62e8..59b842af 100644 --- a/tests/integration/annotations/test_upload_annotations.py +++ b/tests/integration/annotations/test_upload_annotations.py @@ -1,3 +1,4 @@ +import base64 import json import os import tempfile @@ -257,6 +258,71 @@ def test_upload_with_integer_names(self): f"{self.PROJECT_NAME}/test_folder", data_spec="multimodal" ) + def test_integration_not_found(self): + with open(self.JSONL_ANNOTATIONS_PATH) as f: + data = [json.loads(line) for line in f] + with self.assertRaisesRegex(AppException, "Integration not found"): + sa.upload_annotations( + self.PROJECT_NAME, + annotations=data, + data_spec="multimodal", + integration="non-existing-integration-xyz", + ) + + def test_integration_only_supported_for_multimodal_data_spec(self): + with open(self.JSONL_ANNOTATIONS_PATH) as f: + data = [json.loads(line) for line in f] + with self.assertRaisesRegex( + AppException, "Integration is only supported for Multimodal projects" + ): + sa.upload_annotations( + self.PROJECT_NAME, + annotations=data, + data_spec="default", + integration="any-integration", + ) + + def test_upload_with_existing_integration(self): + integrations = sa.get_integrations() + if not integrations: + self.skipTest("No integrations available in the team.") + integration = integrations[0] + with open(self.JSONL_ANNOTATIONS_PATH) as f: + data = [json.loads(line) for line in f] + response = sa.upload_annotations( + self.PROJECT_NAME, + annotations=data, + data_spec="multimodal", + integration=integration["name"], + ) + assert len(response["succeeded"]) == 3 + + # Newly created items must carry the integration id used to sign URLs. + # The item metadata's integration_id isn't exposed by the SDK entities, + # so query the backend directly for it. + from lib.core.jsx_conditions import EmptyQuery + from lib.core.jsx_conditions import Join + + project, folder = sa.controller.get_project_folder( + f"{self.PROJECT_NAME}/test_folder" + ) + item_service = sa.controller.service_provider.item_service + client = item_service.client + entity_context = base64.b64encode( + f'{{"team_id":{client.team_id},"project_id":{project.id},' + f'"folder_id":{folder.id}}}'.encode() + ).decode() + raw_items = client.jsx_paginate( + url=item_service.URL_LIST, + chunk_size=2000, + body_query=EmptyQuery() & Join("metadata", ["path", "integration_id"]), + method="post", + headers={"x-sa-entity-context": entity_context}, + ).data + assert len(raw_items) == 3 + for item in raw_items: + assert item["metadata"]["integration_id"] == integration["id"] + def test_download_annotations(self): with open(self.JSONL_ANNOTATIONS_PATH) as f: data = [json.loads(line) for line in f] From db0a63d885111fd52fb2de94ed476b41ec53a4e4 Mon Sep 17 00:00:00 2001 From: Narek Mkhitaryan Date: Tue, 7 Jul 2026 12:20:10 +0400 Subject: [PATCH 2/4] add 200 name limit for generate_items --- src/superannotate/lib/core/usecases/items.py | 3 ++- tests/integration/items/test_generate_items.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/superannotate/lib/core/usecases/items.py b/src/superannotate/lib/core/usecases/items.py index 2b9cf3b6..744867c8 100644 --- a/src/superannotate/lib/core/usecases/items.py +++ b/src/superannotate/lib/core/usecases/items.py @@ -425,6 +425,7 @@ def execute(self) -> Response: class GenerateItems(BaseReportableUseCase): CHUNK_SIZE = 500 + ITEM_NAME_PREFIX_LIMIT = 194 INVALID_CHARS_PATTERN = re.compile(r"[<>:\"'/\\|?*&$!+]") def __init__( @@ -445,7 +446,7 @@ def __init__( def validate_name(self): if ( - len(self._name_prefix) > 114 + len(self._name_prefix) > self.ITEM_NAME_PREFIX_LIMIT or self.INVALID_CHARS_PATTERN.search(self._name_prefix) is not None ): raise AppException("Invalid item name.") diff --git a/tests/integration/items/test_generate_items.py b/tests/integration/items/test_generate_items.py index 352c80e8..03a800b0 100644 --- a/tests/integration/items/test_generate_items.py +++ b/tests/integration/items/test_generate_items.py @@ -85,6 +85,22 @@ def test_invalid_name(self): ): sa.generate_items(self.PROJECT_NAME, 100, name="m<:") + def test_long_item_names(self): + # Max allowed prefix is 114 chars; with the "_00001" suffix the full + # item name is 120 chars, which is under the backend limit of 200 chars, + # so names must be stored in full without being truncated. + name = "a" * 194 + sa.generate_items(self.PROJECT_NAME, 100, name=name) + items = sa.list_items(self.PROJECT_NAME) + + assert len(items) == 100 + + expected_names = {f"{name}_{i:05d}" for i in range(1, 101)} + actual_names = {item["name"] for item in items} + + assert actual_names == expected_names + assert all(len(n) == 200 for n in actual_names) + def test_item_count(self): with self.assertRaisesRegex( AppException, From ae1b4705f1b47d50470e93599f024446b3e233ab Mon Sep 17 00:00:00 2001 From: Narek Mkhitaryan Date: Fri, 10 Jul 2026 15:43:32 +0400 Subject: [PATCH 3/4] Revert "upload_annotations support integration for mm" This reverts commit 5a13e676982f650fdcff52c60389535f5c469fa9. --- src/superannotate/__init__.py | 2 +- .../lib/app/interface/sdk_interface.py | 30 --------- .../lib/core/usecases/annotations.py | 10 +-- .../lib/infrastructure/controller.py | 2 - .../annotations/test_upload_annotations.py | 66 ------------------- 5 files changed, 2 insertions(+), 108 deletions(-) diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index 001bd1f4..bc903a88 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -2,7 +2,7 @@ import os import sys -__version__ = "4.5.7dev1" +__version__ = "4.5.6dev1" os.environ.update({"sa_version": __version__}) diff --git a/src/superannotate/lib/app/interface/sdk_interface.py b/src/superannotate/lib/app/interface/sdk_interface.py index a0a76c3b..ee940303 100644 --- a/src/superannotate/lib/app/interface/sdk_interface.py +++ b/src/superannotate/lib/app/interface/sdk_interface.py @@ -3702,7 +3702,6 @@ def upload_annotations( keep_status: bool | None = None, *, data_spec: Literal["default", "multimodal"] = "default", - integration: str | None = None, ): """Uploads a list of annotation dictionaries to the specified SuperAnnotate project or folder. @@ -3724,11 +3723,6 @@ def upload_annotations( compact and modality-specific data representation. :type data_spec: str, optional - :param integration: The name of an existing integration on the SuperAnnotate platform, - used to access external URLs in the annotations. Only supported for Multimodal projects - and only applies to items being newly created — it has no effect on existing items. - :type integration: str, optional - :return: A dictionary containing the results of the upload, categorized into successfully uploaded, failed, and skipped annotations. :rtype: dict @@ -3765,17 +3759,6 @@ def upload_annotations( keep_status=True, data_spec='multimodal' ) - - Example Usage with private URLs signed via an integration:: - - # Upload annotations with private URLs using integration - sa_client.upload_annotations( - project="project1/folder1", - annotations=annotations, - keep_status=True, - data_spec="multimodal", - integration="AWS Main Bucket" - ) """ if keep_status is not None: warnings.warn( @@ -3785,18 +3768,6 @@ def upload_annotations( ) ) project, folder = self.controller.get_project_folder(project) - integration_entity = None - if integration: - if data_spec != "multimodal" or project.type != ProjectType.MULTIMODAL: - raise AppException( - "Integration is only supported for Multimodal projects" - ) - for i in self.controller.integrations.list().data: - if i.name == integration: - integration_entity = i - break - else: - raise AppException("Integration not found") response = self.controller.annotations.upload_multiple( project=project, folder=folder, @@ -3804,7 +3775,6 @@ def upload_annotations( keep_status=keep_status, user=self.controller.current_user, output_format=data_spec, - integration=integration_entity, ) if response.errors: raise AppException(response.errors) diff --git a/src/superannotate/lib/core/usecases/annotations.py b/src/superannotate/lib/core/usecases/annotations.py index fd4ab841..3b5d6f48 100644 --- a/src/superannotate/lib/core/usecases/annotations.py +++ b/src/superannotate/lib/core/usecases/annotations.py @@ -30,7 +30,6 @@ from lib.core.entities import ConfigEntity from lib.core.entities import FolderEntity from lib.core.entities import ImageEntity -from lib.core.entities import IntegrationEntity from lib.core.entities import ProjectEntity from lib.core.entities import UserEntity from lib.core.exceptions import AppException @@ -1746,7 +1745,6 @@ def __init__( user: UserEntity, keep_status: bool = False, transform_version: str = None, - integration: IntegrationEntity = None, ): super().__init__(reporter) self._project = project @@ -1760,7 +1758,6 @@ def __init__( self._transform_version = ( "llmJsonV2" if transform_version is None else transform_version ) - self._integration = integration self._category_name_to_id_map = {} @property @@ -1898,12 +1895,7 @@ def attach_items( project=self._project, folder=folder, attachments=[ - AttachmentEntity( - name=item_name, - url="", - integration_id=self._integration.id if self._integration else None, - ) - for item_name in item_names + AttachmentEntity(name=item_name, url="") for item_name in item_names ], service_provider=self._service_provider, ).execute() diff --git a/src/superannotate/lib/infrastructure/controller.py b/src/superannotate/lib/infrastructure/controller.py index d55673fc..c14cd69e 100644 --- a/src/superannotate/lib/infrastructure/controller.py +++ b/src/superannotate/lib/infrastructure/controller.py @@ -1432,7 +1432,6 @@ def upload_multiple( keep_status: bool, user: UserEntity, output_format: str = None, - integration: IntegrationEntity = None, ): if project.type == ProjectType.MULTIMODAL and output_format == "multimodal": use_case = usecases.UploadMultiModalAnnotationsUseCase( @@ -1444,7 +1443,6 @@ def upload_multiple( keep_status=keep_status, user=user, transform_version="llmJsonV2", - integration=integration, ) else: use_case = usecases.UploadAnnotationsUseCase( diff --git a/tests/integration/annotations/test_upload_annotations.py b/tests/integration/annotations/test_upload_annotations.py index 59b842af..a86e62e8 100644 --- a/tests/integration/annotations/test_upload_annotations.py +++ b/tests/integration/annotations/test_upload_annotations.py @@ -1,4 +1,3 @@ -import base64 import json import os import tempfile @@ -258,71 +257,6 @@ def test_upload_with_integer_names(self): f"{self.PROJECT_NAME}/test_folder", data_spec="multimodal" ) - def test_integration_not_found(self): - with open(self.JSONL_ANNOTATIONS_PATH) as f: - data = [json.loads(line) for line in f] - with self.assertRaisesRegex(AppException, "Integration not found"): - sa.upload_annotations( - self.PROJECT_NAME, - annotations=data, - data_spec="multimodal", - integration="non-existing-integration-xyz", - ) - - def test_integration_only_supported_for_multimodal_data_spec(self): - with open(self.JSONL_ANNOTATIONS_PATH) as f: - data = [json.loads(line) for line in f] - with self.assertRaisesRegex( - AppException, "Integration is only supported for Multimodal projects" - ): - sa.upload_annotations( - self.PROJECT_NAME, - annotations=data, - data_spec="default", - integration="any-integration", - ) - - def test_upload_with_existing_integration(self): - integrations = sa.get_integrations() - if not integrations: - self.skipTest("No integrations available in the team.") - integration = integrations[0] - with open(self.JSONL_ANNOTATIONS_PATH) as f: - data = [json.loads(line) for line in f] - response = sa.upload_annotations( - self.PROJECT_NAME, - annotations=data, - data_spec="multimodal", - integration=integration["name"], - ) - assert len(response["succeeded"]) == 3 - - # Newly created items must carry the integration id used to sign URLs. - # The item metadata's integration_id isn't exposed by the SDK entities, - # so query the backend directly for it. - from lib.core.jsx_conditions import EmptyQuery - from lib.core.jsx_conditions import Join - - project, folder = sa.controller.get_project_folder( - f"{self.PROJECT_NAME}/test_folder" - ) - item_service = sa.controller.service_provider.item_service - client = item_service.client - entity_context = base64.b64encode( - f'{{"team_id":{client.team_id},"project_id":{project.id},' - f'"folder_id":{folder.id}}}'.encode() - ).decode() - raw_items = client.jsx_paginate( - url=item_service.URL_LIST, - chunk_size=2000, - body_query=EmptyQuery() & Join("metadata", ["path", "integration_id"]), - method="post", - headers={"x-sa-entity-context": entity_context}, - ).data - assert len(raw_items) == 3 - for item in raw_items: - assert item["metadata"]["integration_id"] == integration["id"] - def test_download_annotations(self): with open(self.JSONL_ANNOTATIONS_PATH) as f: data = [json.loads(line) for line in f] From 44628449884eac032de4be6357e5e616b2f0c34c Mon Sep 17 00:00:00 2001 From: Narek Mkhitaryan Date: Fri, 10 Jul 2026 15:52:13 +0400 Subject: [PATCH 4/4] revert integretion support in upload_annotations --- CHANGELOG.rst | 9 +++++++++ src/superannotate/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 58d797bc..25cff0b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,15 @@ History All release highlights of this project will be documented in this file. + +4.5.7 - July 12, 2026 +______________________ + +**Updated** + + - ``SAClient.generate_items()`` The name key now supports values with up to 200 characters. + + 4.5.6 - July 5, 2026 ______________________ diff --git a/src/superannotate/__init__.py b/src/superannotate/__init__.py index bc903a88..001bd1f4 100644 --- a/src/superannotate/__init__.py +++ b/src/superannotate/__init__.py @@ -2,7 +2,7 @@ import os import sys -__version__ = "4.5.6dev1" +__version__ = "4.5.7dev1" os.environ.update({"sa_version": __version__})