Skip to content

Commit bc182db

Browse files
authored
Merge pull request #454 from superannotateai/1028_delete_items
1028 delete items
2 parents 307784e + 1cef3a0 commit bc182db

File tree

5 files changed

+189
-22
lines changed

5 files changed

+189
-22
lines changed

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,17 +652,25 @@ def set_images_annotation_statuses(
652652
def delete_images(
653653
self, project: Union[NotEmptyStr, dict], image_names: Optional[List[str]] = None
654654
):
655-
"""Delete images in project.
655+
"""Delete Images in project.
656656
657657
:param project: project name or folder path (e.g., "project1/folder1")
658658
:type project: str
659659
:param image_names: to be deleted images' names. If None, all the images will be deleted
660660
:type image_names: list of strs
661661
"""
662+
663+
warning_msg = (
664+
"We're deprecating the delete_images function. Please use delete_items instead."
665+
"Learn more. \n"
666+
"https://superannotate.readthedocs.io/en/stable/superannotate.sdk.html#superannotate.delete_items"
667+
)
668+
logger.warning(warning_msg)
669+
warnings.warn(warning_msg, DeprecationWarning)
662670
project_name, folder_name = extract_project_folder(project)
663671

664672
if not isinstance(image_names, list) and image_names is not None:
665-
raise AppException("Image_names should be a list of str or None.")
673+
raise AppException("image_names should be a list of str or None.")
666674

667675
response = self.controller.delete_images(
668676
project_name=project_name, folder_name=folder_name, image_names=image_names
@@ -674,6 +682,25 @@ def delete_images(
674682
f"Images deleted in project {project_name}{'/' + folder_name if folder_name else ''}"
675683
)
676684

685+
def delete_items(
686+
self, project: str, items: Optional[List[str]] = None
687+
):
688+
"""Delete items in a given project.
689+
690+
:param project: project name or folder path (e.g., "project1/folder1")
691+
:type project: str
692+
:param items: to be deleted items' names. If None, all the items will be deleted
693+
:type items: list of str
694+
"""
695+
project_name, folder_name = extract_project_folder(project)
696+
697+
response = self.controller.delete_items(
698+
project_name=project_name, folder_name=folder_name, items=items
699+
)
700+
if response.errors:
701+
raise AppException(response.errors)
702+
703+
677704
def assign_items(
678705
self, project: Union[NotEmptyStr, dict], items: List[str], user: str
679706
):

src/superannotate/lib/core/serviceproviders.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ def set_images_statuses_bulk(
165165
def delete_images(self, project_id: int, team_id: int, image_ids: List[int]):
166166
raise NotImplementedError
167167

168+
def delete_items(self, project_id: int, team_id: int, item_ids: List[int]):
169+
raise NotImplementedError
170+
168171
def assign_images(
169172
self,
170173
team_id: int,

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

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List
33
from typing import Optional
44

5-
import superannotate.lib.core as constances
5+
import superannotate.lib.core as constants
66
from lib.core.conditions import Condition
77
from lib.core.conditions import CONDITION_EQ as EQ
88
from lib.core.entities import AttachmentEntity
@@ -22,10 +22,44 @@
2222
from lib.core.serviceproviders import SuperannotateServiceProvider
2323
from lib.core.usecases.base import BaseReportableUseCase
2424
from lib.core.usecases.base import BaseUseCase
25+
from lib.core.entities import ImageEntity
2526
from superannotate.logger import get_default_logger
2627

2728
logger = get_default_logger()
2829

30+
class GetBulkItems(BaseUseCase):
31+
def __init__(
32+
self,
33+
service: SuperannotateServiceProvider,
34+
project_id: int,
35+
team_id: int,
36+
folder_id: int,
37+
items: List[str],
38+
):
39+
super().__init__()
40+
self._service = service
41+
self._project_id = project_id
42+
self._team_id = team_id
43+
self._folder_id = folder_id
44+
self._items = items
45+
self._chunk_size = 500
46+
47+
def execute(self):
48+
res = []
49+
for i in range(0, len(self._items), self._chunk_size):
50+
response = self._service.get_bulk_items(
51+
project_id=self._project_id,
52+
team_id=self._team_id,
53+
folder_id=self._folder_id,
54+
items=self._items[i : i + self._chunk_size], # noqa: E203
55+
)
56+
57+
if not response.ok:
58+
raise AppException(response.error)
59+
#TODO stop using Image Entity when it gets deprecated and from_dict gets implemented for items
60+
res += [ImageEntity.from_dict(**item) for item in response.data]
61+
self._response.data = res
62+
return self._response
2963

3064
class GetItem(BaseReportableUseCase):
3165
def __init__(
@@ -44,22 +78,22 @@ def __init__(
4478

4579
@staticmethod
4680
def serialize_entity(entity: Entity, project: ProjectEntity):
47-
if project.upload_state != constances.UploadState.EXTERNAL.value:
81+
if project.upload_state != constants.UploadState.EXTERNAL.value:
4882
entity.url = None
4983
if project.type in (
50-
constances.ProjectType.VECTOR.value,
51-
constances.ProjectType.PIXEL.value,
84+
constants.ProjectType.VECTOR.value,
85+
constants.ProjectType.PIXEL.value,
5286
):
5387
tmp_entity = entity
54-
if project.type == constances.ProjectType.VECTOR.value:
88+
if project.type == constants.ProjectType.VECTOR.value:
5589
entity.segmentation_status = None
56-
if project.upload_state == constances.UploadState.EXTERNAL.value:
90+
if project.upload_state == constants.UploadState.EXTERNAL.value:
5791
tmp_entity.prediction_status = None
5892
tmp_entity.segmentation_status = None
5993
return TmpImageEntity(**tmp_entity.dict(by_alias=True))
60-
elif project.type == constances.ProjectType.VIDEO.value:
94+
elif project.type == constants.ProjectType.VIDEO.value:
6195
return VideoEntity(**entity.dict(by_alias=True))
62-
elif project.type == constances.ProjectType.DOCUMENT.value:
96+
elif project.type == constants.ProjectType.DOCUMENT.value:
6397
return DocumentEntity(**entity.dict(by_alias=True))
6498
return entity
6599

@@ -320,13 +354,13 @@ def __init__(
320354
attachments: List[AttachmentEntity],
321355
annotation_status: str,
322356
backend_service_provider: SuperannotateServiceProvider,
323-
upload_state_code: int = constances.UploadState.EXTERNAL.value,
357+
upload_state_code: int = constants.UploadState.EXTERNAL.value,
324358
):
325359
super().__init__(reporter)
326360
self._project = project
327361
self._folder = folder
328362
self._attachments = attachments
329-
self._annotation_status_code = constances.AnnotationStatus.get_value(
363+
self._annotation_status_code = constants.AnnotationStatus.get_value(
330364
annotation_status
331365
)
332366
self._upload_state_code = upload_state_code
@@ -349,18 +383,18 @@ def validate_limitations(self):
349383
if not response.ok:
350384
raise AppValidationException(response.error)
351385
if attachments_count > response.data.folder_limit.remaining_image_count:
352-
raise AppValidationException(constances.ATTACH_FOLDER_LIMIT_ERROR_MESSAGE)
386+
raise AppValidationException(constants.ATTACH_FOLDER_LIMIT_ERROR_MESSAGE)
353387
elif attachments_count > response.data.project_limit.remaining_image_count:
354-
raise AppValidationException(constances.ATTACH_PROJECT_LIMIT_ERROR_MESSAGE)
388+
raise AppValidationException(constants.ATTACH_PROJECT_LIMIT_ERROR_MESSAGE)
355389
elif (
356390
response.data.user_limit
357391
and attachments_count > response.data.user_limit.remaining_image_count
358392
):
359-
raise AppValidationException(constances.ATTACH_USER_LIMIT_ERROR_MESSAGE)
393+
raise AppValidationException(constants.ATTACH_USER_LIMIT_ERROR_MESSAGE)
360394

361395
def validate_upload_state(self):
362-
if self._project.upload_state == constances.UploadState.BASIC.value:
363-
raise AppValidationException(constances.ATTACHING_UPLOAD_STATE_ERROR)
396+
if self._project.upload_state == constants.UploadState.BASIC.value:
397+
raise AppValidationException(constants.ATTACHING_UPLOAD_STATE_ERROR)
364398

365399
@staticmethod
366400
def generate_meta():
@@ -447,9 +481,9 @@ def _validate_limitations(self, items_count):
447481
if not response.ok:
448482
raise AppValidationException(response.error)
449483
if items_count > response.data.folder_limit.remaining_image_count:
450-
raise AppValidationException(constances.COPY_FOLDER_LIMIT_ERROR_MESSAGE)
484+
raise AppValidationException(constants.COPY_FOLDER_LIMIT_ERROR_MESSAGE)
451485
if items_count > response.data.project_limit.remaining_image_count:
452-
raise AppValidationException(constances.COPY_PROJECT_LIMIT_ERROR_MESSAGE)
486+
raise AppValidationException(constants.COPY_PROJECT_LIMIT_ERROR_MESSAGE)
453487

454488
def validate_item_names(self):
455489
if self._item_names:
@@ -575,9 +609,9 @@ def _validate_limitations(self, items_count):
575609
if not response.ok:
576610
raise AppValidationException(response.error)
577611
if items_count > response.data.folder_limit.remaining_image_count:
578-
raise AppValidationException(constances.MOVE_FOLDER_LIMIT_ERROR_MESSAGE)
612+
raise AppValidationException(constants.MOVE_FOLDER_LIMIT_ERROR_MESSAGE)
579613
if items_count > response.data.project_limit.remaining_image_count:
580-
raise AppValidationException(constances.MOVE_PROJECT_LIMIT_ERROR_MESSAGE)
614+
raise AppValidationException(constants.MOVE_PROJECT_LIMIT_ERROR_MESSAGE)
581615

582616
def execute(self):
583617
if self.is_valid():
@@ -635,7 +669,7 @@ def __init__(
635669
self._folder = folder
636670
self._item_names = item_names
637671
self._items = items
638-
self._annotation_status_code = constances.AnnotationStatus.get_value(
672+
self._annotation_status_code = constants.AnnotationStatus.get_value(
639673
annotation_status
640674
)
641675
self._backend_service = backend_service_provider
@@ -687,3 +721,57 @@ def execute(self):
687721
self._response.errors = AppException(self.ERROR_MESSAGE)
688722
break
689723
return self._response
724+
725+
class DeleteItemsUseCase(BaseUseCase):
726+
CHUNK_SIZE = 1000
727+
728+
def __init__(
729+
self,
730+
project: ProjectEntity,
731+
folder: FolderEntity,
732+
backend_service_provider: SuperannotateServiceProvider,
733+
items: BaseReadOnlyRepository,
734+
item_names: List[str] = None,
735+
):
736+
super().__init__()
737+
self._project = project
738+
self._folder = folder
739+
self._items = items
740+
self._backend_service = backend_service_provider
741+
self._item_names = item_names
742+
743+
def execute(self):
744+
if self.is_valid():
745+
if self._item_names:
746+
item_ids = [
747+
item.uuid
748+
for item in GetBulkItems(
749+
service=self._backend_service,
750+
project_id=self._project.id,
751+
team_id=self._project.team_id,
752+
folder_id=self._folder.uuid,
753+
items=self._item_names,
754+
)
755+
.execute()
756+
.data
757+
]
758+
else:
759+
condition = (
760+
Condition("team_id", self._project.team_id, EQ)
761+
& Condition("project_id", self._project.id, EQ)
762+
& Condition("folder_id", self._folder.uuid, EQ)
763+
)
764+
item_ids = [item.id for item in self._items.get_all(condition)]
765+
766+
for i in range(0, len(item_ids), self.CHUNK_SIZE):
767+
response = self._backend_service.delete_items(
768+
project_id=self._project.id,
769+
team_id=self._project.team_id,
770+
item_ids=item_ids[i : i + self.CHUNK_SIZE], # noqa: E203
771+
)
772+
773+
logger.info(
774+
f"Items deleted in project {self._project.name}{'/' + self._folder.name if not self._folder.is_root else ''}"
775+
)
776+
777+
return self._response

src/superannotate/lib/infrastructure/controller.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,24 @@ def delete_images(
835835
)
836836
return use_case.execute()
837837

838+
def delete_items(
839+
self,
840+
project_name: str,
841+
folder_name: str,
842+
items: List[str] = None,
843+
):
844+
project = self._get_project(project_name)
845+
folder = self._get_folder(project, folder_name)
846+
847+
use_case = usecases.DeleteItemsUseCase(
848+
project=project,
849+
folder=folder,
850+
items=self.items,
851+
item_names=items,
852+
backend_service_provider=self._backend_client,
853+
)
854+
return use_case.execute()
855+
838856
def assign_items(
839857
self, project_name: str, folder_name: str, item_names: list, user: str
840858
):

src/superannotate/lib/infrastructure/services.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ class SuperannotateBackendService(BaseBackendService):
203203
URL_GET_IMAGES = "images"
204204
URL_GET_ITEMS = "items"
205205
URL_BULK_GET_IMAGES = "images/getBulk"
206+
URL_BULK_GET_ITEMS = "images/getBulk"
206207
URL_DELETE_FOLDERS = "image/delete/images"
208+
URL_DELETE_ITEMS = "image/delete/images"
207209
URL_CREATE_IMAGE = "image/ext-create"
208210
URL_PROJECT_SETTINGS = "project/{}/settings"
209211
URL_PROJECT_WORKFLOW = "project/{}/workflow"
@@ -710,6 +712,23 @@ def get_bulk_images(
710712
)
711713
return res.json()
712714

715+
def get_bulk_items(
716+
self, project_id: int, team_id: int, folder_id: int, items: List[str]
717+
) -> List[dict]:
718+
719+
bulk_get_items_url = urljoin(self.api_url, self.URL_BULK_GET_ITEMS)
720+
res = self._request(
721+
bulk_get_items_url,
722+
"post",
723+
data={
724+
"project_id": project_id,
725+
"team_id": team_id,
726+
"folder_id": folder_id,
727+
"names": items,
728+
},
729+
)
730+
return ServiceResponse(res, ServiceResponse)
731+
713732
def delete_images(self, project_id: int, team_id: int, image_ids: List[int]):
714733
delete_images_url = urljoin(self.api_url, self.URL_DELETE_FOLDERS)
715734
res = self._request(
@@ -720,6 +739,18 @@ def delete_images(self, project_id: int, team_id: int, image_ids: List[int]):
720739
)
721740
return res.json()
722741

742+
def delete_items(self, project_id: int, team_id: int, item_ids: List[int]):
743+
delete_items_url = urljoin(self.api_url, self.URL_DELETE_ITEMS)
744+
745+
res = self._request(
746+
delete_items_url,
747+
"put",
748+
params={"team_id": team_id, "project_id": project_id},
749+
data={"image_ids": item_ids},
750+
)
751+
752+
return ServiceResponse(res, ServiceResponse)
753+
723754
def assign_images(
724755
self,
725756
team_id: int,

0 commit comments

Comments
 (0)