Skip to content

Commit fa497da

Browse files
authored
Merge pull request #36 from superannotateai/folder-assign
Folder assign
2 parents 9445e03 + bb68f0f commit fa497da

File tree

4 files changed

+261
-22
lines changed

4 files changed

+261
-22
lines changed

superannotate/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def consensus(*args, **kwargs):
5959
)
6060
from .db.project_images import (
6161
assign_images, copy_image, copy_images, delete_images, move_image,
62-
move_images, pin_image, upload_image_to_project
62+
move_images, pin_image, upload_image_to_project, assign_folder,
63+
unassign_folder, unassign_images
6364
)
6465
from .db.projects import (
6566
clone_project, create_project, create_project_from_metadata, delete_project,

superannotate/db/project_images.py

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
)
1717
from .project_api import get_project_and_folder_metadata
1818
from .projects import (
19-
get_project_default_image_quality_in_editor, _get_available_image_counts
19+
get_project_default_image_quality_in_editor, _get_available_image_counts,
20+
get_project_metadata
2021
)
22+
from .teams import get_team_metadata
2123
from ..mixp.decorators import Trackable
2224
from .utils import _get_upload_auth_token, _get_boto_session_by_credentials, upload_image_array_to_s3, get_image_array_to_upload, __create_image, __copy_images, __move_images, get_project_folder_string
2325

@@ -566,27 +568,149 @@ def assign_images(project, image_names, user):
566568
logger.info("Assign %s images to user %s", len(image_names), user)
567569
if len(image_names) == 0:
568570
return
569-
project, project_folder = get_project_and_folder_metadata(project)
570-
images = search_images((project, project_folder), return_metadata=True)
571-
image_dict = {}
572-
for image in images:
573-
image_dict[image["name"]] = image["id"]
574571

575-
image_ids = []
576-
for image_name in image_names:
577-
image_ids.append(image_dict[image_name])
578-
team_id, project_id = project["team_id"], project["id"]
579-
params = {"team_id": team_id, "project_id": project_id}
580-
if project_folder is not None:
581-
params['folder_id'] = project_folder['id']
582-
json_req = {"user_id": user, "image_ids": image_ids}
572+
project, folder = get_project_and_folder_metadata(project)
573+
574+
verified_users = get_team_metadata()["users"]
575+
verified_users = [i['id'] for i in verified_users]
576+
if user not in verified_users:
577+
logging.warn(
578+
f'Skipping {user}. {user} is not a verified contributor for the {project["name"]}'
579+
)
580+
581+
folder_name = 'root'
582+
if folder:
583+
folder_name = folder['name']
584+
585+
params = {"project_id": project['id'], "team_id": project["team_id"]}
586+
json_req = {
587+
"image_names": image_names,
588+
"assign_user_id": user,
589+
"folder_name": folder_name,
590+
}
583591
response = _api.send_request(
584-
req_type='POST',
585-
path='/images/assign',
592+
req_type='PUT',
593+
path='/images/editAssignment',
586594
params=params,
587595
json_req=json_req
588596
)
597+
589598
if not response.ok:
590599
raise SABaseException(
591600
response.status_code, "Couldn't assign images " + response.text
592601
)
602+
603+
604+
def assign_folder(project, folder_name, users):
605+
"""Assigns folder to users. With SDK, the user can be
606+
assigned to a role in the project with the share_project function.
607+
608+
:param project: project name or metadata of the project
609+
:type project: str or dict
610+
:param folder_name: folder name to assign
611+
:type folder_name: str
612+
:param users: list of user emails
613+
:type user: list of str
614+
"""
615+
616+
project_meta = get_project_metadata(project, include_contributors=True)
617+
verified_users = get_team_metadata()["users"]
618+
project_name = project_meta['name']
619+
verified_users = [i['id'] for i in verified_users]
620+
verified_users = set(users).intersection(set(verified_users))
621+
unverified_contributor = set(users) - verified_users
622+
623+
for user in unverified_contributor:
624+
logging.warn(
625+
f'Skipping {user} from assignees. {user} is not a verified contributor for the {project_name}'
626+
)
627+
continue
628+
629+
params = {
630+
"project_id": project_meta['id'],
631+
"team_id": project_meta["team_id"]
632+
}
633+
json_req = {
634+
"assign_user_ids": list(verified_users),
635+
"folder_name": folder_name
636+
}
637+
response = _api.send_request(
638+
req_type='POST',
639+
path='/folder/editAssignment',
640+
params=params,
641+
json_req=json_req
642+
)
643+
644+
if not response.ok:
645+
raise SABaseException(
646+
response.status_code, "Couldn't assign folder " + response.text
647+
)
648+
logger.info(f'Assigned {folder_name} to users: {list(verified_users)}')
649+
650+
651+
def unassign_folder(project, folder_name):
652+
"""Removes assignment of given folder for all assignees.
653+
With SDK, the user can be assigned to a role in the project
654+
with the share_project function.
655+
656+
:param project: project name or folder path (e.g., "project1/folder1")
657+
:type project: str
658+
:param folder_name: folder name to remove assignees
659+
:type folder_name: str
660+
"""
661+
662+
project_meta = get_project_metadata(project)
663+
params = {
664+
"project_id": project_meta['id'],
665+
"team_id": project_meta["team_id"]
666+
}
667+
json_req = {"folder_name": folder_name, "remove_user_ids": ["all"]}
668+
response = _api.send_request(
669+
req_type='POST',
670+
path='/folder/editAssignment',
671+
params=params,
672+
json_req=json_req
673+
)
674+
675+
if not response.ok:
676+
raise SABaseException(
677+
response.status_code, "Couldn't unassign folder " + response.text
678+
)
679+
680+
681+
def unassign_images(project, image_names):
682+
"""Removes assignment of given images for all assignees.With SDK,
683+
the user can be assigned to a role in the project with the share_project
684+
function.
685+
686+
:param project: project name or folder path (e.g., "project1/folder1")
687+
:type project: str
688+
:param image_names: list of image unassign
689+
:type image_names: list of str
690+
"""
691+
project_meta = get_project_metadata(project)
692+
project, folder = get_project_and_folder_metadata(project)
693+
folder_name = 'root'
694+
if folder:
695+
folder_name = folder['name']
696+
params = {
697+
"project_id": project_meta['id'],
698+
"team_id": project_meta["team_id"]
699+
}
700+
json_req = {
701+
"image_names": image_names,
702+
"remove_user_ids": ["all"],
703+
"folder_name": folder_name
704+
}
705+
706+
response = _api.send_request(
707+
req_type='PUT',
708+
path='/images/editAssignment',
709+
params=params,
710+
json_req=json_req
711+
)
712+
713+
if not response.ok:
714+
raise SABaseException(
715+
response.status_code, "Couldn't unassign images " + response.text
716+
)

superannotate/db/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ..exceptions import SABaseException, SAImageSizeTooLarge, SANonExistingProjectNameException
1414
import datetime
1515
import boto3
16+
from .project_api import get_project_metadata_bare
1617

1718
_api = API.get_instance()
1819
logger = logging.getLogger("superannotate-python-sdk")

tests/test_assign_images.py

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from pathlib import Path
22
import time
3-
43
import pytest
5-
64
import superannotate as sa
5+
from superannotate.api import API
6+
7+
_api = API.get_instance()
78

89
PROJECT_NAME_VECTOR1 = "test assign images1"
910
PROJECT_NAME_VECTOR2 = "test assign images2"
@@ -16,7 +17,7 @@ def test_assign_images(tmpdir):
1617
projects = sa.search_projects(PROJECT_NAME_VECTOR1, return_metadata=True)
1718
for project in projects:
1819
sa.delete_project(project)
19-
20+
time.sleep(1)
2021
project = sa.create_project(PROJECT_NAME_VECTOR1, "test", "Vector")
2122
email = sa.get_team_metadata()["users"][0]["email"]
2223
sa.share_project(project, email, "QA")
@@ -70,7 +71,7 @@ def test_assign_images_folder(tmpdir):
7071
projects = sa.search_projects(PROJECT_NAME_VECTOR2, return_metadata=True)
7172
for project in projects:
7273
sa.delete_project(project)
73-
74+
time.sleep(1)
7475
project = sa.create_project(PROJECT_NAME_VECTOR2, "test", "Vector")
7576
email = sa.get_team_metadata()["users"][0]["email"]
7677
sa.share_project(project, email, "QA")
@@ -118,4 +119,116 @@ def test_assign_images_folder(tmpdir):
118119
assert im1_metadata["annotator_id"] == email
119120
assert im2_metadata["annotator_id"] == email
120121
assert im1_metadata["qa_id"] is None
121-
assert im2_metadata["qa_id"] is None
122+
assert im2_metadata["qa_id"] is None
123+
124+
125+
def test_unassign_images(tmpdir):
126+
tmpdir = Path(tmpdir)
127+
projects = sa.search_projects(PROJECT_NAME_VECTOR1, return_metadata=True)
128+
for project in projects:
129+
sa.delete_project(project)
130+
time.sleep(1)
131+
project = sa.create_project(PROJECT_NAME_VECTOR1, "test", "Vector")
132+
email = sa.get_team_metadata()["users"][0]["email"]
133+
sa.share_project(project, email, "QA")
134+
sa.upload_images_from_folder_to_project(
135+
project, "./tests/sample_project_vector"
136+
)
137+
sa.assign_images(
138+
project, ["example_image_1.jpg", "example_image_2.jpg"], email
139+
)
140+
sa.unassign_images(
141+
project,
142+
["example_image_1.jpg", "example_image_2.jpg"],
143+
)
144+
145+
im1_metadata = sa.get_image_metadata(project, "example_image_1.jpg")
146+
im2_metadata = sa.get_image_metadata(project, "example_image_2.jpg")
147+
148+
assert im1_metadata["qa_id"] == None
149+
assert im2_metadata["qa_id"] == None
150+
151+
152+
def test_assign_folder(tmpdir):
153+
tmpdir = Path(tmpdir)
154+
projects = sa.search_projects(PROJECT_NAME_VECTOR1, return_metadata=True)
155+
for project in projects:
156+
sa.delete_project(project)
157+
time.sleep(1)
158+
project = sa.create_project(PROJECT_NAME_VECTOR1, "test", "Vector")
159+
folder_name = "assign_folder"
160+
sa.create_folder(project, folder_name)
161+
email = sa.get_team_metadata()["users"][1]["email"]
162+
sa.share_project(project, email, "QA")
163+
sa.assign_folder(project, folder_name, [email])
164+
folders = _search_folders(project, includeUsers=True)
165+
assert len(folders["data"][0]['folder_users']) > 0
166+
167+
168+
def test_unassign_folder(tmpdir):
169+
tmpdir = Path(tmpdir)
170+
projects = sa.search_projects(PROJECT_NAME_VECTOR1, return_metadata=True)
171+
for project in projects:
172+
sa.delete_project(project)
173+
time.sleep(1)
174+
project = sa.create_project(PROJECT_NAME_VECTOR1, "test", "Vector")
175+
folder_name = "assign_folder"
176+
sa.create_folder(project, folder_name)
177+
email = sa.get_team_metadata()["users"][1]["email"]
178+
sa.share_project(project, email, "QA")
179+
sa.assign_folder(project, folder_name, [email])
180+
folders = _search_folders(project, includeUsers=True)
181+
assert len(folders["data"][0]['folder_users']) > 0
182+
sa.unassign_folder(project, folder_name)
183+
folders = _search_folders(project, includeUsers=True)
184+
assert len(folders["data"][0]['folder_users']) == 0
185+
186+
187+
def _search_folders(project, folder_name=None, includeUsers=False):
188+
team_id, project_id = project["team_id"], project["id"]
189+
params = {
190+
'team_id': team_id,
191+
'project_id': project_id,
192+
'offset': 0,
193+
'name': folder_name,
194+
'is_root': 0,
195+
'includeUsers': includeUsers
196+
}
197+
198+
response = _api.send_request(req_type='GET', path='/folders', params=params)
199+
response = response.json()
200+
return response
201+
202+
203+
def test_assign_folder_unverified_users(tmpdir, caplog):
204+
tmpdir = Path(tmpdir)
205+
projects = sa.search_projects(PROJECT_NAME_VECTOR1, return_metadata=True)
206+
for project in projects:
207+
sa.delete_project(project)
208+
time.sleep(1)
209+
project = sa.create_project(PROJECT_NAME_VECTOR1, "test", "Vector")
210+
folder_name = "assign_folder"
211+
sa.create_folder(project, folder_name)
212+
email = "unverified_user@mail.com"
213+
sa.assign_folder(project, folder_name, [email])
214+
"Skipping unverified_user@mail.com from assignees." in caplog.text
215+
216+
217+
def test_assign_images_unverified_user(tmpdir, caplog):
218+
tmpdir = Path(tmpdir)
219+
220+
projects = sa.search_projects(PROJECT_NAME_VECTOR2, return_metadata=True)
221+
for project in projects:
222+
sa.delete_project(project)
223+
time.sleep(1)
224+
project = sa.create_project(PROJECT_NAME_VECTOR2, "test", "Vector")
225+
sa.create_folder(project, FOLDER2)
226+
project_folder = project["name"] + "/" + FOLDER2
227+
sa.upload_images_from_folder_to_project(
228+
project_folder, "./tests/sample_project_vector"
229+
)
230+
email = "unverified_user@email.com"
231+
sa.assign_images(
232+
project_folder, ["example_image_1.jpg", "example_image_2.jpg"], email
233+
)
234+
"Skipping unverified_user@mail.com from assignees." in caplog.text

0 commit comments

Comments
 (0)