Skip to content

Commit 10ec165

Browse files
authored
Merge pull request #785 from superannotateai/FRIDAY_3640
Add generate_items method
2 parents 1bffede + f114078 commit 10ec165

File tree

8 files changed

+208
-7
lines changed

8 files changed

+208
-7
lines changed

docs/source/api_reference/api_item.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Items
88
.. automethod:: superannotate.SAClient.list_items
99
.. automethod:: superannotate.SAClient.search_items
1010
.. automethod:: superannotate.SAClient.attach_items
11+
.. automethod:: superannotate.SAClient.generate_items
1112
.. automethod:: superannotate.SAClient.item_context
1213
.. autoclass:: superannotate.ItemContext
1314
:members: get_metadata, get_component_value, set_component_value

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,21 @@ def get_item_by_id(self, project_id: int, item_id: int):
354354

355355
return BaseSerializer(item).serialize(exclude={"url", "meta"})
356356

357-
def get_team_metadata(self):
358-
"""Returns team metadata
357+
def get_team_metadata(self, include: List[Literal["scores"]] = None):
358+
"""
359+
Returns team metadata, including optionally, scores
359360
360361
:return: team metadata
361362
:rtype: dict
362363
"""
363364
response = self.controller.get_team()
364-
return TeamSerializer(response.data).serialize()
365+
team = response.data
366+
if include and "scores" in include:
367+
team.scores = [
368+
i.name
369+
for i in self.controller.work_management.list_score_templates().data
370+
]
371+
return TeamSerializer(team).serialize(exclude_unset=True)
365372

366373
def get_user_metadata(
367374
self, pk: Union[int, str], include: List[Literal["custom_fields"]] = None
@@ -4014,6 +4021,35 @@ def attach_items(
40144021
]
40154022
return uploaded, fails, duplicated
40164023

4024+
def generate_items(
4025+
self,
4026+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
4027+
count: int,
4028+
name: str,
4029+
):
4030+
"""
4031+
Generate multiple items in a specific project and folder.`
4032+
If there are no items in the folder, it will generate a blank item otherwise, it will generate items based on the Custom Form.
4033+
4034+
:param project: Project and folder as a tuple, folder is optional.
4035+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
4036+
4037+
:param count: the count of items to generate
4038+
:type count: int
4039+
4040+
:param name: the name of the item. After generating the items,
4041+
the item names will contain the provided name and a numeric suffix based on the item count.
4042+
:type name: str
4043+
"""
4044+
project, folder = self.controller.get_project_folder(project)
4045+
4046+
response = self.controller.items.generate_items(
4047+
project=project, folder=folder, count=count, name=name
4048+
)
4049+
if response.errors:
4050+
raise AppException(response.errors)
4051+
logger.info(f"{response.data} items successfully generated.")
4052+
40174053
def copy_items(
40184054
self,
40194055
source: Union[NotEmptyStr, dict],

src/superannotate/lib/core/entities/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class TeamEntity(BaseModel):
180180
pending_invitations: Optional[List[Any]]
181181
creator_id: Optional[str]
182182
owner_id: Optional[str]
183+
scores: Optional[List[str]]
183184

184185
class Config:
185186
extra = Extra.ignore

src/superannotate/lib/core/serviceproviders.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,9 @@ def attach(
399399
project: entities.ProjectEntity,
400400
folder: entities.FolderEntity,
401401
attachments: List[Attachment],
402-
annotation_status_code,
403402
upload_state_code,
404-
meta: Dict[str, AttachmentMeta],
403+
annotation_status_code=None,
404+
meta: Dict[str, AttachmentMeta] = None,
405405
) -> ServiceResponse:
406406
raise NotImplementedError
407407

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
2+
import re
23
import traceback
34
from collections import defaultdict
45
from concurrent.futures import as_completed
56
from concurrent.futures import ThreadPoolExecutor
67
from typing import Dict
8+
from typing import Generator
79
from typing import List
810
from typing import Union
911

@@ -386,6 +388,88 @@ def execute(self) -> Response:
386388
return self._response
387389

388390

391+
class GenerateItems(BaseReportableUseCase):
392+
CHUNK_SIZE = 500
393+
INVALID_CHARS_PATTERN = re.compile(r"[<>:\"'/\\|?*&$!+]")
394+
395+
def __init__(
396+
self,
397+
reporter: Reporter,
398+
project: ProjectEntity,
399+
folder: FolderEntity,
400+
name_prefix: str,
401+
count: int,
402+
service_provider: BaseServiceProvider,
403+
):
404+
super().__init__(reporter)
405+
self._project = project
406+
self._folder = folder
407+
self._name_prefix = name_prefix
408+
self._count = count
409+
self._service_provider = service_provider
410+
411+
def validate_name(self):
412+
if (
413+
len(self._name_prefix) > 114
414+
or self.INVALID_CHARS_PATTERN.search(self._name_prefix) is not None
415+
):
416+
raise AppException("Invalid item name.")
417+
418+
def validate_limitations(self):
419+
response = self._service_provider.get_limitations(
420+
project=self._project, folder=self._folder
421+
)
422+
if not response.ok:
423+
raise AppValidationException(response.error)
424+
if self._count > response.data.folder_limit.remaining_image_count:
425+
raise AppValidationException(constants.ATTACH_FOLDER_LIMIT_ERROR_MESSAGE)
426+
if self._count > response.data.project_limit.remaining_image_count:
427+
raise AppValidationException(constants.ATTACH_PROJECT_LIMIT_ERROR_MESSAGE)
428+
if (
429+
response.data.user_limit
430+
and self._count > response.data.user_limit.remaining_image_count
431+
):
432+
raise AppValidationException(constants.ATTACH_USER_LIMIT_ERROR_MESSAGE)
433+
434+
def validate_project_type(self):
435+
if self._project.type != constants.ProjectType.MULTIMODAL:
436+
raise AppException(
437+
"This function is only supported for Multimodal projects."
438+
)
439+
440+
@staticmethod
441+
def generate_attachments(
442+
name: str, start: int, end: int, chunk_size: int
443+
) -> Generator[List[Attachment], None, None]:
444+
chunk = []
445+
for i in range(start, end + 1):
446+
chunk.append(Attachment(name=f"{name}_{i:05d}", path="custom_llm"))
447+
if len(chunk) == chunk_size:
448+
yield chunk
449+
chunk = []
450+
if chunk:
451+
yield chunk
452+
453+
def execute(self) -> Response:
454+
if self.is_valid():
455+
attached_items_count = 0
456+
for chunk in self.generate_attachments(
457+
self._name_prefix, start=1, end=self._count, chunk_size=self.CHUNK_SIZE
458+
):
459+
backend_response = self._service_provider.items.attach(
460+
project=self._project,
461+
folder=self._folder,
462+
attachments=chunk,
463+
upload_state_code=3,
464+
)
465+
if not backend_response.ok:
466+
self._response.errors = AppException(backend_response.error)
467+
return self._response
468+
attached_items_count += len(chunk)
469+
self._response.data = attached_items_count
470+
return self._response
471+
472+
389473
class CopyItems(BaseReportableUseCase):
390474
"""
391475
Copy items in bulk between folders in a project.

src/superannotate/lib/infrastructure/controller.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ def __init__(self, service_provider: ServiceProvider):
127127

128128

129129
class WorkManagementManager(BaseManager):
130+
def list_score_templates(self):
131+
return self.service_provider.work_management.list_scores()
132+
130133
def get_user_metadata(
131134
self, pk: Union[str, int], include: List[Literal["custom_fields"]] = None
132135
):
@@ -914,6 +917,23 @@ def attach(
914917
)
915918
return use_case.execute()
916919

920+
def generate_items(
921+
self,
922+
project: ProjectEntity,
923+
folder: FolderEntity,
924+
count: int,
925+
name: str,
926+
):
927+
use_case = usecases.GenerateItems(
928+
reporter=Reporter(),
929+
project=project,
930+
folder=folder,
931+
name_prefix=name,
932+
count=count,
933+
service_provider=self.service_provider,
934+
)
935+
return use_case.execute()
936+
917937
def delete(
918938
self,
919939
project: ProjectEntity,

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def attach(
3737
folder: entities.FolderEntity,
3838
attachments: List[Attachment],
3939
upload_state_code,
40-
meta: Dict[str, AttachmentMeta],
40+
meta: Dict[str, AttachmentMeta] = None,
4141
annotation_status_code=None,
4242
):
4343
data = {
@@ -46,8 +46,10 @@ def attach(
4646
"team_id": project.team_id,
4747
"images": [i.dict() for i in attachments],
4848
"upload_state": upload_state_code,
49-
"meta": meta,
49+
"meta": {},
5050
}
51+
if meta:
52+
data["meta"] = meta
5153
if annotation_status_code:
5254
data["annotation_status"] = annotation_status_code
5355
return self.client.request(self.URL_ATTACH, "post", data=data)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from src.superannotate import AppException
2+
from src.superannotate import SAClient
3+
from tests.integration.base import BaseTestCase
4+
5+
sa = SAClient()
6+
7+
8+
class TestGenerateItemsMM(BaseTestCase):
9+
PROJECT_NAME = "TestGenerateItemsMM"
10+
PROJECT_DESCRIPTION = "TestGenerateItemsMM"
11+
PROJECT_TYPE = "Multimodal"
12+
FOLDER_NAME = "test_folder"
13+
14+
def test_generate_items_root(self):
15+
sa.generate_items(self.PROJECT_NAME, 100, name="a")
16+
items = sa.list_items(self.PROJECT_NAME)
17+
18+
assert len(items) == 100
19+
20+
expected_names = {f"a_{i:05d}" for i in range(1, 101)}
21+
actual_names = {item["name"] for item in items}
22+
23+
assert actual_names == expected_names
24+
25+
def test_generate_items_in_folder(self):
26+
path = f"{self.PROJECT_NAME}/{self.FOLDER_NAME}"
27+
28+
sa.create_folder(self.PROJECT_NAME, self.FOLDER_NAME)
29+
sa.generate_items(path, 100, name="a")
30+
items = sa.list_items(project=self.PROJECT_NAME, folder=self.FOLDER_NAME)
31+
32+
assert len(items) == 100
33+
34+
expected_names = {f"a_{i:05d}" for i in range(1, 101)}
35+
actual_names = {item["name"] for item in items}
36+
37+
assert actual_names == expected_names
38+
39+
def test_invalid_name(self):
40+
with self.assertRaisesRegexp(
41+
AppException,
42+
"Invalid item name.",
43+
):
44+
sa.generate_items(self.PROJECT_NAME, 100, name="a" * 115)
45+
46+
with self.assertRaisesRegexp(
47+
AppException,
48+
"Invalid item name.",
49+
):
50+
sa.generate_items(self.PROJECT_NAME, 100, name="m<:")
51+
52+
def test_item_count(self):
53+
with self.assertRaisesRegexp(
54+
AppException,
55+
"The number of items you want to attach exceeds the limit of 50 000 items per folder.",
56+
):
57+
sa.generate_items(self.PROJECT_NAME, 50_001, name="a")

0 commit comments

Comments
 (0)