Skip to content

Commit 8e87f6d

Browse files
authored
Merge pull request #432 from superannotateai/attach_items
Attach items
2 parents 9ab3bcd + 916614b commit 8e87f6d

File tree

19 files changed

+1131
-246
lines changed

19 files changed

+1131
-246
lines changed

docs/source/superannotate.sdk.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ ______
7474

7575
.. autofunction:: superannotate.query
7676
.. autofunction:: superannotate.search_items
77+
.. autofunction:: superannotate.attach_items
78+
.. autofunction:: superannotate.copy_items
79+
.. autofunction:: superannotate.move_items
7780
.. autofunction:: superannotate.get_item_metadata
7881

7982
----------

src/superannotate/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
attach_document_urls_to_project,
2626
)
2727
from superannotate.lib.app.interface.sdk_interface import attach_image_urls_to_project
28+
from superannotate.lib.app.interface.sdk_interface import attach_items
2829
from superannotate.lib.app.interface.sdk_interface import (
2930
attach_items_from_integrated_storage,
3031
)
@@ -34,6 +35,7 @@
3435
from superannotate.lib.app.interface.sdk_interface import consensus
3536
from superannotate.lib.app.interface.sdk_interface import copy_image
3637
from superannotate.lib.app.interface.sdk_interface import copy_images
38+
from superannotate.lib.app.interface.sdk_interface import copy_items
3739
from superannotate.lib.app.interface.sdk_interface import create_annotation_class
3840
from superannotate.lib.app.interface.sdk_interface import (
3941
create_annotation_classes_from_classes_json,
@@ -71,6 +73,7 @@
7173
from superannotate.lib.app.interface.sdk_interface import init
7274
from superannotate.lib.app.interface.sdk_interface import invite_contributors_to_team
7375
from superannotate.lib.app.interface.sdk_interface import move_images
76+
from superannotate.lib.app.interface.sdk_interface import move_items
7477
from superannotate.lib.app.interface.sdk_interface import pin_image
7578
from superannotate.lib.app.interface.sdk_interface import prepare_export
7679
from superannotate.lib.app.interface.sdk_interface import query
@@ -175,6 +178,9 @@
175178
"get_item_metadata",
176179
"search_items",
177180
"query",
181+
"attach_items",
182+
"copy_items",
183+
"move_items",
178184
# Image Section
179185
"copy_images",
180186
"move_images",

src/superannotate/lib/app/helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import boto3
1010
import pandas as pd
11+
from superannotate.lib.app.exceptions import AppException
1112
from superannotate.lib.app.exceptions import PathError
1213
from superannotate.lib.core import ATTACHED_VIDEO_ANNOTATION_POSTFIX
1314
from superannotate.lib.core import PIXEL_ANNOTATION_POSTFIX
@@ -168,3 +169,33 @@ def get_paths_and_duplicated_from_csv(csv_path):
168169
else:
169170
duplicate_images.append(temp)
170171
return images_to_upload, duplicate_images
172+
173+
174+
def get_name_url_duplicated_from_csv(csv_path):
175+
image_data = pd.read_csv(csv_path, dtype=str)
176+
if "url" not in image_data.columns:
177+
raise AppException("Column 'url' is required")
178+
image_data = image_data[~image_data["url"].isnull()]
179+
if "name" in image_data.columns:
180+
image_data["name"] = (
181+
image_data["name"]
182+
.fillna("")
183+
.apply(lambda cell: cell if str(cell).strip() else str(uuid.uuid4()))
184+
)
185+
else:
186+
image_data["name"] = [str(uuid.uuid4()) for _ in range(len(image_data.index))]
187+
188+
image_data = pd.DataFrame(image_data, columns=["name", "url"])
189+
img_names_urls = image_data.to_dict(orient="records")
190+
duplicate_images = []
191+
seen = []
192+
images_to_upload = []
193+
for i in img_names_urls:
194+
temp = i["name"]
195+
i["name"] = i["name"].strip()
196+
if i["name"] not in seen:
197+
seen.append(i["name"])
198+
images_to_upload.append(i)
199+
else:
200+
duplicate_images.append(temp)
201+
return images_to_upload, duplicate_images

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

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections
12
import io
23
import json
34
import os
@@ -17,10 +18,13 @@
1718
from lib.app.annotation_helpers import add_annotation_point_to_json
1819
from lib.app.helpers import extract_project_folder
1920
from lib.app.helpers import get_annotation_paths
21+
from lib.app.helpers import get_name_url_duplicated_from_csv
2022
from lib.app.helpers import get_paths_and_duplicated_from_csv
2123
from lib.app.interface.types import AnnotationStatuses
2224
from lib.app.interface.types import AnnotationType
2325
from lib.app.interface.types import AnnotatorRole
26+
from lib.app.interface.types import AttachmentArg
27+
from lib.app.interface.types import AttachmentDict
2428
from lib.app.interface.types import ClassType
2529
from lib.app.interface.types import EmailStr
2630
from lib.app.interface.types import ImageQualityChoices
@@ -36,6 +40,7 @@
3640
from lib.app.serializers import SettingsSerializer
3741
from lib.app.serializers import TeamSerializer
3842
from lib.core import LIMITED_FUNCTIONS
43+
from lib.core.entities import AttachmentEntity
3944
from lib.core.entities.integrations import IntegrationEntity
4045
from lib.core.entities.project_entities import AnnotationClassEntity
4146
from lib.core.enums import ImageQuality
@@ -588,15 +593,20 @@ def copy_images(
588593
:return: list of skipped image names
589594
:rtype: list of strs
590595
"""
591-
596+
warning_msg = (
597+
"We're deprecating the copy_images function. Please use copy_items instead. Learn more. \n"
598+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.copy_items"
599+
)
600+
logger.warning(warning_msg)
601+
warnings.warn(warning_msg, DeprecationWarning)
592602
project_name, source_folder_name = extract_project_folder(source_project)
593603

594604
to_project_name, destination_folder_name = extract_project_folder(
595605
destination_project
596606
)
597607
if project_name != to_project_name:
598608
raise AppException(
599-
"Source and destination projects should be the same for copy_images"
609+
"Source and destination projects should be the same"
600610
)
601611
if not image_names:
602612
images = (
@@ -652,6 +662,12 @@ def move_images(
652662
:return: list of skipped image names
653663
:rtype: list of strs
654664
"""
665+
warning_msg = (
666+
"We're deprecating the move_images function. Please use move_items instead. Learn more."
667+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.move_items"
668+
)
669+
logger.warning(warning_msg)
670+
warnings.warn(warning_msg, DeprecationWarning)
655671
project_name, source_folder_name = extract_project_folder(source_project)
656672

657673
project = Controller.get_default().get_project_metadata(project_name).data
@@ -1811,6 +1827,12 @@ def attach_image_urls_to_project(
18111827
:return: list of linked image names, list of failed image names, list of duplicate image names
18121828
:rtype: tuple
18131829
"""
1830+
warning_msg = (
1831+
"We're deprecating the attach_image_urls_to_project function. Please use attach_items instead. Learn more."
1832+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.attach_items"
1833+
)
1834+
logger.warning(warning_msg)
1835+
warnings.warn(warning_msg, DeprecationWarning)
18141836
project_name, folder_name = extract_project_folder(project)
18151837
project = Controller.get_default().get_project_metadata(project_name).data
18161838
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -1878,6 +1900,12 @@ def attach_video_urls_to_project(
18781900
:return: attached videos, failed videos, skipped videos
18791901
:rtype: (list, list, list)
18801902
"""
1903+
warning_msg = (
1904+
"We're deprecating the attach_video_urls_to_project function. Please use attach_items instead. Learn more."
1905+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.attach_items"
1906+
)
1907+
logger.warning(warning_msg)
1908+
warnings.warn(warning_msg, DeprecationWarning)
18811909
project_name, folder_name = extract_project_folder(project)
18821910
project = Controller.get_default().get_project_metadata(project_name).data
18831911
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -2480,8 +2508,10 @@ def search_images_all_folders(
24802508
24812509
:param project: project name
24822510
:type project: str
2511+
24832512
:param image_name_prefix: image name prefix for search
24842513
:type image_name_prefix: str
2514+
24852515
:param annotation_status: if not None, annotation statuses of images to filter,
24862516
should be one of NotStarted InProgress QualityCheck Returned Completed Skipped
24872517
:type annotation_status: str
@@ -2741,6 +2771,12 @@ def attach_document_urls_to_project(
27412771
:return: list of attached documents, list of not attached documents, list of skipped documents
27422772
:rtype: tuple
27432773
"""
2774+
warning_msg = (
2775+
"We're deprecating the attach_document_urls_to_project function. Please use attach_items instead. Learn more."
2776+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.attach_items"
2777+
)
2778+
logger.warning(warning_msg)
2779+
warnings.warn(warning_msg, DeprecationWarning)
27442780
project_name, folder_name = extract_project_folder(project)
27452781
project = Controller.get_default().get_project_metadata(project_name).data
27462782
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -3093,3 +3129,127 @@ def search_items(
30933129
if response.errors:
30943130
raise AppException(response.errors)
30953131
return BaseSerializer.serialize_iterable(response.data)
3132+
3133+
3134+
@Trackable
3135+
@validate_arguments
3136+
def attach_items(
3137+
project: Union[NotEmptyStr, dict],
3138+
attachments: AttachmentArg,
3139+
annotation_status: Optional[AnnotationStatuses] = "NotStarted"
3140+
):
3141+
attachments = attachments.data
3142+
project_name, folder_name = extract_project_folder(project)
3143+
if attachments and isinstance(attachments[0], AttachmentDict):
3144+
unique_attachments = set(attachments)
3145+
duplicate_attachments = [item for item, count in collections.Counter(attachments).items() if count > 1]
3146+
else:
3147+
unique_attachments, duplicate_attachments = get_name_url_duplicated_from_csv(attachments)
3148+
if duplicate_attachments:
3149+
logger.info("Dropping duplicates.")
3150+
unique_attachments = parse_obj_as(List[AttachmentEntity], unique_attachments)
3151+
uploaded, fails, duplicated = [], [], []
3152+
if unique_attachments:
3153+
logger.info(f"Attaching {len(unique_attachments)} file(s) to project {project}.")
3154+
response = Controller.get_default().attach_items(
3155+
project_name=project_name,
3156+
folder_name=folder_name,
3157+
attachments=unique_attachments,
3158+
annotation_status=annotation_status,
3159+
)
3160+
if response.errors:
3161+
raise AppException(response.errors)
3162+
uploaded, duplicated = response.data
3163+
uploaded = [i["name"] for i in uploaded]
3164+
fails = [
3165+
attachment.name
3166+
for attachment in unique_attachments
3167+
if attachment.name not in uploaded and attachment.name not in duplicated
3168+
]
3169+
return uploaded, fails, duplicated
3170+
3171+
3172+
@Trackable
3173+
@validate_arguments
3174+
def copy_items(
3175+
source: Union[NotEmptyStr, dict],
3176+
destination: Union[NotEmptyStr, dict],
3177+
items: Optional[List[NotEmptyStr]] = None,
3178+
include_annotations: Optional[StrictBool] = True,
3179+
):
3180+
"""Copy images in bulk between folders in a project
3181+
3182+
:param source: project name or folder path to select items from (e.g., “project1/folder1”).
3183+
:type source: str
3184+
3185+
:param destination: project name (root) or folder path to place copied items.
3186+
:type destination: str
3187+
3188+
:param items: names of items to copy. If None, all items from the source directory will be copied.
3189+
:type itmes: list of str
3190+
3191+
:param include_annotations: enables annotations copy
3192+
:type include_annotations: bool
3193+
3194+
:return: list of skipped item names
3195+
:rtype: list of strs
3196+
"""
3197+
3198+
project_name, source_folder = extract_project_folder(source)
3199+
3200+
to_project_name, destination_folder = extract_project_folder(destination)
3201+
if project_name != to_project_name:
3202+
raise AppException(
3203+
"Source and destination projects should be the same for copy_images"
3204+
)
3205+
3206+
response = Controller.get_default().copy_items(
3207+
project_name=project_name,
3208+
from_folder=source_folder,
3209+
to_folder=destination_folder,
3210+
items=items,
3211+
include_annotations=include_annotations,
3212+
)
3213+
if response.errors:
3214+
raise AppException(response.errors)
3215+
3216+
return response.data
3217+
3218+
3219+
@Trackable
3220+
@validate_arguments
3221+
def move_items(
3222+
source: Union[NotEmptyStr, dict],
3223+
destination: Union[NotEmptyStr, dict],
3224+
items: Optional[List[NotEmptyStr]] = None,
3225+
):
3226+
"""Copy images in bulk between folders in a project
3227+
3228+
:param source: project name or folder path to pick items from (e.g., “project1/folder1”).
3229+
:type source: str
3230+
3231+
:param destination: project name (root) or folder path to move items to.
3232+
:type destination: str
3233+
3234+
:param items: names of items to move. If None, all items from the source directory will be moved.
3235+
:type items: list of str
3236+
3237+
:return: list of skipped item names
3238+
:rtype: list of strs
3239+
"""
3240+
3241+
project_name, source_folder = extract_project_folder(source)
3242+
to_project_name, destination_folder = extract_project_folder(destination)
3243+
if project_name != to_project_name:
3244+
raise AppException(
3245+
"Source and destination projects should be the same"
3246+
)
3247+
response = Controller.get_default().move_items(
3248+
project_name=project_name,
3249+
from_folder=source_folder,
3250+
to_folder=destination_folder,
3251+
items=items,
3252+
)
3253+
if response.errors:
3254+
raise AppException(response.errors)
3255+
return response.data

0 commit comments

Comments
 (0)