Skip to content

Commit c4ac665

Browse files
authored
Merge pull request #875 from superannotateai/FRIDAY-5282
add integration for MM in upload_annotations
2 parents f365653 + d94e654 commit c4ac665

4 files changed

Lines changed: 106 additions & 1 deletion

File tree

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

Lines changed: 29 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,10 @@ 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, used to access external URLs in the annotations. Only supported for
3728+
Multimodal projects and data_spec="multimodal" and only applies to items being newly created — it has no effect on existing items.
3729+
:type integration: str, optional
3730+
37263731
:return: A dictionary containing the results of the upload, categorized into successfully uploaded,
37273732
failed, and skipped annotations.
37283733
:rtype: dict
@@ -3759,6 +3764,17 @@ def upload_annotations(
37593764
keep_status=True,
37603765
data_spec='multimodal'
37613766
)
3767+
3768+
Example Usage with private URLs signed via an integration::
3769+
3770+
# Upload annotations with private URLs using integration
3771+
sa_client.upload_annotations(
3772+
project="project1/folder1",
3773+
annotations=annotations,
3774+
keep_status=True,
3775+
data_spec="multimodal",
3776+
integration="AWS Main Bucket"
3777+
)
37623778
"""
37633779
if keep_status is not None:
37643780
warnings.warn(
@@ -3768,13 +3784,26 @@ def upload_annotations(
37683784
)
37693785
)
37703786
project, folder = self.controller.get_project_folder(project)
3787+
integration_entity = None
3788+
if integration:
3789+
if data_spec != "multimodal" or project.type != ProjectType.MULTIMODAL:
3790+
raise AppException(
3791+
"Integration is only supported for Multimodal projects"
3792+
)
3793+
for i in self.controller.integrations.list().data:
3794+
if i.name == integration:
3795+
integration_entity = i
3796+
break
3797+
else:
3798+
raise AppException("Integration not found")
37713799
response = self.controller.annotations.upload_multiple(
37723800
project=project,
37733801
folder=folder,
37743802
annotations=annotations,
37753803
keep_status=keep_status,
37763804
user=self.controller.current_user,
37773805
output_format=data_spec,
3806+
integration=integration_entity,
37783807
)
37793808
if response.errors:
37803809
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="custom_llm", # hardcoded for multimodal items
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)