Skip to content

Commit 2644c2c

Browse files
authored
Merge pull request #799 from superannotateai/FRIDAY-3992
added item category set/remove functions
2 parents 2e80804 + c7a8ec9 commit 2644c2c

File tree

7 files changed

+331
-7
lines changed

7 files changed

+331
-7
lines changed

docs/source/api_reference/api_item.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ Items
2121
.. automethod:: superannotate.SAClient.unassign_items
2222
.. automethod:: superannotate.SAClient.get_item_metadata
2323
.. automethod:: superannotate.SAClient.set_approval_statuses
24+
.. automethod:: superannotate.SAClient.set_items_category
25+
.. automethod:: superannotate.SAClient.remove_items_category

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4393,6 +4393,73 @@ def move_items(
43934393
raise AppException(response.errors)
43944394
return response.data
43954395

4396+
def set_items_category(
4397+
self,
4398+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
4399+
items: List[Union[int, str]],
4400+
category: str,
4401+
):
4402+
"""
4403+
Add categories to one or more items.
4404+
4405+
:param project: Project and folder as a tuple, folder is optional.
4406+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
4407+
4408+
:param items: A list of names or IDs of the items to modify.
4409+
:type items: List[Union[int, str]]
4410+
4411+
:param category: Category to assign to the item.
4412+
:type category: Str
4413+
4414+
Request Example:
4415+
::
4416+
4417+
client.set_items_category(
4418+
project=("product-review-mm", "folder1"),
4419+
items=[112233, 112344],
4420+
category="Shoes"
4421+
)
4422+
"""
4423+
project, folder = self.controller.get_project_folder(project)
4424+
self.controller.check_multimodal_project_categorization(project)
4425+
4426+
self.controller.items.attach_detach_items_category(
4427+
project=project,
4428+
folder=folder,
4429+
items=items,
4430+
category=category,
4431+
operation="attach",
4432+
)
4433+
4434+
def remove_items_category(
4435+
self,
4436+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
4437+
items: List[Union[int, str]],
4438+
):
4439+
"""
4440+
Remove categories from one or more items.
4441+
4442+
:param project: Project and folder as a tuple, folder is optional.
4443+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
4444+
4445+
:param items: A list of names or IDs of the items to modify.
4446+
:type items: List[Union[int, str]]
4447+
4448+
Request Example:
4449+
::
4450+
4451+
client.remove_items_category(
4452+
project=("product-review-mm", "folder1"),
4453+
items=[112233, 112344]
4454+
)
4455+
"""
4456+
project, folder = self.controller.get_project_folder(project)
4457+
self.controller.check_multimodal_project_categorization(project)
4458+
4459+
self.controller.items.attach_detach_items_category(
4460+
project=project, folder=folder, items=items, operation="detach"
4461+
)
4462+
43964463
def set_annotation_statuses(
43974464
self,
43984465
project: Union[NotEmptyStr, dict],

src/superannotate/lib/core/serviceproviders.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,14 @@ def delete_multiple(
499499

500500
@abstractmethod
501501
def bulk_attach_categories(
502-
self, project_id: int, folder_id: int, item_category_map: Dict[int, int]
503-
) -> bool:
502+
self, project_id: int, folder_id: int, item_id_category_id_map: Dict[int, int]
503+
) -> ServiceResponse:
504+
raise NotImplementedError
505+
506+
@abstractmethod
507+
def bulk_detach_categories(
508+
self, project_id: int, folder_id: int, item_ids: List[int]
509+
) -> ServiceResponse:
504510
raise NotImplementedError
505511

506512

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Dict
88
from typing import Generator
99
from typing import List
10+
from typing import Optional
1011
from typing import Union
1112

1213
import lib.core as constants
@@ -20,6 +21,7 @@
2021
from lib.core.entities import ProjectEntity
2122
from lib.core.entities import VideoEntity
2223
from lib.core.entities.items import MultiModalItemEntity
24+
from lib.core.entities.items import ProjectCategoryEntity
2325
from lib.core.exceptions import AppException
2426
from lib.core.exceptions import AppValidationException
2527
from lib.core.exceptions import BackendError
@@ -38,6 +40,7 @@
3840
from lib.infrastructure.utils import extract_project_folder
3941
from typing_extensions import Literal
4042

43+
4144
logger = logging.getLogger("sa")
4245

4346

@@ -1272,3 +1275,51 @@ def execute(
12721275
# returning control to the interface function that called it. So no need for
12731276
# error handling in the response
12741277
return self._response
1278+
1279+
1280+
class AttacheDetachItemsCategoryUseCase(BaseUseCase):
1281+
CHUNK_SIZE = 2000
1282+
1283+
def __init__(
1284+
self,
1285+
project: ProjectEntity,
1286+
folder: FolderEntity,
1287+
items: List[MultiModalItemEntity],
1288+
service_provider: BaseServiceProvider,
1289+
operation: Literal["attach", "detach"],
1290+
category: Optional[ProjectCategoryEntity] = None,
1291+
):
1292+
super().__init__()
1293+
self._project = project
1294+
self._folder = folder
1295+
self._items = items
1296+
self._category = category
1297+
self._operation = operation
1298+
self._service_provider = service_provider
1299+
1300+
def execute(self):
1301+
if self._operation == "attach":
1302+
success_count = 0
1303+
for chunk in divide_to_chunks(self._items, self.CHUNK_SIZE):
1304+
item_id_category_id_map: Dict[int, int] = {
1305+
i.id: self._category.id for i in chunk
1306+
}
1307+
response = self._service_provider.items.bulk_attach_categories(
1308+
project_id=self._project.id,
1309+
folder_id=self._folder.id,
1310+
item_id_category_id_map=item_id_category_id_map,
1311+
)
1312+
success_count += len(response.data)
1313+
logger.info(
1314+
f"{self._category.name} category successfully added to {success_count} items."
1315+
)
1316+
elif self._operation == "detach":
1317+
success_count = 0
1318+
for chunk in divide_to_chunks(self._items, self.CHUNK_SIZE):
1319+
response = self._service_provider.items.bulk_detach_categories(
1320+
project_id=self._project.id,
1321+
folder_id=self._folder.id,
1322+
item_ids=[i.id for i in chunk],
1323+
)
1324+
success_count += len(response.data)
1325+
logger.info(f"Category successfully removed from {success_count} items.")

src/superannotate/lib/infrastructure/controller.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from lib.core.entities.filters import ProjectFilters
3636
from lib.core.entities.filters import UserFilters
3737
from lib.core.entities.integrations import IntegrationEntity
38+
from lib.core.entities.items import ProjectCategoryEntity
3839
from lib.core.entities.work_managament import ScoreEntity
3940
from lib.core.entities.work_managament import ScorePayloadEntity
4041
from lib.core.enums import CustomFieldEntityEnum
@@ -877,7 +878,7 @@ def list_items(
877878
folder: FolderEntity,
878879
/,
879880
include: List[str] = None,
880-
**filters: Unpack[ItemFilters],
881+
**filters: Optional[Unpack[ItemFilters]],
881882
) -> List[BaseItemEntity]:
882883

883884
entity = PROJECT_ITEM_ENTITY_MAP.get(project.type, BaseItemEntity)
@@ -1062,6 +1063,46 @@ def update(self, project: ProjectEntity, item: BaseItemEntity):
10621063
)
10631064
return use_case.execute()
10641065

1066+
def attach_detach_items_category(
1067+
self,
1068+
project: ProjectEntity,
1069+
folder: FolderEntity,
1070+
items: List[Union[int, str]],
1071+
operation: Literal["attach", "detach"],
1072+
category: Optional[str] = None,
1073+
):
1074+
if items and isinstance(items[0], str):
1075+
items = self.list_items(project, folder, name__in=items)
1076+
elif items and isinstance(items[0], int):
1077+
items = self.list_items(project, folder, id__in=items)
1078+
else:
1079+
raise AppException(
1080+
"Items must be a list of strings or integers representing item IDs."
1081+
)
1082+
1083+
if category:
1084+
all_categories = (
1085+
self.service_provider.work_management.list_project_categories(
1086+
project.id, ProjectCategoryEntity # noqa
1087+
)
1088+
)
1089+
category = next(
1090+
(c for c in all_categories.data if c.name.lower() == category.lower()),
1091+
None,
1092+
)
1093+
if not category:
1094+
raise AppException("Category not defined in project.")
1095+
1096+
use_case = usecases.AttacheDetachItemsCategoryUseCase(
1097+
project=project,
1098+
folder=folder,
1099+
items=items,
1100+
category=category,
1101+
operation=operation,
1102+
service_provider=self.service_provider,
1103+
)
1104+
return use_case.execute()
1105+
10651106

10661107
class AnnotationManager(BaseManager):
10671108
def __init__(self, service_provider: ServiceProvider, config: ConfigEntity):

src/superannotate/lib/infrastructure/services/item.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def delete_multiple(self, project: entities.ProjectEntity, item_ids: List[int]):
217217
)
218218

219219
def bulk_attach_categories(
220-
self, project_id: int, folder_id: int, item_category_map: Dict[int, int]
220+
self, project_id: int, folder_id: int, item_id_category_id_map: Dict[int, int]
221221
) -> bool:
222222
params = {"project_id": project_id, "folder_id": folder_id}
223223
response = self.client.request(
@@ -226,10 +226,25 @@ def bulk_attach_categories(
226226
params=params,
227227
data={
228228
"bulk": [
229-
{"item_id": item_id, "categories": [category]}
230-
for item_id, category in item_category_map.items()
229+
{"item_id": item_id, "categories": [category_id]}
230+
for item_id, category_id in item_id_category_id_map.items()
231231
]
232232
},
233233
)
234234
response.raise_for_status()
235-
return response.ok
235+
return response
236+
237+
def bulk_detach_categories(
238+
self, project_id: int, folder_id: int, item_ids: List[int]
239+
) -> bool:
240+
params = {"project_id": project_id, "folder_id": folder_id}
241+
response = self.client.request(
242+
self.URL_ATTACH_CATEGORIES,
243+
"post",
244+
params=params,
245+
data={
246+
"bulk": [{"item_id": item_id, "categories": []} for item_id in item_ids]
247+
},
248+
)
249+
response.raise_for_status()
250+
return response

0 commit comments

Comments
 (0)