Skip to content

Commit 38d8106

Browse files
Narek MkhitaryanNarek Mkhitaryan
authored andcommitted
added get/set_user_scores
1 parent 24741d2 commit 38d8106

File tree

14 files changed

+836
-3
lines changed

14 files changed

+836
-3
lines changed

docs/source/api_reference/api_team.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ Team
1212
.. automethod:: superannotate.SAClient.list_users
1313
.. automethod:: superannotate.SAClient.pause_user_activity
1414
.. automethod:: superannotate.SAClient.resume_user_activity
15+
.. automethod:: superannotate.SAClient.get_user_scores
16+
.. automethod:: superannotate.SAClient.set_user_scores

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ minversion = 3.7
33
log_cli=true
44
python_files = test_*.py
55
;pytest_plugins = ['pytest_profiling']
6-
addopts = -n 4 --dist loadscope
6+
;addopts = -n 4 --dist loadscope

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

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,166 @@ def resume_user_activity(
608608
f"User with email {user.email} has been successfully unblocked from the specified projects: {projects}."
609609
)
610610

611+
def get_user_scores(
612+
self,
613+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
614+
item: Union[NotEmptyStr, int],
615+
scored_user: NotEmptyStr,
616+
*,
617+
score_names: Optional[List[str]] = None,
618+
) -> List[Dict[str, Any]]:
619+
"""
620+
Retrieve score metadata for a user for a specific item in a specific project.
621+
622+
:param project: Project and folder as a tuple, folder is optional.
623+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
624+
625+
:param item: The unique ID or name of the item.
626+
:type item: Union[str, int]
627+
628+
:param scored_user: The email address of the project user.
629+
:type scored_user: str
630+
631+
:param score_names: A list of score names to filter by. If None, returns all scores.
632+
:type score_names: Optional[List[str]]
633+
634+
:return: A list of dictionaries containing score metadata for the user.
635+
:rtype: list of dicts
636+
637+
Request Example:
638+
::
639+
640+
client.get_user_scores(
641+
project=("my_multimodal", "folder1"),
642+
item="item1",
643+
scored_user="example@superannotate.com",
644+
score_names=["Accuracy Score", "Speed"]
645+
)
646+
647+
Response Example:
648+
::
649+
650+
[
651+
{
652+
"createdAt": "2024-06-01T13:00:00",
653+
"updatedAt": "2024-06-01T13:00:00",
654+
"id": 3217575,
655+
"name": "Accuracy Score",
656+
"value": 98,
657+
"weight": 4,
658+
},
659+
{
660+
"createdAt": "2024-06-01T13:00:00",
661+
"updatedAt": "2024-06-01T13:00:00",
662+
"id": 5657575,
663+
"name": "Speed",
664+
"value": 9,
665+
"weight": 0.4,
666+
},
667+
]
668+
"""
669+
670+
if isinstance(project, str):
671+
project_name, folder_name = extract_project_folder(project)
672+
project, folder = self.controller.get_project_folder(
673+
project_name, folder_name
674+
)
675+
else:
676+
project_pk, folder_pk = project
677+
if isinstance(project_pk, int) and isinstance(folder_pk, int):
678+
project = self.controller.get_project_by_id(project_pk).data
679+
folder = self.controller.get_folder_by_id(folder_pk, project.id).data
680+
elif isinstance(project_pk, str) and isinstance(folder_pk, str):
681+
project = self.controller.get_project(project_pk)
682+
folder = self.controller.get_folder(project, folder_pk)
683+
else:
684+
raise AppException("Provided project param is not valid.")
685+
response = BaseSerializer.serialize_iterable(
686+
self.controller.work_management.get_user_scores(
687+
project=project,
688+
folder=folder,
689+
item=item,
690+
scored_user=scored_user,
691+
provided_score_names=score_names,
692+
)
693+
)
694+
return response
695+
696+
def set_user_scores(
697+
self,
698+
project: Union[NotEmptyStr, Tuple[int, int], Tuple[str, str]],
699+
item: Union[NotEmptyStr, int],
700+
scored_user: NotEmptyStr,
701+
scores: List[Dict[str, Any]],
702+
):
703+
"""
704+
Assign score metadata for a user in a scoring component.
705+
706+
:param project: Project and folder as a tuple, folder is optional.
707+
:type project: Union[str, Tuple[int, int], Tuple[str, str]]
708+
709+
:param item: The unique ID or name of the item.
710+
:type item: Union[str, int]
711+
712+
:param scored_user: Set the email of the user being scored.
713+
:type scored_user: str
714+
715+
:param scores: A list of dictionaries containing the following key-value pairs:
716+
* **name** (*str*): The name of the score (required).
717+
* **value** (*Any*): The score value (required).
718+
* **weight** (*Union[float, int]*, optional): The weight of the score. Defaults to `1` if not provided.
719+
720+
**Example**:
721+
::
722+
723+
scores = [
724+
{
725+
"name": "Speed", # str (required)
726+
"value": 90, # Any (required)
727+
"weight": 1 # Union[float, int] (optional, defaults to 1.0 if not provided)
728+
}
729+
]
730+
:type scores: List[Dict[str, Any]
731+
732+
Request Example:
733+
::
734+
735+
client.set_user_scores(
736+
project=("my_multimodal", "folder1"),
737+
item_=12345,
738+
scored_user="example@superannotate.com",
739+
scores=[
740+
{"name": "Speed", "value": 90},
741+
{"name": "Accuracy", "value": 9, "weight": 4.0},
742+
{"name": "Attention to Detail", "value": None, "weight": None},
743+
]
744+
)
745+
746+
"""
747+
if isinstance(project, str):
748+
project_name, folder_name = extract_project_folder(project)
749+
project, folder = self.controller.get_project_folder(
750+
project_name, folder_name
751+
)
752+
else:
753+
project_pk, folder_pk = project
754+
if isinstance(project_pk, int) and isinstance(folder_pk, int):
755+
project = self.controller.get_project_by_id(project_pk).data
756+
folder = self.controller.get_folder_by_id(folder_pk, project.id).data
757+
elif isinstance(project_pk, str) and isinstance(folder_pk, str):
758+
project = self.controller.get_project(project_pk)
759+
folder = self.controller.get_folder(project, folder_pk)
760+
else:
761+
raise AppException("Provided project param is not valid.")
762+
self.controller.work_management.set_user_scores(
763+
project=project,
764+
folder=folder,
765+
item=item,
766+
scored_user=scored_user,
767+
scores=scores,
768+
)
769+
logger.info("Scores successfully set.")
770+
611771
def get_component_config(self, project: Union[NotEmptyStr, int], component_id: str):
612772
"""
613773
Retrieves the configuration for a given project and component ID.

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import datetime
22
from enum import auto
33
from enum import Enum
4+
from typing import Any
45
from typing import Optional
6+
from typing import Union
57

68
from lib.core.entities.base import TimedBaseModel
79
from lib.core.enums import WMUserStateEnum
10+
from lib.core.exceptions import AppException
11+
from lib.core.pydantic_v1 import BaseModel
812
from lib.core.pydantic_v1 import Extra
913
from lib.core.pydantic_v1 import Field
1014
from lib.core.pydantic_v1 import parse_datetime
15+
from lib.core.pydantic_v1 import root_validator
1116
from lib.core.pydantic_v1 import validator
1217

1318

@@ -149,3 +154,55 @@ def json(self, **kwargs):
149154
if "exclude" not in kwargs:
150155
kwargs["exclude"] = {"custom_fields"}
151156
return super().json(**kwargs)
157+
158+
159+
class WMScoreEntity(TimedBaseModel):
160+
id: int
161+
team_id: int
162+
name: str
163+
description: Optional[str]
164+
type: str
165+
payload: Optional[dict]
166+
167+
168+
class TelemetryScoreEntity(BaseModel):
169+
item_id: int
170+
team_id: int
171+
project_id: int
172+
user_id: str
173+
user_role: str
174+
score_id: int
175+
value: Optional[Any]
176+
weight: Optional[float]
177+
178+
179+
class ScoreEntity(TimedBaseModel):
180+
id: int
181+
name: str
182+
value: Optional[Any]
183+
weight: Optional[float]
184+
185+
186+
class ScorePayloadEntity(BaseModel):
187+
name: str
188+
value: Any
189+
weight: Optional[Union[float, int]] = 1.0
190+
191+
class Config:
192+
extra = Extra.forbid
193+
194+
@validator("weight", pre=True, always=True)
195+
def validate_weight(cls, v):
196+
if v is not None and (not isinstance(v, (int, float)) or v <= 0):
197+
raise AppException("Please provide a valid number greater than 0")
198+
return v
199+
200+
@root_validator(pre=True)
201+
def check_weight_and_value(cls, values):
202+
value = values.get("value")
203+
weight = values.get("weight")
204+
if (weight is None and value is not None) or (
205+
weight is not None and value is None
206+
):
207+
raise AppException("Weight and Value must both be set or both be None.")
208+
return values

src/superannotate/lib/core/pydantic_v1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ROOT_KEY = pydantic.utils.ROOT_KEY # noqa
2323
sequence_like = pydantic.utils.sequence_like # noqa
2424
validator = pydantic.validator # noqa
25+
root_validator = pydantic.root_validator # noqa
2526
constr = pydantic.constr # noqa
2627
conlist = pydantic.conlist # noqa
2728
parse_datetime = pydantic.datetime_parse.parse_datetime # noqa

src/superannotate/lib/core/service_types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from typing import Union
66

77
from lib.core import entities
8+
from lib.core.entities.work_managament import TelemetryScoreEntity
89
from lib.core.entities.work_managament import WMProjectEntity
10+
from lib.core.entities.work_managament import WMScoreEntity
911
from lib.core.entities.work_managament import WMUserEntity
1012
from lib.core.enums import ProjectType
1113
from lib.core.exceptions import AppException
@@ -260,6 +262,14 @@ class SettingsListResponse(ServiceResponse):
260262
res_data: List[entities.SettingEntity] = None
261263

262264

265+
class WMScoreListResponse(ServiceResponse):
266+
res_data: List[WMScoreEntity] = None
267+
268+
269+
class TelemetryScoreListResponse(ServiceResponse):
270+
res_data: List[TelemetryScoreEntity] = None
271+
272+
263273
PROJECT_TYPE_RESPONSE_MAP = {
264274
ProjectType.VECTOR: ImageResponse,
265275
ProjectType.OTHER: ClassificationResponse,

src/superannotate/lib/core/serviceproviders.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from lib.core.service_types import UserResponse
3030
from lib.core.service_types import WMCustomFieldResponse
3131
from lib.core.service_types import WMProjectListResponse
32+
from lib.core.service_types import WMScoreListResponse
3233
from lib.core.service_types import WMUserListResponse
3334
from lib.core.service_types import WorkflowListResponse
3435
from lib.core.types import Attachment
@@ -197,6 +198,24 @@ def update_user_activity(
197198
) -> ServiceResponse:
198199
raise NotImplementedError
199200

201+
@abstractmethod
202+
def list_scores(self) -> WMScoreListResponse:
203+
raise NotImplementedError
204+
205+
@abstractmethod
206+
def create_score(
207+
self,
208+
name: str,
209+
description: Optional[str],
210+
score_type: Literal["rating", "number", "radio"],
211+
payload: dict,
212+
) -> ServiceResponse:
213+
raise NotImplementedError
214+
215+
@abstractmethod
216+
def delete_score(self, score_id: int) -> ServiceResponse:
217+
raise NotImplementedError
218+
200219

201220
class BaseProjectService(SuperannotateServiceProvider):
202221
@abstractmethod
@@ -688,6 +707,20 @@ def query_item_count(
688707
raise NotImplementedError
689708

690709

710+
class BaseTelemetryScoringService(SuperannotateServiceProvider):
711+
@abstractmethod
712+
def get_score_values(self, project_id: int, item_id: int, user_id: str):
713+
raise NotImplementedError
714+
715+
@abstractmethod
716+
def set_score_values(
717+
self,
718+
project_id: int,
719+
data: List[dict],
720+
) -> ServiceResponse:
721+
raise NotImplementedError
722+
723+
691724
class BaseServiceProvider:
692725
projects: BaseProjectService
693726
folders: BaseFolderService

0 commit comments

Comments
 (0)