Skip to content

Commit eba04cd

Browse files
authored
Merge pull request #868 from superannotateai/FRIDAY-5282
upload_annotations support integration for mm
2 parents 8dfd598 + 655cab6 commit eba04cd

5 files changed

Lines changed: 108 additions & 2 deletions

File tree

src/superannotate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import sys
44

5-
__version__ = "4.5.6"
5+
__version__ = "4.5.7dev1"
66

77

88
os.environ.update({"sa_version": __version__})

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3702,6 +3702,7 @@ def upload_annotations(
37023702
keep_status: bool | None = None,
37033703
*,
37043704
data_spec: Literal["default", "multimodal"] = "default",
3705+
integration: str | None = None,
37053706
):
37063707
"""Uploads a list of annotation dictionaries to the specified SuperAnnotate project or folder.
37073708
@@ -3723,6 +3724,11 @@ def upload_annotations(
37233724
compact and modality-specific data representation.
37243725
:type data_spec: str, optional
37253726
3727+
:param integration: The name of an existing integration on the SuperAnnotate platform,
3728+
used to access external URLs in the annotations. Only supported for Multimodal projects
3729+
and only applies to items being newly created — it has no effect on existing items.
3730+
:type integration: str, optional
3731+
37263732
:return: A dictionary containing the results of the upload, categorized into successfully uploaded,
37273733
failed, and skipped annotations.
37283734
:rtype: dict
@@ -3759,6 +3765,17 @@ def upload_annotations(
37593765
keep_status=True,
37603766
data_spec='multimodal'
37613767
)
3768+
3769+
Example Usage with private URLs signed via an integration::
3770+
3771+
# Upload annotations with private URLs using integration
3772+
sa_client.upload_annotations(
3773+
project="project1/folder1",
3774+
annotations=annotations,
3775+
keep_status=True,
3776+
data_spec="multimodal",
3777+
integration="AWS Main Bucket"
3778+
)
37623779
"""
37633780
if keep_status is not None:
37643781
warnings.warn(
@@ -3768,13 +3785,26 @@ def upload_annotations(
37683785
)
37693786
)
37703787
project, folder = self.controller.get_project_folder(project)
3788+
integration_entity = None
3789+
if integration:
3790+
if data_spec != "multimodal" or project.type != ProjectType.MULTIMODAL:
3791+
raise AppException(
3792+
"Integration is only supported for Multimodal projects"
3793+
)
3794+
for i in self.controller.integrations.list().data:
3795+
if i.name == integration:
3796+
integration_entity = i
3797+
break
3798+
else:
3799+
raise AppException("Integration not found")
37713800
response = self.controller.annotations.upload_multiple(
37723801
project=project,
37733802
folder=folder,
37743803
annotations=annotations,
37753804
keep_status=keep_status,
37763805
user=self.controller.current_user,
37773806
output_format=data_spec,
3807+
integration=integration_entity,
37783808
)
37793809
if response.errors:
37803810
raise AppException(response.errors)

src/superannotate/lib/core/usecases/annotations.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from lib.core.entities import ConfigEntity
3131
from lib.core.entities import FolderEntity
3232
from lib.core.entities import ImageEntity
33+
from lib.core.entities import IntegrationEntity
3334
from lib.core.entities import ProjectEntity
3435
from lib.core.entities import UserEntity
3536
from lib.core.exceptions import AppException
@@ -1745,6 +1746,7 @@ def __init__(
17451746
user: UserEntity,
17461747
keep_status: bool = False,
17471748
transform_version: str = None,
1749+
integration: IntegrationEntity = None,
17481750
):
17491751
super().__init__(reporter)
17501752
self._project = project
@@ -1758,6 +1760,7 @@ def __init__(
17581760
self._transform_version = (
17591761
"llmJsonV2" if transform_version is None else transform_version
17601762
)
1763+
self._integration = integration
17611764
self._category_name_to_id_map = {}
17621765

17631766
@property
@@ -1895,7 +1898,12 @@ def attach_items(
18951898
project=self._project,
18961899
folder=folder,
18971900
attachments=[
1898-
AttachmentEntity(name=item_name, url="") for item_name in item_names
1901+
AttachmentEntity(
1902+
name=item_name,
1903+
url="",
1904+
integration_id=self._integration.id if self._integration else None,
1905+
)
1906+
for item_name in item_names
18991907
],
19001908
service_provider=self._service_provider,
19011909
).execute()

src/superannotate/lib/infrastructure/controller.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1432,6 +1432,7 @@ def upload_multiple(
14321432
keep_status: bool,
14331433
user: UserEntity,
14341434
output_format: str = None,
1435+
integration: IntegrationEntity = None,
14351436
):
14361437
if project.type == ProjectType.MULTIMODAL and output_format == "multimodal":
14371438
use_case = usecases.UploadMultiModalAnnotationsUseCase(
@@ -1443,6 +1444,7 @@ def upload_multiple(
14431444
keep_status=keep_status,
14441445
user=user,
14451446
transform_version="llmJsonV2",
1447+
integration=integration,
14461448
)
14471449
else:
14481450
use_case = usecases.UploadAnnotationsUseCase(

tests/integration/annotations/test_upload_annotations.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import json
23
import os
34
import tempfile
@@ -257,6 +258,71 @@ def test_upload_with_integer_names(self):
257258
f"{self.PROJECT_NAME}/test_folder", data_spec="multimodal"
258259
)
259260

261+
def test_integration_not_found(self):
262+
with open(self.JSONL_ANNOTATIONS_PATH) as f:
263+
data = [json.loads(line) for line in f]
264+
with self.assertRaisesRegex(AppException, "Integration not found"):
265+
sa.upload_annotations(
266+
self.PROJECT_NAME,
267+
annotations=data,
268+
data_spec="multimodal",
269+
integration="non-existing-integration-xyz",
270+
)
271+
272+
def test_integration_only_supported_for_multimodal_data_spec(self):
273+
with open(self.JSONL_ANNOTATIONS_PATH) as f:
274+
data = [json.loads(line) for line in f]
275+
with self.assertRaisesRegex(
276+
AppException, "Integration is only supported for Multimodal projects"
277+
):
278+
sa.upload_annotations(
279+
self.PROJECT_NAME,
280+
annotations=data,
281+
data_spec="default",
282+
integration="any-integration",
283+
)
284+
285+
def test_upload_with_existing_integration(self):
286+
integrations = sa.get_integrations()
287+
if not integrations:
288+
self.skipTest("No integrations available in the team.")
289+
integration = integrations[0]
290+
with open(self.JSONL_ANNOTATIONS_PATH) as f:
291+
data = [json.loads(line) for line in f]
292+
response = sa.upload_annotations(
293+
self.PROJECT_NAME,
294+
annotations=data,
295+
data_spec="multimodal",
296+
integration=integration["name"],
297+
)
298+
assert len(response["succeeded"]) == 3
299+
300+
# Newly created items must carry the integration id used to sign URLs.
301+
# The item metadata's integration_id isn't exposed by the SDK entities,
302+
# so query the backend directly for it.
303+
from lib.core.jsx_conditions import EmptyQuery
304+
from lib.core.jsx_conditions import Join
305+
306+
project, folder = sa.controller.get_project_folder(
307+
f"{self.PROJECT_NAME}/test_folder"
308+
)
309+
item_service = sa.controller.service_provider.item_service
310+
client = item_service.client
311+
entity_context = base64.b64encode(
312+
f'{{"team_id":{client.team_id},"project_id":{project.id},'
313+
f'"folder_id":{folder.id}}}'.encode()
314+
).decode()
315+
raw_items = client.jsx_paginate(
316+
url=item_service.URL_LIST,
317+
chunk_size=2000,
318+
body_query=EmptyQuery() & Join("metadata", ["path", "integration_id"]),
319+
method="post",
320+
headers={"x-sa-entity-context": entity_context},
321+
).data
322+
assert len(raw_items) == 3
323+
for item in raw_items:
324+
assert item["metadata"]["integration_id"] == integration["id"]
325+
260326
def test_download_annotations(self):
261327
with open(self.JSONL_ANNOTATIONS_PATH) as f:
262328
data = [json.loads(line) for line in f]

0 commit comments

Comments
 (0)