Skip to content

Commit 5553428

Browse files
authored
Merge pull request #786 from superannotateai/develop
Develop
2 parents 1b7a1e1 + d2413be commit 5553428

File tree

11 files changed

+235
-12
lines changed

11 files changed

+235
-12
lines changed

CHANGELOG.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,29 @@ History
66

77
All release highlights of this project will be documented in this file.
88

9+
10+
4.4.35 - May 2, 2025
11+
____________________
12+
13+
**Added**
14+
15+
- ``SAClient.generate_items`` Generates multiple items in a specified project and folder.
16+
17+
**Updated**
18+
19+
- ``SAClient.get_team_metadata`` Added a new include parameter. When set to "scores", the response includes score names associated with the team user.
20+
21+
922
4.4.34 - April 11, 2025
10-
______________________
23+
_______________________
1124

1225
**Added**
1326

1427
- ``SAClient.get_integrations`` Added id, createdAt, updatedAt, and creator_id in integration metadata.
1528
- ``SAClient.list_workflows`` Retrieves all workflows for your team along with their metadata.
1629

1730
**Updated**
31+
1832
- ``SAClient.get_project_metadata``
1933

2034
**Removed**

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/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44

55

6-
__version__ = "4.4.34"
6+
__version__ = "4.4.35"
77

88

99
os.environ.update({"sa_version": __version__})

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,29 @@ 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.
360+
361+
:param include: Specifies additional fields to include in the response.
362+
363+
Possible values are
364+
365+
- "scores": If provided, the response will include score names associated with the team user.
366+
367+
:type include: list of str, optional
359368
360369
:return: team metadata
361370
:rtype: dict
362371
"""
363372
response = self.controller.get_team()
364-
return TeamSerializer(response.data).serialize()
373+
team = response.data
374+
if include and "scores" in include:
375+
team.scores = [
376+
i.name
377+
for i in self.controller.work_management.list_score_templates().data
378+
]
379+
return TeamSerializer(team).serialize(exclude_unset=True)
365380

366381
def get_user_metadata(
367382
self, pk: Union[int, str], include: List[Literal["custom_fields"]] = None
@@ -4014,6 +4029,35 @@ def attach_items(
40144029
]
40154030
return uploaded, fails, duplicated
40164031

4032+
def generate_items(
4033+
self,
4034+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
4035+
count: int,
4036+
name: str,
4037+
):
4038+
"""
4039+
Generate multiple items in a specific project and folder.
4040+
If there are no items in the folder, it will generate a blank item otherwise, it will generate items based on the Custom Form.
4041+
4042+
:param project: Project and folder as a tuple, folder is optional.
4043+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
4044+
4045+
:param count: the count of items to generate
4046+
:type count: int
4047+
4048+
:param name: the name of the item. After generating the items,
4049+
the item names will contain the provided name and a numeric suffix based on the item count.
4050+
:type name: str
4051+
"""
4052+
project, folder = self.controller.get_project_folder(project)
4053+
4054+
response = self.controller.items.generate_items(
4055+
project=project, folder=folder, count=count, name=name
4056+
)
4057+
if response.errors:
4058+
raise AppException(response.errors)
4059+
logger.info(f"{response.data} items successfully generated.")
4060+
40174061
def copy_items(
40184062
self,
40194063
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/annotation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ async def _sync_large_annotation(
8080
"desired_source": "secondary",
8181
}
8282
if transform_version:
83-
sync_params["transform_version"] = transform_version
83+
sync_params["desired_transform_version"] = transform_version
8484
sync_url = urljoin(
8585
self.get_assets_provider_url(),
8686
self.URL_START_FILE_SYNC.format(item_id=item_id),
@@ -124,14 +124,14 @@ async def get_big_annotation(
124124
"annotation_type": "MAIN",
125125
"version": "V1.00",
126126
}
127-
127+
if transform_version:
128+
query_params["transform_version"] = transform_version
128129
await self._sync_large_annotation(
129130
team_id=project.team_id,
130131
project_id=project.id,
131132
item_id=item.id,
132133
transform_version=transform_version,
133134
)
134-
135135
async with AIOHttpSession(
136136
connector=aiohttp.TCPConnector(ssl=False),
137137
headers=self.client.default_headers,

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)

0 commit comments

Comments
 (0)