Skip to content

Commit 3e47710

Browse files
committed
Get invitations
1 parent d48b5c0 commit 3e47710

File tree

3 files changed

+360
-7
lines changed

3 files changed

+360
-7
lines changed

libs/labelbox/src/labelbox/schema/invite.py

+141-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
from typing import TYPE_CHECKING
12
from dataclasses import dataclass
23

34
from labelbox.orm.db_object import DbObject
45
from labelbox.orm.model import Field
56
from labelbox.schema.role import ProjectRole, format_role
67

8+
from labelbox.pagination import PaginatedCollection
9+
10+
if TYPE_CHECKING:
11+
from labelbox import Client
12+
713

814
@dataclass
915
class InviteLimit:
@@ -31,10 +37,138 @@ def __init__(self, client, invite_response):
3137
project_roles = invite_response.pop("projectInvites", [])
3238
super().__init__(client, invite_response)
3339

34-
self.project_roles = [
35-
ProjectRole(
36-
project=client.get_project(r["projectId"]),
37-
role=client.get_roles()[format_role(r["projectRoleName"])],
38-
)
39-
for r in project_roles
40-
]
40+
self.project_roles = []
41+
42+
# If a project is deleted then it doesn't show up in the invite
43+
for pr in project_roles:
44+
try:
45+
project = client.get_project(pr["projectId"])
46+
if project: # Check if project exists
47+
self.project_roles.append(
48+
ProjectRole(
49+
project=project,
50+
role=client.get_roles()[
51+
format_role(pr["projectRoleName"])
52+
],
53+
)
54+
)
55+
except Exception:
56+
# Skip this project role if the project is no longer available
57+
continue
58+
59+
def cancel(self) -> bool:
60+
"""
61+
Cancels this invite.
62+
63+
This will prevent the invited user from accepting the invitation.
64+
65+
Returns:
66+
bool: True if the invite was successfully canceled, False otherwise.
67+
"""
68+
69+
# Case of a newly invited user
70+
if self.uid == "invited":
71+
return False
72+
73+
query_str = """
74+
mutation CancelInvitePyApi($where: WhereUniqueIdInput!) {
75+
cancelInvite(where: $where) {
76+
id
77+
}
78+
}"""
79+
result = self.client.execute(
80+
query_str, {"where": {"id": self.uid}}, experimental=True
81+
)
82+
return (
83+
result is not None
84+
and "cancelInvite" in result
85+
and result.get("cancelInvite") is not None
86+
)
87+
88+
@staticmethod
89+
def get_project_invites(
90+
client: "Client", project_id: str
91+
) -> PaginatedCollection:
92+
"""
93+
Retrieves all invites for a specific project.
94+
95+
Args:
96+
client (Client): The Labelbox client instance.
97+
project_id (str): The ID of the project to get invites for.
98+
99+
Returns:
100+
PaginatedCollection: A collection of Invite objects for the specified project.
101+
"""
102+
query = """query GetProjectInvitationsPyApi(
103+
$from: ID
104+
$first: PageSize
105+
$projectId: ID!
106+
) {
107+
project(where: { id: $projectId }) {
108+
id
109+
invites(from: $from, first: $first) {
110+
nodes {
111+
id
112+
createdAt
113+
organizationRoleName
114+
inviteeEmail
115+
projectInvites {
116+
id
117+
projectRoleName
118+
projectId
119+
}
120+
}
121+
nextCursor
122+
}
123+
}
124+
}"""
125+
126+
invites = PaginatedCollection(
127+
client,
128+
query,
129+
{"projectId": project_id, "search": ""},
130+
["project", "invites", "nodes"],
131+
Invite,
132+
cursor_path=["project", "invites", "nextCursor"],
133+
)
134+
return invites
135+
136+
@staticmethod
137+
def get_invites(client: "Client") -> PaginatedCollection:
138+
"""
139+
Retrieves all invites for the organization.
140+
141+
Args:
142+
client (Client): The Labelbox client instance.
143+
144+
Returns:
145+
PaginatedCollection: A collection of Invite objects for the organization.
146+
"""
147+
query_str = """query GetOrgInvitationsPyApi($from: ID, $first: PageSize) {
148+
organization {
149+
id
150+
invites(from: $from, first: $first) {
151+
nodes {
152+
id
153+
createdAt
154+
organizationRoleName
155+
inviteeEmail
156+
projectInvites {
157+
id
158+
projectRoleName
159+
projectId
160+
}
161+
}
162+
nextCursor
163+
}
164+
}
165+
}"""
166+
invites = PaginatedCollection(
167+
client,
168+
query_str,
169+
{},
170+
["organization", "invites", "nodes"],
171+
Invite,
172+
cursor_path=["organization", "invites", "nextCursor"],
173+
)
174+
return invites

libs/labelbox/src/labelbox/schema/organization.py

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from labelbox.orm.model import Field, Relationship
88
from labelbox.schema.invite import InviteLimit
99
from labelbox.schema.resource_tag import ResourceTag
10+
from labelbox.pagination import PaginatedCollection
1011

1112
if TYPE_CHECKING:
1213
from labelbox import (
@@ -243,3 +244,24 @@ def get_default_iam_integration(self) -> Optional["IAMIntegration"]:
243244
return (
244245
None if not len(default_integration) else default_integration.pop()
245246
)
247+
248+
def get_invites(self) -> PaginatedCollection:
249+
"""
250+
Retrieves all invites for this organization.
251+
252+
Returns:
253+
PaginatedCollection: A collection of Invite objects for the organization.
254+
"""
255+
return Entity.Invite.get_invites(self.client)
256+
257+
def get_project_invites(self, project_id: str) -> PaginatedCollection:
258+
"""
259+
Retrieves all invites for a specific project in this organization.
260+
261+
Args:
262+
project_id (str): The ID of the project to get invites for.
263+
264+
Returns:
265+
PaginatedCollection: A collection of Invite objects for the specified project.
266+
"""
267+
return Entity.Invite.get_project_invites(self.client, project_id)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import pytest
2+
from faker import Faker
3+
from labelbox.schema.media_type import MediaType
4+
from labelbox import ProjectRole
5+
import time
6+
7+
faker = Faker()
8+
9+
10+
@pytest.fixture
11+
def dummy_email():
12+
"""Generate a random dummy email for testing"""
13+
return f"none+{faker.uuid4()}@labelbox.com"
14+
15+
16+
@pytest.fixture(scope="module")
17+
def test_project(client):
18+
"""Create a temporary project for testing"""
19+
project = client.create_project(
20+
name=f"test-project-{faker.uuid4()}", media_type=MediaType.Image
21+
)
22+
yield project
23+
24+
# Occurs after the test is finished based on scope
25+
project.delete()
26+
27+
28+
@pytest.fixture
29+
def org_invite(client, dummy_email):
30+
"""Create an organization-level invite"""
31+
role = client.get_roles()["LABELER"]
32+
organization = client.get_organization()
33+
invite = organization.invite_user(dummy_email, role)
34+
35+
yield invite
36+
37+
if invite.uid:
38+
invite.cancel()
39+
40+
41+
@pytest.fixture
42+
def project_invite(client, test_project, dummy_email):
43+
"""Create a project-level invite"""
44+
roles = client.get_roles()
45+
project_role = ProjectRole(project=test_project, role=roles["LABELER"])
46+
organization = client.get_organization()
47+
48+
invite = organization.invite_user(
49+
dummy_email, roles["NONE"], project_roles=[project_role]
50+
)
51+
52+
yield invite
53+
54+
# Cleanup: Use invite.cancel() instead of organization.cancel_invite()
55+
if invite.uid:
56+
invite.cancel()
57+
58+
59+
def test_get_organization_invites(client, org_invite):
60+
"""Test retrieving all organization invites"""
61+
# Add a small delay to ensure invite is created
62+
time.sleep(1)
63+
64+
organization = client.get_organization()
65+
invites = organization.get_invites()
66+
invite_list = [invite for invite in invites]
67+
assert len(invite_list) > 0
68+
69+
# Verify our test invite is in the list
70+
invite_emails = [invite.email for invite in invite_list]
71+
assert org_invite.email in invite_emails
72+
73+
74+
def test_get_project_invites(client, test_project, project_invite):
75+
"""Test retrieving project-specific invites"""
76+
# Add a small delay to ensure invite is created
77+
time.sleep(1)
78+
79+
organization = client.get_organization()
80+
project_invites = organization.get_project_invites(test_project.uid)
81+
invite_list = [invite for invite in project_invites]
82+
assert len(invite_list) > 0
83+
84+
# Verify our test invite is in the list
85+
invite_emails = [invite.email for invite in invite_list]
86+
assert project_invite.email in invite_emails
87+
88+
# Verify project role assignment
89+
found_invite = next(
90+
invite for invite in invite_list if invite.email == project_invite.email
91+
)
92+
assert len(found_invite.project_roles) == 1
93+
assert found_invite.project_roles[0].project.uid == test_project.uid
94+
95+
96+
def test_cancel_invite(client, dummy_email):
97+
"""Test canceling an invite"""
98+
# Create a new invite
99+
role = client.get_roles()["LABELER"]
100+
organization = client.get_organization()
101+
organization.invite_user(dummy_email, role)
102+
103+
# Add a small delay to ensure invite is created
104+
time.sleep(1)
105+
106+
# Find the actual invite by email
107+
invites = organization.get_invites()
108+
found_invite = next(
109+
(invite for invite in invites if invite.email == dummy_email), None
110+
)
111+
assert found_invite is not None, f"Invite for {dummy_email} not found"
112+
113+
# Cancel the invite using the found invite object
114+
result = found_invite.cancel()
115+
assert result is True
116+
117+
# Verify the invite is no longer in the organization's invites
118+
invites = organization.get_invites()
119+
invite_emails = [i.email for i in invites]
120+
assert dummy_email not in invite_emails
121+
122+
123+
def test_cancel_project_invite(client, test_project, dummy_email):
124+
"""Test canceling a project invite"""
125+
# Create a project invite
126+
roles = client.get_roles()
127+
project_role = ProjectRole(project=test_project, role=roles["LABELER"])
128+
organization = client.get_organization()
129+
130+
organization.invite_user(
131+
dummy_email, roles["NONE"], project_roles=[project_role]
132+
)
133+
134+
# Add a small delay to ensure invite is created
135+
time.sleep(1)
136+
137+
# Find the actual invite by email
138+
invites = organization.get_invites()
139+
found_invite = next(
140+
(invite for invite in invites if invite.email == dummy_email), None
141+
)
142+
assert found_invite is not None, f"Invite for {dummy_email} not found"
143+
144+
# Cancel the invite using the found invite object
145+
result = found_invite.cancel()
146+
assert result is True
147+
148+
# Verify the invite is no longer in the project's invites
149+
project_invites = organization.get_project_invites(test_project.uid)
150+
invite_emails = [i.email for i in project_invites]
151+
assert dummy_email not in invite_emails
152+
153+
154+
def test_project_invite_after_project_deletion(client, dummy_email):
155+
"""Test that project invites are properly filtered when a project is deleted"""
156+
# Create two test projects
157+
project1 = client.create_project(
158+
name=f"test-project1-{faker.uuid4()}", media_type=MediaType.Image
159+
)
160+
project2 = client.create_project(
161+
name=f"test-project2-{faker.uuid4()}", media_type=MediaType.Image
162+
)
163+
164+
# Create project roles
165+
roles = client.get_roles()
166+
project_role1 = ProjectRole(project=project1, role=roles["LABELER"])
167+
project_role2 = ProjectRole(project=project2, role=roles["LABELER"])
168+
169+
# Invite user to both projects
170+
organization = client.get_organization()
171+
organization.invite_user(
172+
dummy_email, roles["NONE"], project_roles=[project_role1, project_role2]
173+
)
174+
175+
# Add a small delay to ensure invite is created
176+
time.sleep(1)
177+
178+
# Delete one project
179+
project1.delete()
180+
181+
# Find the invite and verify project roles
182+
invites = organization.get_invites()
183+
found_invite = next(
184+
(invite for invite in invites if invite.email == dummy_email), None
185+
)
186+
assert found_invite is not None, f"Invite for {dummy_email} not found"
187+
188+
# Verify only one project role remains
189+
assert (
190+
len(found_invite.project_roles) == 1
191+
), "Expected only one project role"
192+
assert found_invite.project_roles[0].project.uid == project2.uid
193+
194+
# Cleanup
195+
project2.delete()
196+
if found_invite.uid:
197+
found_invite.cancel()

0 commit comments

Comments
 (0)