Skip to content

Commit d65ff10

Browse files
committed
finalizing this delete items
2 parents 69b9fae + 3c85504 commit d65ff10

File tree

7 files changed

+169
-63
lines changed

7 files changed

+169
-63
lines changed

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

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ def set_images_annotation_statuses(
650650
logger.info("Annotations status of images changed")
651651

652652
def delete_images(
653-
self, project: Union[NotEmptyStr, dict], image_names: Optional[list[str]] = None
653+
self, project: Union[NotEmptyStr, dict], image_names: Optional[List[str]] = None
654654
):
655655
"""delete images in project.
656656
@@ -694,19 +694,12 @@ def delete_items(
694694
"""
695695
project_name, folder_name = extract_project_folder(project)
696696

697-
## Type checking should be done in controller or by PyDantic?
698-
# if not isinstance(image_names, list) and image_names is not None:
699-
# raise AppException("Image_names should be a list of str or None.")
700-
701697
response = self.controller.delete_items(
702698
project_name=project_name, folder_name=folder_name, items=items
703699
)
704700
if response.errors:
705701
raise AppException(response.errors)
706702

707-
logger.info(
708-
f"Items deleted in project {project_name}{'/' + folder_name if folder_name else ''}"
709-
)
710703

711704
def assign_items(
712705
self, project: Union[NotEmptyStr, dict], items: List[str], user: str
@@ -718,8 +711,10 @@ def assign_items(
718711
719712
:param project: project name or folder path (e.g., "project1/folder1")
720713
:type project: str
714+
721715
:param items: list of items to assign
722-
:type item_names: list of str
716+
:type items: list of str
717+
723718
:param user: user email
724719
:type user: str
725720
"""
@@ -728,9 +723,7 @@ def assign_items(
728723

729724
response = self.controller.assign_items(project_name, folder_name, items, user)
730725

731-
if not response.errors:
732-
logger.info(f"Assign items to user {user}")
733-
else:
726+
if response.errors:
734727
raise AppException(response.errors)
735728

736729
def unassign_items(
@@ -743,7 +736,7 @@ def unassign_items(
743736
:param project: project name or folder path (e.g., "project1/folder1")
744737
:type project: str
745738
:param items: list of items to unassign
746-
:type item_names: list of str
739+
:type items: list of str
747740
"""
748741
project_name, folder_name = extract_project_folder(project)
749742

src/superannotate/lib/core/service_types.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ def __init__(self, response, content_type=None):
7979
"reason": response.reason,
8080
"content": response.content,
8181
}
82-
if response.ok:
82+
try:
8383
if content_type and content_type is not self.__class__:
8484
data["data"] = content_type(**response.json())
8585
else:
8686
data["data"] = response.json()
87+
except Exception as e:
88+
data["data"] = {}
8789
super().__init__(**data)
8890

8991
@property
@@ -92,5 +94,8 @@ def ok(self):
9294

9395
@property
9496
def error(self):
95-
default_message = self.data["reason"] if self.data["reason"] else "Unknown error"
96-
return getattr(self.data, "error", default_message)
97+
default_message = self.reason if self.reason else "Unknown Error"
98+
if isinstance(self.data, dict):
99+
return self.data.get("error", default_message)
100+
else:
101+
return getattr(self.data, "error", default_message)

src/superannotate/lib/core/serviceproviders.py

Lines changed: 4 additions & 1 deletion
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,
@@ -182,7 +185,7 @@ def assign_items(
182185
folder_name: str,
183186
user: str,
184187
item_names: list,
185-
):
188+
) -> ServiceResponse:
186189
raise NotImplementedError
187190

188191
def get_bulk_images(

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

Lines changed: 122 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import copy
22
from typing import List
33

4-
import superannotate.lib.core as constances
4+
import superannotate.lib.core as constants
55
from lib.core.conditions import Condition
66
from lib.core.conditions import CONDITION_EQ as EQ
77
from lib.core.entities import AttachmentEntity
@@ -20,11 +20,44 @@
2020
from lib.core.serviceproviders import SuperannotateServiceProvider
2121
from lib.core.usecases.base import BaseReportableUseCase
2222
from lib.core.usecases.base import BaseUseCase
23+
from lib.core.entities import ImageEntity
2324
from superannotate.logger import get_default_logger
2425

25-
2626
logger = get_default_logger()
2727

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

2962
class GetItem(BaseReportableUseCase):
3063
def __init__(
@@ -43,22 +76,22 @@ def __init__(
4376

4477
@staticmethod
4578
def serialize_entity(entity: Entity, project: ProjectEntity):
46-
if project.upload_state != constances.UploadState.EXTERNAL.value:
79+
if project.upload_state != constants.UploadState.EXTERNAL.value:
4780
entity.url = None
4881
if project.type in (
49-
constances.ProjectType.VECTOR.value,
50-
constances.ProjectType.PIXEL.value,
82+
constants.ProjectType.VECTOR.value,
83+
constants.ProjectType.PIXEL.value,
5184
):
5285
tmp_entity = entity
53-
if project.type == constances.ProjectType.VECTOR.value:
86+
if project.type == constants.ProjectType.VECTOR.value:
5487
entity.segmentation_status = None
55-
if project.upload_state == constances.UploadState.EXTERNAL.value:
88+
if project.upload_state == constants.UploadState.EXTERNAL.value:
5689
tmp_entity.prediction_status = None
5790
tmp_entity.segmentation_status = None
5891
return TmpImageEntity(**tmp_entity.dict(by_alias=True))
59-
elif project.type == constances.ProjectType.VIDEO.value:
92+
elif project.type == constants.ProjectType.VIDEO.value:
6093
return VideoEntity(**entity.dict(by_alias=True))
61-
elif project.type == constances.ProjectType.DOCUMENT.value:
94+
elif project.type == constants.ProjectType.DOCUMENT.value:
6295
return DocumentEntity(**entity.dict(by_alias=True))
6396
return entity
6497

@@ -104,7 +137,7 @@ def validate_query(self):
104137
self._query = response["parsedQuery"]
105138
else:
106139
raise AppException("Incorrect query.")
107-
if self._project.sync_status != constances.ProjectState.SYNCED.value:
140+
if self._project.sync_status != constants.ProjectState.SYNCED.value:
108141
raise AppException("Data is not synced.")
109142

110143
def execute(self) -> Response:
@@ -210,33 +243,31 @@ def __init__(
210243
self._user = user
211244
self._service = service
212245

213-
def validate_user(
246+
def validate_item_names(
214247
self,
215248
):
216-
217-
for c in self._project.users:
218-
if c["user_id"] == self._user:
219-
return True
220-
221-
raise AppValidationException(
222-
f"{self._user} is not a verified contributor for the {self._project.name}"
223-
)
249+
self._item_names = list(set(self._item_names))
224250

225251
def execute(self):
252+
cnt_assigned = 0
253+
total_count = len(self._item_names)
226254
if self.is_valid():
227255
for i in range(0, len(self._item_names), self.CHUNK_SIZE):
228-
is_assigned = self._service.assign_items(
256+
response = self._service.assign_items(
229257
team_id=self._project.team_id,
230258
project_id=self._project.id,
231259
folder_name=self._folder.name,
232260
user=self._user,
233261
item_names=self._item_names[i : i + self.CHUNK_SIZE], # noqa: E203
234262
)
235-
if not is_assigned:
236-
self._response.errors = AppException(
237-
f"Cant assign {', '.join(self._item_names[i: i + self.CHUNK_SIZE])}"
238-
)
239-
continue
263+
if not response.ok and response.error: # User not found
264+
self._response.errors += response.error
265+
return self._response
266+
267+
cnt_assigned += response.data["successCount"]
268+
logger.info(
269+
f"Assigned {cnt_assigned}/{total_count} items to user {self._user}"
270+
)
240271
return self._response
241272

242273

@@ -284,13 +315,13 @@ def __init__(
284315
attachments: List[AttachmentEntity],
285316
annotation_status: str,
286317
backend_service_provider: SuperannotateServiceProvider,
287-
upload_state_code: int = constances.UploadState.EXTERNAL.value,
318+
upload_state_code: int = constants.UploadState.EXTERNAL.value,
288319
):
289320
super().__init__(reporter)
290321
self._project = project
291322
self._folder = folder
292323
self._attachments = attachments
293-
self._annotation_status_code = constances.AnnotationStatus.get_value(
324+
self._annotation_status_code = constants.AnnotationStatus.get_value(
294325
annotation_status
295326
)
296327
self._upload_state_code = upload_state_code
@@ -313,18 +344,18 @@ def validate_limitations(self):
313344
if not response.ok:
314345
raise AppValidationException(response.error)
315346
if attachments_count > response.data.folder_limit.remaining_image_count:
316-
raise AppValidationException(constances.ATTACH_FOLDER_LIMIT_ERROR_MESSAGE)
347+
raise AppValidationException(constants.ATTACH_FOLDER_LIMIT_ERROR_MESSAGE)
317348
elif attachments_count > response.data.project_limit.remaining_image_count:
318-
raise AppValidationException(constances.ATTACH_PROJECT_LIMIT_ERROR_MESSAGE)
349+
raise AppValidationException(constants.ATTACH_PROJECT_LIMIT_ERROR_MESSAGE)
319350
elif (
320351
response.data.user_limit
321352
and attachments_count > response.data.user_limit.remaining_image_count
322353
):
323-
raise AppValidationException(constances.ATTACH_USER_LIMIT_ERROR_MESSAGE)
354+
raise AppValidationException(constants.ATTACH_USER_LIMIT_ERROR_MESSAGE)
324355

325356
def validate_upload_state(self):
326-
if self._project.upload_state == constances.UploadState.BASIC.value:
327-
raise AppValidationException(constances.ATTACHING_UPLOAD_STATE_ERROR)
357+
if self._project.upload_state == constants.UploadState.BASIC.value:
358+
raise AppValidationException(constants.ATTACHING_UPLOAD_STATE_ERROR)
328359

329360
@staticmethod
330361
def generate_meta():
@@ -411,9 +442,9 @@ def _validate_limitations(self, items_count):
411442
if not response.ok:
412443
raise AppValidationException(response.error)
413444
if items_count > response.data.folder_limit.remaining_image_count:
414-
raise AppValidationException(constances.COPY_FOLDER_LIMIT_ERROR_MESSAGE)
445+
raise AppValidationException(constants.COPY_FOLDER_LIMIT_ERROR_MESSAGE)
415446
if items_count > response.data.project_limit.remaining_image_count:
416-
raise AppValidationException(constances.COPY_PROJECT_LIMIT_ERROR_MESSAGE)
447+
raise AppValidationException(constants.COPY_PROJECT_LIMIT_ERROR_MESSAGE)
417448

418449
def validate_item_names(self):
419450
if self._item_names:
@@ -539,9 +570,9 @@ def _validate_limitations(self, items_count):
539570
if not response.ok:
540571
raise AppValidationException(response.error)
541572
if items_count > response.data.folder_limit.remaining_image_count:
542-
raise AppValidationException(constances.MOVE_FOLDER_LIMIT_ERROR_MESSAGE)
573+
raise AppValidationException(constants.MOVE_FOLDER_LIMIT_ERROR_MESSAGE)
543574
if items_count > response.data.project_limit.remaining_image_count:
544-
raise AppValidationException(constances.MOVE_PROJECT_LIMIT_ERROR_MESSAGE)
575+
raise AppValidationException(constants.MOVE_PROJECT_LIMIT_ERROR_MESSAGE)
545576

546577
def execute(self):
547578
if self.is_valid():
@@ -599,7 +630,7 @@ def __init__(
599630
self._folder = folder
600631
self._item_names = item_names
601632
self._items = items
602-
self._annotation_status_code = constances.AnnotationStatus.get_value(
633+
self._annotation_status_code = constants.AnnotationStatus.get_value(
603634
annotation_status
604635
)
605636
self._backend_service = backend_service_provider
@@ -651,3 +682,57 @@ def execute(self):
651682
self._response.errors = AppException(self.ERROR_MESSAGE)
652683
break
653684
return self._response
685+
686+
class DeleteItemsUseCase(BaseUseCase):
687+
CHUNK_SIZE = 1000
688+
689+
def __init__(
690+
self,
691+
project: ProjectEntity,
692+
folder: FolderEntity,
693+
backend_service_provider: SuperannotateServiceProvider,
694+
items: BaseReadOnlyRepository,
695+
item_names: List[str] = None,
696+
):
697+
super().__init__()
698+
self._project = project
699+
self._folder = folder
700+
self._items = items
701+
self._backend_service = backend_service_provider
702+
self._item_names = item_names
703+
704+
def execute(self):
705+
if self.is_valid():
706+
if self._item_names:
707+
item_ids = [
708+
item.uuid
709+
for item in GetBulkItems(
710+
service=self._backend_service,
711+
project_id=self._project.id,
712+
team_id=self._project.team_id,
713+
folder_id=self._folder.uuid,
714+
items=self._item_names,
715+
)
716+
.execute()
717+
.data
718+
]
719+
else:
720+
condition = (
721+
Condition("team_id", self._project.team_id, EQ)
722+
& Condition("project_id", self._project.id, EQ)
723+
& Condition("folder_id", self._folder.uuid, EQ)
724+
)
725+
item_ids = [item.id for item in self._items.get_all(condition)]
726+
727+
for i in range(0, len(item_ids), self.CHUNK_SIZE):
728+
response = self._backend_service.delete_items(
729+
project_id=self._project.id,
730+
team_id=self._project.team_id,
731+
item_ids=item_ids[i : i + self.CHUNK_SIZE], # noqa: E203
732+
)
733+
734+
logger.info(
735+
f"Items deleted in project {self._project.name}{'/' + self._folder.name if not self._folder.is_root else ''}"
736+
)
737+
738+
return self._response

src/superannotate/lib/infrastructure/controller.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,8 @@ def delete_items(
847847
use_case = usecases.DeleteItemsUseCase(
848848
project=project,
849849
folder=folder,
850-
items=items,
850+
items=self.items,
851+
item_names=items,
851852
backend_service_provider=self._backend_client,
852853
)
853854
return use_case.execute()

0 commit comments

Comments
 (0)