Skip to content

Commit a281171

Browse files
authored
Merge pull request #349 from superannotateai/friday
Friday
2 parents 61202a6 + 32d9257 commit a281171

File tree

15 files changed

+284
-71
lines changed

15 files changed

+284
-71
lines changed

docs/source/superannotate.sdk.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Team contributors
108108
_________________
109109

110110
.. autofunction:: superannotate.get_team_metadata
111-
.. autofunction:: superannotate.invite_contributor_to_team
111+
.. autofunction:: superannotate.invite_contributors_to_team
112112
.. autofunction:: superannotate.search_team_contributors
113113

114114
----------

docs/source/tutorial.sdk.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ A team contributor can be invited to the team with:
388388

389389
.. code-block:: python
390390
391-
sa.invite_contributor_to_team(email="hovnatan@superannotate.com", admin=False)
391+
sa.invite_contributors_to_team(emails=["hovnatan@superannotate.com"], admin=False)
392392
393393
394394
This invitation should be accepted by the contributor. After which, to share the

src/superannotate/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
add_annotation_comment_to_image,
1818
)
1919
from superannotate.lib.app.interface.sdk_interface import add_annotation_point_to_image
20+
from superannotate.lib.app.interface.sdk_interface import add_contributors_to_project
2021
from superannotate.lib.app.interface.sdk_interface import aggregate_annotations_as_df
2122
from superannotate.lib.app.interface.sdk_interface import assign_folder
2223
from superannotate.lib.app.interface.sdk_interface import assign_images
@@ -63,7 +64,7 @@
6364
from superannotate.lib.app.interface.sdk_interface import get_project_workflow
6465
from superannotate.lib.app.interface.sdk_interface import get_team_metadata
6566
from superannotate.lib.app.interface.sdk_interface import init
66-
from superannotate.lib.app.interface.sdk_interface import invite_contributor_to_team
67+
from superannotate.lib.app.interface.sdk_interface import invite_contributors_to_team
6768
from superannotate.lib.app.interface.sdk_interface import move_images
6869
from superannotate.lib.app.interface.sdk_interface import pin_image
6970
from superannotate.lib.app.interface.sdk_interface import prepare_export
@@ -128,7 +129,6 @@
128129
"convert_project_type",
129130
# Teams Section
130131
"get_team_metadata",
131-
"invite_contributor_to_team",
132132
"search_team_contributors",
133133
# Projects Section
134134
"create_project_from_metadata",
@@ -199,6 +199,8 @@
199199
"consensus",
200200
"upload_video_to_project",
201201
"upload_images_to_project",
202+
"add_contributors_to_project",
203+
"invite_contributors_to_team",
202204
]
203205

204206
__author__ = "Superannotate"

src/superannotate/lib/app/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get_s3_annotation_paths(folder_path, s3_bucket, annotation_paths, recursive)
8787

8888
paginator = s3_client.get_paginator("list_objects_v2")
8989
for data in paginator.paginate(Bucket=s3_bucket, Prefix=folder_path):
90-
for annotation in data["Contents"]:
90+
for annotation in data.get("Contents", []):
9191
key = annotation["Key"]
9292
if (
9393
key.endswith(VECTOR_ANNOTATION_POSTFIX)

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

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from lib.app.helpers import get_paths_and_duplicated_from_csv
2121
from lib.app.interface.types import AnnotationStatuses
2222
from lib.app.interface.types import AnnotationType
23+
from lib.app.interface.types import AnnotatorRole
2324
from lib.app.interface.types import ImageQualityChoices
2425
from lib.app.interface.types import NotEmptyStr
2526
from lib.app.interface.types import ProjectTypes
@@ -80,19 +81,6 @@ def get_team_metadata():
8081
return TeamSerializer(response.data).serialize()
8182

8283

83-
@Trackable
84-
@validate_arguments
85-
def invite_contributor_to_team(email: EmailStr, admin: bool = False):
86-
"""Invites a contributor to team
87-
88-
:param email: email of the contributor
89-
:type email: str
90-
:param admin: enables admin priviledges for the contributor
91-
:type admin: bool
92-
"""
93-
controller.invite_contributor(email, is_admin=admin)
94-
95-
9684
@Trackable
9785
@validate_arguments
9886
def search_team_contributors(
@@ -2911,3 +2899,50 @@ def validate_annotations(
29112899
return True
29122900
print(response.report)
29132901
return False
2902+
2903+
2904+
@Trackable
2905+
@validate_arguments
2906+
def add_contributors_to_project(
2907+
project_name: NotEmptyStr, emails: List[EmailStr], role: AnnotatorRole
2908+
) -> Tuple[List[str], List[str]]:
2909+
"""Add contributors to project.
2910+
2911+
:param project_name: project name
2912+
:type project_name: str
2913+
2914+
:param emails: users email
2915+
:type emails: list
2916+
2917+
:param role: user role to apply, one of Admin , Annotator , QA
2918+
:type role: str
2919+
2920+
return: lists of added, skipped contributors of the project
2921+
rtype: tuple (2 members) of lists of strs
2922+
"""
2923+
response = controller.add_contributors_to_project(
2924+
project_name=project_name, emails=emails, role=role
2925+
)
2926+
if response.errors:
2927+
raise AppException(response.errors)
2928+
return response.data
2929+
2930+
2931+
@Trackable
2932+
@validate_arguments
2933+
def invite_contributors_to_team(emails: List[EmailStr], admin: StrictBool = False) -> Tuple[List[str], List[str]]:
2934+
"""Invites contributors to the team.
2935+
2936+
:param emails: list of contributor emails
2937+
:type emails: list
2938+
2939+
:param admin: enables admin privileges for the contributor
2940+
:type admin: bool
2941+
2942+
return: lists of invited, skipped contributors of the team
2943+
rtype: tuple (2 members) of lists of strs
2944+
"""
2945+
response = controller.invite_contributors_to_team(emails=emails, set_admin=admin)
2946+
if response.errors:
2947+
raise AppException(response.errors)
2948+
return response.data

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from lib.core.enums import AnnotationStatus
55
from lib.core.enums import ProjectType
6+
from lib.core.enums import UserRole
67
from lib.core.exceptions import AppException
78
from lib.infrastructure.validators import wrap_error
89
from pydantic import constr
@@ -25,6 +26,20 @@ def validate(cls, value: Union[str]) -> Union[str]:
2526
return value
2627

2728

29+
class AnnotatorRole(StrictStr):
30+
ANNOTATOR_ROLES = (UserRole.ADMIN.name, UserRole.ANNOTATOR.name, UserRole.QA.name)
31+
32+
@classmethod
33+
def validate(cls, value: Union[str]) -> Union[str]:
34+
if cls.curtail_length and len(value) > cls.curtail_length:
35+
value = value[: cls.curtail_length]
36+
if value.lower() not in [role.lower() for role in cls.ANNOTATOR_ROLES]:
37+
raise TypeError(
38+
f"Available statuses is {', '.join(AnnotatorRole)}. "
39+
)
40+
return value
41+
42+
2843
class AnnotationType(StrictStr):
2944
VALID_TYPES = ["bbox", "polygon", "point"]
3045

src/superannotate/lib/app/mixp/utils/parsers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ def get_team_metadata(*args, **kwargs):
2222
return {"event_name": "get_team_metadata", "properties": {}}
2323

2424

25-
def invite_contributor_to_team(*args, **kwargs):
25+
def invite_contributors_to_team(*args, **kwargs):
2626
admin = kwargs.get("admin", None)
2727
if not admin:
2828
admin = args[1:2]
2929
if admin:
3030
admin = "CUSTOM"
3131
else:
3232
admin = "DEFAULT"
33-
return {"event_name": "invite_contributor_to_team", "properties": {"Admin": admin}}
33+
return {"event_name": "invite_contributors_to_team", "properties": {"Admin": admin}}
3434

3535

3636
def search_team_contributors(*args, **kwargs):

src/superannotate/lib/core/enums.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class ProjectType(BaseTitledEnum):
4040

4141

4242
class UserRole(BaseTitledEnum):
43-
SUPER_AMIN = "Superadmin", 1
43+
SUPER_ADMIN = "Superadmin", 1
4444
ADMIN = "Admin", 2
4545
ANNOTATOR = "Annotator", 3
4646
QA = "QA", 4

src/superannotate/lib/core/serviceproviders.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ def get_annotation_classes(
4242
raise NotImplementedError
4343

4444
@abstractmethod
45-
def share_project_bulk(self, project_id: int, team_id: int, users: list):
45+
def share_project_bulk(self, project_id: int, team_id: int, users: Iterable):
46+
raise NotImplementedError
47+
48+
@abstractmethod
49+
def invite_contributors(self, team_id: int, team_role: int, emails: Iterable) -> bool:
4650
raise NotImplementedError
4751

4852
@abstractmethod
@@ -57,10 +61,6 @@ def prepare_export(
5761
):
5862
raise NotImplementedError
5963

60-
@abstractmethod
61-
def invite_contributor(self, team_id: int, email: str, user_role: str):
62-
raise NotImplementedError
63-
6464
@abstractmethod
6565
def delete_team_invitation(self, team_id: int, token: str, email: str):
6666
raise NotImplementedError

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

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from lib.core.entities import FolderEntity
1313
from lib.core.entities import ProjectEntity
1414
from lib.core.entities import ProjectSettingEntity
15+
from lib.core.entities import TeamEntity
1516
from lib.core.entities import WorkflowEntity
1617
from lib.core.exceptions import AppException
1718
from lib.core.exceptions import AppValidationException
@@ -915,31 +916,6 @@ def execute(self):
915916
return self._response
916917

917918

918-
class InviteContributorUseCase(BaseUseCase):
919-
def __init__(
920-
self,
921-
backend_service_provider: SuerannotateServiceProvider,
922-
email: str,
923-
team_id: int,
924-
is_admin: bool = False,
925-
):
926-
super().__init__()
927-
self._backend_service = backend_service_provider
928-
self._email = email
929-
self._team_id = team_id
930-
self._is_admin = is_admin
931-
932-
def execute(self):
933-
role = (
934-
constances.UserRole.ADMIN.value
935-
if self._is_admin
936-
else constances.UserRole.ANNOTATOR.value
937-
)
938-
self._backend_service.invite_contributor(
939-
team_id=self._team_id, email=self._email, user_role=role
940-
)
941-
942-
943919
class SearchContributorsUseCase(BaseUseCase):
944920
def __init__(
945921
self,
@@ -963,3 +939,122 @@ def execute(self):
963939
)
964940
self._response.data = res
965941
return self._response
942+
943+
944+
class AddContributorsToProject(BaseReportableUseCae):
945+
"""
946+
Returns tuple of lists (added, skipped)
947+
"""
948+
949+
def __init__(
950+
self,
951+
reporter: Reporter,
952+
team: TeamEntity,
953+
project: ProjectEntity,
954+
emails: list,
955+
role: str,
956+
service: SuerannotateServiceProvider,
957+
):
958+
super().__init__(reporter)
959+
self._team = team
960+
self._project = project
961+
self._emails = emails
962+
self._role = role
963+
self._service = service
964+
965+
@property
966+
def user_role(self):
967+
return constances.UserRole.get_value(self._role)
968+
969+
def execute(self):
970+
team_users = set()
971+
project_users = {user["user_id"] for user in self._project.users}
972+
for user in self._team.users:
973+
if user.user_role > constances.UserRole.ADMIN.value:
974+
team_users.add(user.email)
975+
# collecting pending team users which is not admin
976+
for user in self._team.pending_invitations:
977+
if user["user_role"] > constances.UserRole.ADMIN.value:
978+
team_users.add(user["email"])
979+
# collecting pending project users which is not admin
980+
for user in self._project.unverified_users:
981+
if user["user_role"] > constances.UserRole.ADMIN.value:
982+
project_users.add(user["email"])
983+
984+
to_add = team_users.intersection(self._emails) - project_users
985+
to_skip = set(self._emails).difference(to_add)
986+
987+
if to_skip:
988+
self.reporter.log_warning(
989+
f"Skipped {len(to_skip)}/{len(self._emails)} "
990+
"contributors that are out of the team scope or already have access to the project."
991+
)
992+
if to_add:
993+
response = self._service.share_project_bulk(
994+
team_id=self._team.uuid,
995+
project_id=self._project.uuid,
996+
users=[
997+
dict(user_id=user_id, user_role=self.user_role)
998+
for user_id in to_add
999+
],
1000+
)
1001+
if response and not response.get("invalidUsers"):
1002+
self._response.data = to_add, to_skip
1003+
self.reporter.log_info(
1004+
f"Added {len(to_add)}/{len(self._emails)} "
1005+
"contributors to the project <project_name> with the <role> role."
1006+
)
1007+
return self._response
1008+
1009+
1010+
class InviteContributorsToTeam(BaseReportableUseCae):
1011+
"""
1012+
Returns tuple of lists (added, skipped)
1013+
"""
1014+
1015+
def __init__(
1016+
self,
1017+
reporter: Reporter,
1018+
team: TeamEntity,
1019+
emails: list,
1020+
set_admin: bool,
1021+
service: SuerannotateServiceProvider,
1022+
):
1023+
super().__init__(reporter)
1024+
self._team = team
1025+
self._emails = emails
1026+
self._set_admin = set_admin
1027+
self._service = service
1028+
1029+
def execute(self):
1030+
team_users = set()
1031+
for user in self._team.users:
1032+
if user.user_role > constances.UserRole.ADMIN.value:
1033+
team_users.add(user.email)
1034+
# collecting pending team users which is not admin
1035+
for user in self._team.pending_invitations:
1036+
if user["user_role"] > constances.UserRole.ADMIN.value:
1037+
team_users.add(user["email"])
1038+
1039+
emails = set(self._emails)
1040+
1041+
to_skip = emails.intersection(team_users)
1042+
to_add = emails.difference(to_skip)
1043+
1044+
if to_skip:
1045+
self.reporter.log_warning(
1046+
f"Found {len(to_skip)}/{len(self._emails)} existing members of the team."
1047+
)
1048+
if to_add:
1049+
response = self._service.invite_contributors(
1050+
team_id=self._team.uuid,
1051+
# REMINDER UserRole.VIEWER is the contributor for the teams
1052+
team_role=constances.UserRole.ADMIN.value if self._set_admin else constances.UserRole.VIEWER.value,
1053+
emails=to_add
1054+
)
1055+
if response:
1056+
self._response.data = to_add, to_skip
1057+
self.reporter.log_info(
1058+
f"Sent team <admin/contributor> invitations to {len(to_add)}/{len(self._emails)}."
1059+
)
1060+
return self._response

0 commit comments

Comments
 (0)