Skip to content

Commit 8d4777f

Browse files
authored
[PLT-1205] Add additional UG Tests / Proper Error Handling (#1712)
1 parent da4fe9c commit 8d4777f

File tree

5 files changed

+222
-55
lines changed

5 files changed

+222
-55
lines changed

libs/labelbox/src/labelbox/exceptions.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@ class AuthorizationError(LabelboxError):
3232
class ResourceNotFoundError(LabelboxError):
3333
"""Exception raised when a given resource is not found. """
3434

35-
def __init__(self, db_object_type, params):
36-
""" Constructor.
35+
def __init__(self, db_object_type=None, params=None, message=None):
36+
"""Constructor for the ResourceNotFoundException class.
3737
3838
Args:
39-
db_object_type (type): A labelbox.schema.DbObject subtype.
40-
params (dict): Dict of params identifying the sought resource.
39+
db_object_type (type): A subtype of labelbox.schema.DbObject.
40+
params (dict): A dictionary of parameters identifying the sought resource.
41+
message (str): An optional message to include in the exception.
4142
"""
42-
super().__init__("Resource '%s' not found for params: %r" %
43-
(db_object_type.type_name(), params))
44-
self.db_object_type = db_object_type
45-
self.params = params
43+
if message is not None:
44+
super().__init__(message)
45+
else:
46+
super().__init__("Resource '%s' not found for params: %r" %
47+
(db_object_type.type_name(), params))
48+
self.db_object_type = db_object_type
49+
self.params = params
4650

4751

4852
class ResourceConflict(LabelboxError):

libs/labelbox/src/labelbox/schema/user_group.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from labelbox.pydantic_compat import BaseModel
88
from labelbox.schema.user import User
99
from labelbox.schema.project import Project
10-
from labelbox.exceptions import UnprocessableEntityError, InvalidQueryError
10+
from labelbox.exceptions import UnprocessableEntityError, MalformedQueryException, ResourceNotFoundError
1111
from labelbox.schema.queue_mode import QueueMode
1212
from labelbox.schema.ontology_kind import EditorTaskType
1313
from labelbox.schema.media_type import MediaType
@@ -105,14 +105,11 @@ def get(self) -> "UserGroup":
105105
about the user group, including its name, color, projects, and members. The fetched
106106
information is then used to update the corresponding attributes of the `Group` object.
107107
108-
Args:
109-
id (str): The ID of the user group to fetch.
110-
111108
Returns:
112-
UserGroup of passed in ID (self)
109+
UserGroup: The updated `UserGroup` object.
113110
114111
Raises:
115-
InvalidQueryError: If the query fails to fetch the group information.
112+
ResourceNotFoundError: If the query fails to fetch the group information.
116113
"""
117114
query = """
118115
query GetUserGroupPyApi($id: ID!) {
@@ -142,7 +139,7 @@ def get(self) -> "UserGroup":
142139
}
143140
result = self.client.execute(query, params)
144141
if not result:
145-
raise InvalidQueryError("Failed to fetch group, ID likely incorrect")
142+
raise ResourceNotFoundError(message="Failed to get user group as user group does not exist")
146143
self.name = result["userGroup"]["name"]
147144
self.color = UserGroupColor(result["userGroup"]["color"])
148145
self.projects = self._get_projects_set(result["userGroup"]["projects"]["nodes"])
@@ -157,7 +154,8 @@ def update(self) -> "UserGroup":
157154
UserGroup: The updated UserGroup object. (self)
158155
159156
Raises:
160-
UnprocessableEntityError: If the update fails.
157+
ResourceNotFoundError: If the update fails due to unknown user group
158+
UnprocessableEntityError: If the update fails due to a malformed input
161159
"""
162160
query = """
163161
mutation UpdateUserGroupPyApi($id: ID!, $name: String!, $color: String!, $projectIds: [String!]!, $userIds: [String!]!) {
@@ -199,9 +197,12 @@ def update(self) -> "UserGroup":
199197
user.uid for user in self.users
200198
]
201199
}
202-
result = self.client.execute(query, params)
203-
if not result:
204-
raise UnprocessableEntityError("Failed to update user group, either user group name is in use currently, or provided user or projects don't exist")
200+
try:
201+
result = self.client.execute(query, params)
202+
if not result:
203+
raise ResourceNotFoundError(message="Failed to update user group as user group does not exist")
204+
except MalformedQueryException as e:
205+
raise UnprocessableEntityError("Failed to update user group") from e
205206
return self
206207

207208
def create(self) -> "UserGroup":
@@ -261,9 +262,15 @@ def create(self) -> "UserGroup":
261262
user.uid for user in self.users
262263
]
263264
}
264-
result = self.client.execute(query, params)
265-
if not result:
266-
raise ResourceCreationError("Failed to create user group, either user group name is in use currently, or provided user or projects don't exist")
265+
result = None
266+
error = None
267+
try:
268+
result = self.client.execute(query, params)
269+
except Exception as e:
270+
error = e
271+
if not result or error:
272+
# this is client side only, server doesn't have an equivalent error
273+
raise ResourceCreationError(f"Failed to create user group, either user group name is in use currently, or provided user or projects don't exist server error: {error}")
267274
result = result["createUserGroup"]["group"]
268275
self.id = result["id"]
269276
return self
@@ -280,7 +287,7 @@ def delete(self) -> bool:
280287
bool: True if the user group was successfully deleted, False otherwise.
281288
282289
Raises:
283-
UnprocessableEntityError: If the deletion of the user group fails.
290+
ResourceNotFoundError: If the deletion of the user group fails due to not existing
284291
"""
285292
query = """
286293
mutation DeleteUserGroupPyApi($id: ID!) {
@@ -292,7 +299,7 @@ def delete(self) -> bool:
292299
params = {"id": self.id}
293300
result = self.client.execute(query, params)
294301
if not result:
295-
raise UnprocessableEntityError("Failed to delete user group")
302+
raise ResourceNotFoundError(message="Failed to delete user group as user group does not exist")
296303
return result["deleteUserGroup"]["success"]
297304

298305
def get_user_groups(self) -> Iterator["UserGroup"]:
@@ -306,8 +313,8 @@ def get_user_groups(self) -> Iterator["UserGroup"]:
306313
Iterator[UserGroup]: An iterator over the user groups.
307314
"""
308315
query = """
309-
query GetUserGroupsPyApi {
310-
userGroups {
316+
query GetUserGroupsPyApi($after: String) {
317+
userGroups(after: $after) {
311318
nodes {
312319
id
313320
name
@@ -334,7 +341,7 @@ def get_user_groups(self) -> Iterator["UserGroup"]:
334341
nextCursor = None
335342
while True:
336343
userGroups = self.client.execute(
337-
query, {"nextCursor": nextCursor})["userGroups"]
344+
query, {"after": nextCursor})["userGroups"]
338345
if not userGroups:
339346
return
340347
yield
@@ -348,9 +355,9 @@ def get_user_groups(self) -> Iterator["UserGroup"]:
348355
userGroup.projects = self._get_projects_set(group["projects"]["nodes"])
349356
yield userGroup
350357
nextCursor = userGroups["nextCursor"]
351-
# this doesn't seem to be implemented right now to return a value other than null from the api
352358
if not nextCursor:
353-
break
359+
return
360+
yield
354361

355362
def _get_users_set(self, user_nodes):
356363
"""

libs/labelbox/tests/conftest.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ def ephemeral_endpoint() -> str:
9191

9292

9393
def graphql_url(environ: str) -> str:
94-
if environ == Environ.PROD:
94+
if environ == Environ.LOCAL:
95+
return 'http://localhost:3000/api/graphql'
96+
elif environ == Environ.PROD:
9597
return 'https://api.labelbox.com/graphql'
9698
elif environ == Environ.STAGING:
9799
return 'https://api.lb-stage.xyz/graphql'
@@ -107,7 +109,9 @@ def graphql_url(environ: str) -> str:
107109

108110

109111
def rest_url(environ: str) -> str:
110-
if environ == Environ.PROD:
112+
if environ == Environ.LOCAL:
113+
return 'http://localhost:3000/api/v1'
114+
elif environ == Environ.PROD:
111115
return 'https://api.labelbox.com/api/v1'
112116
elif environ == Environ.STAGING:
113117
return 'https://api.lb-stage.xyz/api/v1'
@@ -124,7 +128,8 @@ def rest_url(environ: str) -> str:
124128
def testing_api_key(environ: Environ) -> str:
125129
keys = [
126130
f"LABELBOX_TEST_API_KEY_{environ.value.upper()}",
127-
"LABELBOX_TEST_API_KEY"
131+
"LABELBOX_TEST_API_KEY",
132+
"LABELBOX_API_KEY"
128133
]
129134
for key in keys:
130135
value = os.environ.get(key)
@@ -311,10 +316,16 @@ def environ() -> Environ:
311316
'prod' or 'staging'
312317
Make sure to set LABELBOX_TEST_ENVIRON in .github/workflows/python-package.yaml
313318
"""
314-
try:
315-
return Environ(os.environ['LABELBOX_TEST_ENVIRON'])
316-
except KeyError:
317-
raise Exception(f'Missing LABELBOX_TEST_ENVIRON in: {os.environ}')
319+
keys = [
320+
"LABELBOX_TEST_ENV",
321+
"LABELBOX_TEST_ENVIRON",
322+
"LABELBOX_ENV"
323+
]
324+
for key in keys:
325+
value = os.environ.get(key)
326+
if value is not None:
327+
return Environ(value)
328+
raise Exception(f'Missing env key in: {os.environ}')
318329

319330

320331
def cancel_invite(client, invite_id):

libs/labelbox/tests/integration/schema/test_user_group.py

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pytest
22
import faker
3+
from uuid import uuid4
34
from labelbox import Client
45
from labelbox.schema.user_group import UserGroup, UserGroupColor
6+
from labelbox.exceptions import ResourceNotFoundError, ResourceCreationError, UnprocessableEntityError
57

68
data = faker.Faker()
79

@@ -29,47 +31,132 @@ def test_existing_user_groups(user_group, client):
2931
assert user_group.color == user_group_equal.color
3032

3133

34+
def test_cannot_get_user_group_with_invalid_id(client):
35+
user_group = UserGroup(client=client, id=str(uuid4()))
36+
with pytest.raises(ResourceNotFoundError):
37+
user_group.get()
38+
39+
40+
def test_throw_error_when_retrieving_deleted_group(client):
41+
user_group = UserGroup(client=client, name=data.name())
42+
user_group.create()
43+
44+
assert user_group.get() is not None
45+
user_group.delete()
46+
47+
with pytest.raises(ResourceNotFoundError):
48+
user_group.get()
49+
50+
51+
def test_create_user_group_no_name(client):
52+
# Create a new user group
53+
with pytest.raises(ResourceCreationError):
54+
user_group = UserGroup(client)
55+
user_group.name = " "
56+
user_group.color = UserGroupColor.BLUE
57+
user_group.create()
58+
59+
60+
def test_cannot_create_group_with_same_name(client, user_group):
61+
with pytest.raises(ResourceCreationError):
62+
user_group_2 = UserGroup(client=client, name=user_group.name)
63+
user_group_2.create()
64+
65+
3266
def test_create_user_group(user_group):
3367
# Verify that the user group was created successfully
3468
assert user_group.id is not None
3569
assert user_group.name is not None
3670
assert user_group.color == UserGroupColor.BLUE
3771

3872

73+
def test_create_user_group_advanced(client, project_pack):
74+
group_name = data.name()
75+
# Create a new user group
76+
user_group = UserGroup(client)
77+
user_group.name = group_name
78+
user_group.color = UserGroupColor.BLUE
79+
users = list(client.get_users())
80+
projects = project_pack
81+
user = users[0]
82+
project = projects[0]
83+
user_group.users.add(user)
84+
user_group.projects.add(project)
85+
86+
user_group.create()
87+
88+
assert user_group.id is not None
89+
assert user_group.name is not None
90+
assert user_group.color == UserGroupColor.BLUE
91+
assert project in user_group.projects
92+
assert user in user_group.users
93+
94+
user_group.delete()
95+
96+
3997
def test_update_user_group(user_group):
4098
# Update the user group
4199
group_name = data.name()
42100
user_group.name = group_name
43101
user_group.color = UserGroupColor.PURPLE
44-
user_group.update()
102+
updated_user_group = user_group.update()
45103

46104
# Verify that the user group was updated successfully
105+
assert user_group.name == updated_user_group.name
47106
assert user_group.name == group_name
107+
assert user_group.color == updated_user_group.color
48108
assert user_group.color == UserGroupColor.PURPLE
49109

50110

51-
def test_get_user_groups(user_group, client):
52-
# Get all user groups
53-
user_groups_old = list(UserGroup(client).get_user_groups())
111+
def test_cannot_update_name_to_empty_string(user_group):
112+
with pytest.raises(UnprocessableEntityError):
113+
user_group.name = ""
114+
user_group.update()
54115

55-
# manual delete for iterators
56-
group_name = data.name()
57-
user_group = UserGroup(client)
58-
user_group.name = group_name
59-
user_group.create()
60116

61-
user_groups_new = list(UserGroup(client).get_user_groups())
117+
def test_cannot_update_group_id(user_group):
118+
old_id = user_group.id
119+
with pytest.raises(ResourceNotFoundError):
120+
user_group.id = str(uuid4())
121+
user_group.update()
122+
# prevent leak
123+
user_group.id = old_id
62124

63-
# Verify that at least one user group is returned
64-
assert len(user_groups_new) > 0
65-
assert len(user_groups_new) == len(user_groups_old) + 1
66125

67-
# Verify that each user group has a valid ID and name
68-
for user_group in user_groups_new:
69-
assert user_group.id is not None
70-
assert user_group.name is not None
126+
def test_get_user_groups_with_creation_deletion(client):
127+
user_group = None
128+
try:
129+
# Get all user groups
130+
user_groups = list(UserGroup(client).get_user_groups())
71131

72-
user_group.delete()
132+
# manual delete for iterators
133+
group_name = data.name()
134+
user_group = UserGroup(client)
135+
user_group.name = group_name
136+
user_group.create()
137+
138+
user_groups_post_creation = list(UserGroup(client).get_user_groups())
139+
140+
# Verify that at least one user group is returned
141+
assert len(user_groups_post_creation) > 0
142+
assert len(user_groups_post_creation) == len(user_groups) + 1
143+
144+
# Verify that each user group has a valid ID and name
145+
for user_group in user_groups_post_creation:
146+
assert user_group.id is not None
147+
assert user_group.name is not None
148+
149+
user_group.delete()
150+
user_group = None
151+
152+
user_groups_post_deletion = list(UserGroup(client).get_user_groups())
153+
154+
assert len(user_groups_post_deletion) > 0
155+
assert len(user_groups_post_deletion) == len(user_groups_post_creation) - 1
156+
157+
finally:
158+
if user_group:
159+
user_group.delete()
73160

74161

75162
# project_pack creates two projects
@@ -89,6 +176,22 @@ def test_update_user_group(user_group, client, project_pack):
89176
assert project in user_group.projects
90177

91178

179+
def test_delete_user_group_with_same_id(client):
180+
user_group_1 = UserGroup(client, name=data.name())
181+
user_group_1.create()
182+
user_group_1.delete()
183+
user_group_2 = UserGroup(client=client, id=user_group_1.id)
184+
185+
with pytest.raises(ResourceNotFoundError):
186+
user_group_2.delete()
187+
188+
189+
def test_throw_error_when_deleting_invalid_id_group(client):
190+
with pytest.raises(ResourceNotFoundError):
191+
user_group = UserGroup(client=client, id=str(uuid4()))
192+
user_group.delete()
193+
194+
92195
if __name__ == "__main__":
93196
import subprocess
94197
subprocess.call(["pytest", "-v", __file__])

0 commit comments

Comments
 (0)