Skip to content

Commit c1b6dd6

Browse files
authored
Merge pull request #739 from superannotateai/FRIDAY-3464
Friday 3464
2 parents d342cd9 + 251c3b9 commit c1b6dd6

File tree

23 files changed

+1174
-346
lines changed

23 files changed

+1174
-346
lines changed

docs/source/api_reference/api_team.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Team
77
.. automethod:: superannotate.SAClient.get_integrations
88
.. automethod:: superannotate.SAClient.invite_contributors_to_team
99
.. automethod:: superannotate.SAClient.search_team_contributors
10+
.. automethod:: superannotate.SAClient.get_user_metadata

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

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from lib.core.entities.integrations import IntegrationEntity
5656
from lib.core.entities.integrations import IntegrationTypeEnum
5757
from lib.core.enums import ImageQuality
58+
from lib.core.enums import CustomFieldEntityEnum
5859
from lib.core.enums import ProjectType
5960
from lib.core.enums import ClassTypeEnum
6061
from lib.core.exceptions import AppException
@@ -279,6 +280,79 @@ def get_team_metadata(self):
279280
response = self.controller.get_team()
280281
return TeamSerializer(response.data).serialize()
281282

283+
def get_user_metadata(
284+
self, pk: Union[int, str], include: List[Literal["custom_fields"]] = None
285+
):
286+
"""
287+
Returns user metadata
288+
289+
:param pk: The email address or ID of the team contributor.
290+
:type pk: str or int
291+
292+
:param include: Specifies additional fields to include in the response.
293+
294+
Possible values are
295+
296+
- "custom_fields": whether to include custom fields that have been created for the team user.
297+
298+
:type include: list of str, optional
299+
300+
:return: metadata of team contributor
301+
:rtype: dict
302+
"""
303+
user = self.controller.work_management.get_user_metadata(pk=pk, include=include)
304+
return BaseSerializer(user).serialize(by_alias=False)
305+
306+
def set_user_custom_field(
307+
self, pk: Union[int, str], custom_field_name: str, value: Any
308+
):
309+
"""
310+
Set the custom field for team contributors.
311+
312+
:param pk: The email address or ID of the team contributor.
313+
:type pk: str or int
314+
315+
:param custom_field_name: the name of the custom field created for the team contributor,
316+
used to set or update its value.
317+
318+
:type custom_field_name: str
319+
320+
:param value: The value
321+
:type value: Any
322+
323+
Request Example:
324+
::
325+
from superannotate import SAClient
326+
327+
328+
sa = SAClient()
329+
330+
client.set_user_custom_field(
331+
"example@email.com",
332+
custom_field_name="Due date",
333+
value="Dec 20, 2024"
334+
)
335+
"""
336+
user_id = self.controller.work_management.get_user_metadata(pk=pk).id
337+
self.controller.work_management.set_custom_field_value(
338+
entity_id=user_id,
339+
field_name=custom_field_name,
340+
value=value,
341+
entity=CustomFieldEntityEnum.CONTRIBUTOR,
342+
parent_entity=CustomFieldEntityEnum.TEAM,
343+
)
344+
345+
def list_users(self, *, include: List[Literal["custom_fields"]] = None, **filters):
346+
"""
347+
348+
@param include:
349+
@param filters:
350+
@return:
351+
"""
352+
return BaseSerializer.serialize_iterable(
353+
self.controller.work_management.list_users(include=include, **filters)
354+
)
355+
282356
def get_component_config(self, project: Union[NotEmptyStr, int], component_id: str):
283357
"""
284358
Retrieves the configuration for a given project and component ID.
@@ -2903,7 +2977,7 @@ def list_items(
29032977
project: Union[NotEmptyStr, int],
29042978
folder: Optional[Union[NotEmptyStr, int]] = None,
29052979
*,
2906-
include: List[Literal["custom_metadata", "category"]] = None,
2980+
include: List[Literal["custom_metadata", "categories"]] = None,
29072981
**filters,
29082982
):
29092983
"""
@@ -2923,6 +2997,7 @@ def list_items(
29232997
Possible values are
29242998
29252999
- "custom_metadata": Includes custom metadata attached to the item.
3000+
- "categories": Includes categories attached to the item.
29263001
:type include: list of str, optional
29273002
29283003
:param filters: Specifies filtering criteria (e.g., name, ID, annotation status),
@@ -2993,6 +3068,40 @@ def list_items(
29933068
}
29943069
]
29953070
3071+
Request Example with include categories:
3072+
::
3073+
3074+
client.list_items(
3075+
project="My Multimodal",
3076+
folder="folder1",
3077+
include=["categories"]
3078+
)
3079+
3080+
Response Example:
3081+
::
3082+
3083+
[
3084+
{
3085+
"id": 48909383,
3086+
"name": "scan_123.jpeg",
3087+
"path": "Medical Annotations/folder1",
3088+
"url": "https://sa-public-files.s3.../scan_123.jpeg",
3089+
"annotation_status": "InProgress",
3090+
"createdAt": "2022-02-10T14:32:21.000Z",
3091+
"updatedAt": "2022-02-15T20:46:44.000Z",
3092+
"entropy_value": None,
3093+
"assignments": [],
3094+
"categories": [
3095+
{
3096+
"createdAt": "2025-01-29T13:51:39.000Z",
3097+
"updatedAt": "2025-01-29T13:51:39.000Z",
3098+
"id": 328577,
3099+
"name": "my_category",
3100+
},
3101+
],
3102+
}
3103+
]
3104+
29963105
Additional Filter Examples:
29973106
::
29983107
@@ -3014,6 +3123,14 @@ def list_items(
30143123
if isinstance(project, int)
30153124
else self.controller.get_project(project)
30163125
)
3126+
if (
3127+
include
3128+
and "categories" in include
3129+
and project.type != ProjectType.MULTIMODAL.value
3130+
):
3131+
raise AppException(
3132+
"The 'categories' option in the 'include' field is only supported for Multimodal projects."
3133+
)
30173134
if folder is None:
30183135
folder = self.controller.get_folder(project, "root")
30193136
else:
@@ -3088,6 +3205,7 @@ def list_projects(
30883205
- status__in: List[Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]]
30893206
- status__notin: List[Literal[“NotStarted”, “InProgress”, “Completed”, “OnHold”]]
30903207
- custom_field: Optional[dict] – Specifies custom fields attributes to filter projects by.
3208+
30913209
Custom fields can be accessed using the `custom_field__` prefix followed by the attribute name.
30923210
30933211
:type filters: ProjectFilters

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from lib.core.entities.project import AttachmentEntity
1515
from lib.core.entities.project import CategoryEntity
1616
from lib.core.entities.project import ContributorEntity
17+
from lib.core.entities.project import CustomFieldEntity
1718
from lib.core.entities.project import ProjectEntity
1819
from lib.core.entities.project import SettingEntity
1920
from lib.core.entities.project import StepEntity
@@ -28,6 +29,7 @@
2829
"ConfigEntity",
2930
"SettingEntity",
3031
"SubSetEntity",
32+
"CustomFieldEntity",
3133
# items
3234
"BaseEntity",
3335
"ImageEntity",

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,19 @@ class ProjectFilters(BaseFilters):
3737
status__ne: Literal["NotStarted", "InProgress", "Completed", "OnHold"]
3838
status__in: List[Literal["NotStarted", "InProgress", "Completed", "OnHold"]]
3939
status__notin: List[Literal["NotStarted", "InProgress", "Completed", "OnHold"]]
40+
custom_field: Optional[str] # dummy field to pass first level validation
41+
42+
43+
class UserFilters(TypedDict, total=False):
44+
id: Optional[int]
45+
id__in: Optional[List[int]]
46+
email: Optional[str]
47+
email__in: Optional[List[str]]
48+
email__contains: Optional[str]
49+
email__starts: Optional[str]
50+
email__ends: Optional[str]
51+
state: Optional[str]
52+
state__in: Optional[List[str]]
53+
role: Optional[str]
54+
role__in: Optional[List[str]]
55+
custom_field: Optional[str] # dummy field to pass first level validation

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22

33
from lib.core.entities.base import BaseItemEntity
4+
from lib.core.entities.base import TimedBaseModel
45
from lib.core.enums import ApprovalStatus
56
from lib.core.enums import ProjectType
67
from lib.core.pydantic_v1 import Extra
@@ -16,6 +17,21 @@ class Config:
1617
extra = Extra.ignore
1718

1819

20+
class MultiModalItemCategoryEntity(TimedBaseModel):
21+
id: int = Field(None, alias="category_id")
22+
name: str = Field(None, alias="category_name")
23+
24+
class Config:
25+
extra = Extra.ignore
26+
27+
28+
class MultiModalItemEntity(BaseItemEntity):
29+
categories: Optional[list[MultiModalItemCategoryEntity]]
30+
31+
class Config:
32+
extra = Extra.ignore
33+
34+
1935
class VideoEntity(BaseItemEntity):
2036
approval_status: Optional[ApprovalStatus] = Field(None)
2137

@@ -51,4 +67,5 @@ class Config:
5167
ProjectType.TILED: ImageEntity,
5268
ProjectType.VIDEO: VideoEntity,
5369
ProjectType.DOCUMENT: DocumentEntity,
70+
ProjectType.MULTIMODAL: MultiModalItemEntity,
5471
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ class Config:
170170
extra = Extra.ignore
171171

172172

173+
class CustomFieldEntity(BaseModel):
174+
...
175+
176+
class Config:
177+
extra = Extra.allow
178+
179+
173180
class WorkflowEntity(BaseModel):
174181
id: Optional[int]
175182
name: Optional[str]

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

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
from enum import auto
23
from enum import Enum
34
from typing import Optional
45

@@ -9,14 +10,6 @@
910
from lib.core.pydantic_v1 import validator
1011

1112

12-
class ProjectCustomFieldType(Enum):
13-
Text = 1
14-
MULTI_SELECT = 2
15-
SINGLE_SELECT = 3
16-
DATE_PICKER = 4
17-
NUMERIC = 5
18-
19-
2013
class ProjectType(str, Enum):
2114
Vector = "VECTOR"
2215
Pixel = "PIXEL"
@@ -28,6 +21,25 @@ class ProjectType(str, Enum):
2821
Multimodal = "CUSTOM_LLM"
2922

3023

24+
class WMUserStateEnum(str, Enum):
25+
Pending = "PENDING"
26+
Confirmed = "CONFIRMED"
27+
Video = "PUBLIC_VIDEO"
28+
Document = "PUBLIC_TEXT"
29+
Tiled = "TILED"
30+
Other = "CLASSIFICATION"
31+
PointCloud = "POINT_CLOUD"
32+
Multimodal = "CUSTOM_LLM"
33+
34+
35+
class WMUserTypeEnum(int, Enum):
36+
Contributor = 4
37+
TeamAdmin = 7
38+
TeamOwner = 12
39+
OrganizationAdmin = 15
40+
other = auto()
41+
42+
3143
class ProjectStatus(str, Enum):
3244
Undefined = "undefined"
3345
NotStarted = "notStarted"
@@ -87,3 +99,33 @@ def json(self, **kwargs):
8799
if "exclude" not in kwargs:
88100
kwargs["exclude"] = {"custom_fields"}
89101
return super().json(**kwargs)
102+
103+
104+
class WMUserEntity(TimedBaseModel):
105+
id: Optional[int]
106+
team_id: Optional[int]
107+
role: WMUserTypeEnum
108+
email: Optional[str]
109+
state: Optional[WMUserStateEnum]
110+
custom_fields: Optional[dict] = Field(dict(), alias="customField")
111+
112+
class Config:
113+
extra = Extra.ignore
114+
use_enum_names = True
115+
116+
json_encoders = {
117+
Enum: lambda v: v.value,
118+
datetime.date: lambda v: v.isoformat(),
119+
datetime.datetime: lambda v: v.isoformat(),
120+
}
121+
122+
@validator("custom_fields")
123+
def custom_fields_transformer(cls, v):
124+
if v and "custom_field_values" in v:
125+
return v.get("custom_field_values", {})
126+
return {}
127+
128+
def json(self, **kwargs):
129+
if "exclude" not in kwargs:
130+
kwargs["exclude"] = {"custom_fields"}
131+
return super().json(**kwargs)

src/superannotate/lib/core/enums.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,17 @@ class TrainingStatus(BaseTitledEnum):
185185
FAILED_BEFORE_EVALUATION = "FailedBeforeEvaluation", 4
186186
FAILED_AFTER_EVALUATION = "FailedAfterEvaluation", 5
187187
FAILED_AFTER_EVALUATION_WITH_SAVE_MODEL = "FailedAfterEvaluationWithSavedModel", 6
188+
189+
190+
class CustomFieldEntityEnum(str, Enum):
191+
CONTRIBUTOR = "Contributor"
192+
TEAM = "Team"
193+
PROJECT = "Project"
194+
195+
196+
class CustomFieldType(Enum):
197+
Text = 1
198+
MULTI_SELECT = 2
199+
SINGLE_SELECT = 3
200+
DATE_PICKER = 4
201+
NUMERIC = 5

src/superannotate/lib/core/service_types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from lib.core import entities
88
from lib.core.entities.work_managament import WMProjectEntity
9+
from lib.core.entities.work_managament import WMUserEntity
910
from lib.core.enums import ProjectType
1011
from lib.core.exceptions import AppException
1112
from lib.core.pydantic_v1 import BaseModel
@@ -247,6 +248,14 @@ class WMProjectListResponse(ServiceResponse):
247248
res_data: List[WMProjectEntity] = None
248249

249250

251+
class WMUserListResponse(ServiceResponse):
252+
res_data: List[WMUserEntity] = None
253+
254+
255+
class WMCustomFieldResponse(ServiceResponse):
256+
res_data: List[entities.CustomFieldEntity] = None
257+
258+
250259
class SettingsListResponse(ServiceResponse):
251260
res_data: List[entities.SettingEntity] = None
252261

0 commit comments

Comments
 (0)