Skip to content

Commit 916614b

Browse files
committed
Added attch/copy/move items
1 parent 289c046 commit 916614b

File tree

15 files changed

+857
-312
lines changed

15 files changed

+857
-312
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from superannotate.lib.app.interface.sdk_interface import consensus
3636
from superannotate.lib.app.interface.sdk_interface import copy_image
3737
from superannotate.lib.app.interface.sdk_interface import copy_images
38+
from superannotate.lib.app.interface.sdk_interface import copy_items
3839
from superannotate.lib.app.interface.sdk_interface import create_annotation_class
3940
from superannotate.lib.app.interface.sdk_interface import (
4041
create_annotation_classes_from_classes_json,
@@ -72,6 +73,7 @@
7273
from superannotate.lib.app.interface.sdk_interface import init
7374
from superannotate.lib.app.interface.sdk_interface import invite_contributors_to_team
7475
from superannotate.lib.app.interface.sdk_interface import move_images
76+
from superannotate.lib.app.interface.sdk_interface import move_items
7577
from superannotate.lib.app.interface.sdk_interface import pin_image
7678
from superannotate.lib.app.interface.sdk_interface import prepare_export
7779
from superannotate.lib.app.interface.sdk_interface import query
@@ -177,6 +179,8 @@
177179
"search_items",
178180
"query",
179181
"attach_items",
182+
"copy_items",
183+
"move_items",
180184
# Image Section
181185
"copy_images",
182186
"move_images",

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

Lines changed: 141 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@
1212
from typing import Union
1313

1414
import boto3
15-
from pydantic import StrictBool
16-
from pydantic import conlist
17-
from pydantic import parse_obj_as
18-
from pydantic.error_wrappers import ValidationError
19-
from tqdm import tqdm
20-
2115
import lib.core as constances
2216
from lib.app.annotation_helpers import add_annotation_bbox_to_json
2317
from lib.app.annotation_helpers import add_annotation_comment_to_json
@@ -56,7 +50,12 @@
5650
from lib.core.types import PriorityScore
5751
from lib.core.types import Project
5852
from lib.infrastructure.controller import Controller
53+
from pydantic import conlist
54+
from pydantic import parse_obj_as
55+
from pydantic import StrictBool
56+
from pydantic.error_wrappers import ValidationError
5957
from superannotate.logger import get_default_logger
58+
from tqdm import tqdm
6059

6160
logger = get_default_logger()
6261

@@ -594,15 +593,20 @@ def copy_images(
594593
:return: list of skipped image names
595594
:rtype: list of strs
596595
"""
597-
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)
598602
project_name, source_folder_name = extract_project_folder(source_project)
599603

600604
to_project_name, destination_folder_name = extract_project_folder(
601605
destination_project
602606
)
603607
if project_name != to_project_name:
604608
raise AppException(
605-
"Source and destination projects should be the same for copy_images"
609+
"Source and destination projects should be the same"
606610
)
607611
if not image_names:
608612
images = (
@@ -658,6 +662,12 @@ def move_images(
658662
:return: list of skipped image names
659663
:rtype: list of strs
660664
"""
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)
661671
project_name, source_folder_name = extract_project_folder(source_project)
662672

663673
project = Controller.get_default().get_project_metadata(project_name).data
@@ -1817,6 +1827,12 @@ def attach_image_urls_to_project(
18171827
:return: list of linked image names, list of failed image names, list of duplicate image names
18181828
:rtype: tuple
18191829
"""
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)
18201836
project_name, folder_name = extract_project_folder(project)
18211837
project = Controller.get_default().get_project_metadata(project_name).data
18221838
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -1884,6 +1900,12 @@ def attach_video_urls_to_project(
18841900
:return: attached videos, failed videos, skipped videos
18851901
:rtype: (list, list, list)
18861902
"""
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)
18871909
project_name, folder_name = extract_project_folder(project)
18881910
project = Controller.get_default().get_project_metadata(project_name).data
18891911
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -2744,6 +2766,12 @@ def attach_document_urls_to_project(
27442766
:return: list of attached documents, list of not attached documents, list of skipped documents
27452767
:rtype: tuple
27462768
"""
2769+
warning_msg = (
2770+
"We're deprecating the attach_document_urls_to_project function. Please use attach_items instead. Learn more."
2771+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.attach_items"
2772+
)
2773+
logger.warning(warning_msg)
2774+
warnings.warn(warning_msg, DeprecationWarning)
27472775
project_name, folder_name = extract_project_folder(project)
27482776
project = Controller.get_default().get_project_metadata(project_name).data
27492777
project_folder_name = project_name + (f"/{folder_name}" if folder_name else "")
@@ -3102,66 +3130,121 @@ def search_items(
31023130
@validate_arguments
31033131
def attach_items(
31043132
project: Union[NotEmptyStr, dict],
3105-
attachments,
3106-
annotation_status="NotStarted"
3133+
attachments: AttachmentArg,
3134+
annotation_status: Optional[AnnotationStatuses] = "NotStarted"
31073135
):
3108-
"""Link items from external storage to SuperAnnotate using URLs.
3136+
attachments = attachments.data
3137+
project_name, folder_name = extract_project_folder(project)
3138+
if attachments and isinstance(attachments[0], AttachmentDict):
3139+
unique_attachments = set(attachments)
3140+
duplicate_attachments = [item for item, count in collections.Counter(attachments).items() if count > 1]
3141+
else:
3142+
unique_attachments, duplicate_attachments = get_name_url_duplicated_from_csv(attachments)
3143+
if duplicate_attachments:
3144+
logger.info("Dropping duplicates.")
3145+
unique_attachments = parse_obj_as(List[AttachmentEntity], unique_attachments)
3146+
uploaded, fails, duplicated = [], [], []
3147+
if unique_attachments:
3148+
logger.info(f"Attaching {len(unique_attachments)} file(s) to project {project}.")
3149+
response = Controller.get_default().attach_items(
3150+
project_name=project_name,
3151+
folder_name=folder_name,
3152+
attachments=unique_attachments,
3153+
annotation_status=annotation_status,
3154+
)
3155+
if response.errors:
3156+
raise AppException(response.errors)
3157+
uploaded, duplicated = response.data
3158+
uploaded = [i["name"] for i in uploaded]
3159+
fails = [
3160+
attachment.name
3161+
for attachment in unique_attachments
3162+
if attachment.name not in uploaded and attachment.name not in duplicated
3163+
]
3164+
return uploaded, fails, duplicated
31093165

3110-
:param project: project name or folder path (e.g., “project1/folder1”)
3111-
:type project: str
31123166

3113-
:param attachments: path to CSV file or list of dicts containing attachments URLs.
3114-
:type attachments: path-like (str or Path) or list of dicts
3167+
@Trackable
3168+
@validate_arguments
3169+
def copy_items(
3170+
source: Union[NotEmptyStr, dict],
3171+
destination: Union[NotEmptyStr, dict],
3172+
items: Optional[List[NotEmptyStr]] = None,
3173+
include_annotations: Optional[StrictBool] = True,
3174+
):
3175+
"""Copy images in bulk between folders in a project
31153176
3116-
:param annotation_status: value to set the annotation statuses of the
3117-
linked items:
3118-
“NotStarted”
3119-
“InProgress”
3120-
“QualityCheck”
3121-
“Returned”
3122-
“Completed”
3123-
“Skipped”
3124-
:type annotation_status: str
3177+
:param source: project name or folder path to select items from (e.g., “project1/folder1”).
3178+
:type source: str
31253179
3126-
:return: list of attached item names, list of not attached item names, list of duplicate item names
3127-
that are already in SuperAnnotate.
3128-
:rtype: tuple
3180+
:param destination: project name (root) or folder path to place copied items.
3181+
:type destination: str
3182+
3183+
:param items: names of items to copy. If None, all items from the source directory will be copied.
3184+
:type itmes: list of str
3185+
3186+
:param include_annotations: enables annotations copy
3187+
:type include_annotations: bool
3188+
3189+
:return: list of skipped item names
3190+
:rtype: list of strs
31293191
"""
3130-
project_name, folder_name = extract_project_folder(project)
31313192

3132-
images_to_upload, duplicate_images = get_paths_and_duplicated_from_csv(attachments)
3193+
project_name, source_folder = extract_project_folder(source)
31333194

3134-
attachments_data
3195+
to_project_name, destination_folder = extract_project_folder(destination)
3196+
if project_name != to_project_name:
3197+
raise AppException(
3198+
"Source and destination projects should be the same for copy_images"
3199+
)
31353200

3136-
use_case = Controller.get_default().attach_items(
3201+
response = Controller.get_default().copy_items(
31373202
project_name=project_name,
3138-
folder_name=folder_name,
3139-
files=ImageSerializer.deserialize(images_to_upload), # noqa: E203
3140-
annotation_status=annotation_status,
3203+
from_folder=source_folder,
3204+
to_folder=destination_folder,
3205+
items=items,
3206+
include_annotations=include_annotations,
31413207
)
3142-
if len(duplicate_images):
3143-
logger.warning(
3144-
constances.ALREADY_EXISTING_FILES_WARNING.format(len(duplicate_images))
3145-
)
3208+
if response.errors:
3209+
raise AppException(response.errors)
31463210

3147-
if use_case.is_valid():
3148-
logger.info(
3149-
constances.ATTACHING_FILES_MESSAGE.format(
3150-
len(images_to_upload), project
3151-
)
3211+
return response.data
3212+
3213+
3214+
@Trackable
3215+
@validate_arguments
3216+
def move_items(
3217+
source: Union[NotEmptyStr, dict],
3218+
destination: Union[NotEmptyStr, dict],
3219+
items: Optional[List[NotEmptyStr]] = None,
3220+
):
3221+
"""Copy images in bulk between folders in a project
3222+
3223+
:param source: project name or folder path to pick items from (e.g., “project1/folder1”).
3224+
:type source: str
3225+
3226+
:param destination: project name (root) or folder path to move items to.
3227+
:type destination: str
3228+
3229+
:param items: names of items to move. If None, all items from the source directory will be moved.
3230+
:type items: list of str
3231+
3232+
:return: list of skipped item names
3233+
:rtype: list of strs
3234+
"""
3235+
3236+
project_name, source_folder = extract_project_folder(source)
3237+
to_project_name, destination_folder = extract_project_folder(destination)
3238+
if project_name != to_project_name:
3239+
raise AppException(
3240+
"Source and destination projects should be the same"
31523241
)
3153-
with tqdm(
3154-
total=use_case.attachments_count, desc="Attaching urls"
3155-
) as progress_bar:
3156-
for attached in use_case.execute():
3157-
progress_bar.update(attached)
3158-
uploaded, duplications = use_case.data
3159-
uploaded = [i["name"] for i in uploaded]
3160-
duplications.extend(duplicate_images)
3161-
failed_images = [
3162-
image["name"]
3163-
for image in images_to_upload
3164-
if image["name"] not in uploaded + duplications
3165-
]
3166-
return uploaded, failed_images, duplications
3167-
raise AppException(use_case.response.errors)
3242+
response = Controller.get_default().move_items(
3243+
project_name=project_name,
3244+
from_folder=source_folder,
3245+
to_folder=destination_folder,
3246+
items=items,
3247+
)
3248+
if response.errors:
3249+
raise AppException(response.errors)
3250+
return response.data

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ class AttachmentArg(BaseModel):
113113
def __getitem__(self, index):
114114
return self.__root__[index]
115115

116+
@property
117+
def data(self):
118+
return self.__root__
119+
116120
@root_validator(pre=True)
117121
def validate_root(cls, values):
118122
try:

src/superannotate/lib/app/mixp/utils/parsers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,47 @@ def search_items(**kwargs):
10291029
"recursive": bool(recursive),
10301030
},
10311031
}
1032+
1033+
1034+
def move_items(**kwargs):
1035+
project = kwargs["project"]
1036+
project_name, _ = extract_project_folder(project)
1037+
project = Controller.get_default().get_project_metadata(project_name).data["project"]
1038+
items = kwargs["items"]
1039+
return {
1040+
"event_name": "move_items",
1041+
"properties": {
1042+
"project_type": ProjectType.get_name(project.project_type),
1043+
"items_count": len(items) if items else None
1044+
},
1045+
}
1046+
1047+
1048+
def copy_items(**kwargs):
1049+
project = kwargs["project"]
1050+
project_name, _ = extract_project_folder(project)
1051+
project = Controller.get_default().get_project_metadata(project_name).data["project"]
1052+
items = kwargs["items"]
1053+
return {
1054+
"event_name": "copy_items",
1055+
"properties": {
1056+
"project_type": ProjectType.get_name(project.project_type),
1057+
"items_count": len(items) if items else None,
1058+
"include_annotations": kwargs["include_annotations"]
1059+
},
1060+
}
1061+
1062+
1063+
def attach_items(**kwargs):
1064+
project = kwargs["project"]
1065+
project_name, _ = extract_project_folder(project)
1066+
project = Controller.get_default().get_project_metadata(project_name).data["project"]
1067+
attachments = kwargs["attachments"]
1068+
return {
1069+
"event_name": "copy_items",
1070+
"properties": {
1071+
"project_type": ProjectType.get_name(project.project_type),
1072+
"attachments": "scv" if isinstance(attachments, (str, Path)) else "dict",
1073+
"annotation_status": kwargs["annotation_status"]
1074+
},
1075+
}

src/superannotate/lib/core/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ class AppException(Exception):
77
def __init__(self, message):
88
super().__init__(message)
99

10-
self.message = message
10+
self.message = str(message)
1111

1212
def __str__(self):
1313
return self.message
1414

1515

16+
class BackendError(AppException):
17+
"""
18+
Backend Error
19+
"""
20+
21+
1622
class AppValidationException(AppException):
1723
"""
1824
App validation exception

0 commit comments

Comments
 (0)