Skip to content

Commit e6e4dd6

Browse files
author
Matt Sokoloff
committed
recommended changes
1 parent 93012c5 commit e6e4dd6

File tree

10 files changed

+130
-138
lines changed

10 files changed

+130
-138
lines changed

labelbox/client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class Client:
3737
def __init__(self,
3838
api_key=None,
3939
endpoint='https://api.labelbox.com/graphql',
40-
enable_beta=False):
40+
enable_experimental=False):
4141
""" Creates and initializes a Labelbox Client.
4242
4343
Logging is defaulted to level WARNING. To receive more verbose
@@ -50,7 +50,7 @@ def __init__(self,
5050
Args:
5151
api_key (str): API key. If None, the key is obtained from the "LABELBOX_API_KEY" environment variable.
5252
endpoint (str): URL of the Labelbox server to connect to.
53-
enable_beta (bool): Indicated whether or not to use beta features
53+
enable_experimental (bool): Indicated whether or not to use experimental features
5454
Raises:
5555
labelbox.exceptions.AuthenticationError: If no `api_key`
5656
is provided as an argument or via the environment
@@ -63,9 +63,9 @@ def __init__(self,
6363
api_key = os.environ[_LABELBOX_API_KEY]
6464
self.api_key = api_key
6565

66-
self.enable_beta = True
67-
if enable_beta:
68-
logger.info("Beta features have been enabled")
66+
self.enable_experimental = enable_experimental
67+
if enable_experimental:
68+
logger.info("Experimental features have been enabled")
6969

7070
logger.info("Initializing Labelbox client at '%s'", endpoint)
7171
self.endpoint = endpoint
@@ -78,7 +78,7 @@ def __init__(self,
7878

7979
@retry.Retry(predicate=retry.if_exception_type(
8080
labelbox.exceptions.InternalServerError))
81-
def execute(self, query, params=None, timeout=30.0, beta=False):
81+
def execute(self, query, params=None, timeout=30.0, experimental=False):
8282
""" Sends a request to the server for the execution of the
8383
given query.
8484
@@ -125,7 +125,7 @@ def convert_value(value):
125125

126126
try:
127127
response = requests.post(self.endpoint.replace('/graphql', '/_gql')
128-
if beta else self.endpoint,
128+
if experimental else self.endpoint,
129129
data=data,
130130
headers=self.headers,
131131
timeout=timeout)
@@ -213,9 +213,9 @@ def check_errors(keywords, *path):
213213
if internal_server_error is not None:
214214
message = internal_server_error.get("message")
215215

216-
if message.startswith("Syntax Error"):
216+
if message.startswith("Syntax Error") or message.startswith(
217+
"Invite(s) cannot be sent"):
217218
raise labelbox.exceptions.InvalidQueryError(message)
218-
219219
else:
220220
raise labelbox.exceptions.InternalServerError(message)
221221

labelbox/orm/db_object.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ def delete(self):
256256
type(self).bulk_delete([self])
257257

258258

259-
def beta(fn):
259+
def experimental(fn):
260260

261261
@wraps(fn)
262262
def wrapper(self, *args, **kwargs):
263-
if not self.client.enable_beta:
263+
if not self.client.enable_experimental:
264264
raise Exception(
265-
f"This function {fn.__name__} relies on a beta feature in the api. This means that the interface could change."
266-
" Set `enable_beta=True` in the client to enable use of beta functions."
265+
f"This function {fn.__name__} relies on a experimental feature in the api. This means that the interface could change."
266+
" Set `enable_experimental=True` in the client to enable use of experimental functions."
267267
)
268268
return fn(self, *args, **kwargs)
269269

labelbox/pagination.py

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Size of a single page in a paginated query.
22
from abc import ABC, abstractmethod
3-
from typing import Any, Callable, Dict, List, Optional
3+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
44

55
from typing import TYPE_CHECKING
66
if TYPE_CHECKING:
@@ -24,9 +24,9 @@ def __init__(self,
2424
query: str,
2525
params: Dict[str, str],
2626
dereferencing: Dict[str, Any],
27-
obj_class: "DbObject",
27+
obj_class: Type["DbObject"],
2828
cursor_path: Optional[Dict[str, Any]] = None,
29-
beta: bool = False):
29+
experimental: bool = False):
3030
""" Creates a PaginatedCollection.
3131
3232
Args:
@@ -43,98 +43,117 @@ def __init__(self,
4343
cursor_path: If not None, this is used to find the cursor
4444
experimental: Used to call experimental endpoints
4545
"""
46-
self.client = client
47-
self.query = query
48-
self.params = params
49-
self.dereferencing = dereferencing
50-
self.obj_class = obj_class
51-
self.cursor_path = cursor_path
52-
self.beta = beta
53-
5446
self._fetched_all = False
5547
self._data: List[Dict[str, Any]] = []
5648
self._data_ind = 0
5749

50+
pagination_kwargs = {
51+
'client': client,
52+
'obj_class': obj_class,
53+
'dereferencing': dereferencing,
54+
'experimental': experimental,
55+
'query': query,
56+
'params': params
57+
}
58+
5859
self.paginator = _CursorPagination(
59-
client, cursor_path) if cursor_path else _OffsetPagination(client)
60+
cursor_path, **
61+
pagination_kwargs) if cursor_path else _OffsetPagination(
62+
**pagination_kwargs)
6063

6164
def __iter__(self):
6265
self._data_ind = 0
6366
return self
6467

65-
def get_page_data(self, results):
66-
for deref in self.dereferencing:
67-
results = results[deref]
68-
return [self.obj_class(self.client, result) for result in results]
69-
7068
def __next__(self):
7169
if len(self._data) <= self._data_ind:
7270
if self._fetched_all:
7371
raise StopIteration()
7472

75-
results = self.paginator.fetch_results(self.query, self.params,
76-
self.beta)
77-
page_data = self.get_page_data(results)
73+
page_data, self._fetched_all = self.paginator.get_next_page()
7874
self._data.extend(page_data)
79-
n_items = len(page_data)
80-
81-
if n_items == 0:
75+
if len(page_data) == 0:
8276
raise StopIteration()
8377

84-
self._fetched_all = self.paginator.fetched_all(n_items, results)
85-
8678
rval = self._data[self._data_ind]
8779
self._data_ind += 1
8880
return rval
8981

9082

9183
class _Pagination(ABC):
9284

93-
@abstractmethod
94-
def fetched_all(self, n_items: int, results: List[Dict[str, Any]]) -> bool:
95-
...
85+
def __init__(self, client: "Client", obj_class: Type["DbObject"],
86+
dereferencing: Dict[str, Any], query: str,
87+
params: Dict[str, Any], experimental: bool):
88+
self.client = client
89+
self.obj_class = obj_class
90+
self.dereferencing = dereferencing
91+
self.experimental = experimental
92+
self.query = query
93+
self.params = params
94+
95+
def get_page_data(self, results: Dict[str, Any]) -> List["DbObject"]:
96+
for deref in self.dereferencing:
97+
results = results[deref]
98+
return [self.obj_class(self.client, result) for result in results]
9699

97100
@abstractmethod
98-
def fetch_results(self, query: str, params: Dict[str, Any],
99-
beta: bool) -> Dict[str, Any]:
101+
def get_next_page(self) -> Tuple[Dict[str, Any], bool]:
100102
...
101103

102104

103105
class _CursorPagination(_Pagination):
104106

105-
def __init__(self, client: "Client", cursor_path: Dict[str, Any]):
106-
self.client = client
107+
def __init__(self, cursor_path: Dict[str, Any], *args, **kwargs):
108+
super().__init__(*args, **kwargs)
107109
self.cursor_path = cursor_path
108-
self.next_cursor: Optional[str] = None
110+
self.next_cursor: Optional[Any] = None
109111

110-
def get_next_cursor(self, results) -> Optional[str]:
112+
def increment_page(self, results: Dict[str, Any]):
111113
for path in self.cursor_path:
112114
results = results[path]
113-
return results
115+
self.next_cursor = results
114116

115-
def fetched_all(self, n_items: int, results: List[Dict[str, Any]]) -> bool:
116-
self.next_cursor = self.get_next_cursor(results)
117-
return bool(self.next_cursor is None)
117+
def fetched_all(self) -> bool:
118+
return bool(self.next_cursor)
118119

119-
def fetch_results(self, query: str, params: Dict[str, Any],
120-
beta: bool) -> Dict[str, Any]:
121-
params.update({'from': self.next_cursor, 'first': _PAGE_SIZE})
122-
return self.client.execute(query, params, beta=beta)
120+
def fetch_results(self) -> Dict[str, Any]:
121+
self.params.update({'from': self.next_cursor, 'first': _PAGE_SIZE})
122+
return self.client.execute(self.query,
123+
self.params,
124+
experimental=self.experimental)
125+
126+
def get_next_page(self):
127+
results = self.fetch_results()
128+
page_data = self.get_page_data(results)
129+
self.increment_page(results)
130+
done = self.fetched_all()
131+
return page_data, done
123132

124133

125134
class _OffsetPagination(_Pagination):
126135

127-
def __init__(self, client: "Client"):
128-
self.client = client
136+
def __init__(self, *args, **kwargs):
137+
super().__init__(*args, **kwargs)
129138
self._fetched_pages = 0
130139

131-
def fetched_all(self, n_items: int, results: List[Dict[str, Any]]) -> bool:
140+
def increment_page(self):
132141
self._fetched_pages += 1
142+
143+
def fetched_all(self, n_items: int) -> bool:
133144
if n_items < _PAGE_SIZE:
134145
return True
135146
return False
136147

137-
def fetch_results(self, query: str, params: Dict[str, Any],
138-
beta: bool) -> Dict[str, Any]:
139-
query = query % (self._fetched_pages * _PAGE_SIZE, _PAGE_SIZE)
140-
return self.client.execute(query, params, beta=beta)
148+
def fetch_results(self) -> Dict[str, Any]:
149+
query = self.query % (self._fetched_pages * _PAGE_SIZE, _PAGE_SIZE)
150+
return self.client.execute(query,
151+
self.params,
152+
experimental=self.experimental)
153+
154+
def get_next_page(self):
155+
results = self.fetch_results()
156+
page_data = self.get_page_data(results)
157+
self.increment_page()
158+
done = self.fetched_all(len(page_data))
159+
return page_data, done

labelbox/schema/invite.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22

3-
from labelbox.orm.db_object import DbObject, beta
3+
from labelbox.orm.db_object import DbObject
44
from labelbox.orm.model import Field
55
from labelbox.schema.role import ProjectRole
66

@@ -27,6 +27,7 @@ class Invite(DbObject):
2727
email = Field.String("email", "inviteeEmail")
2828

2929
def __init__(self, client, invite_response):
30+
#invite_response.get('inviter')
3031
project_roles = invite_response.pop("projectInvites", [])
3132
super().__init__(client, invite_response)
3233

labelbox/schema/organization.py

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from labelbox.exceptions import LabelboxError
44
from labelbox import utils
5-
from labelbox.orm.db_object import DbObject, beta
5+
from labelbox.orm.db_object import DbObject, experimental, query
66
from labelbox.orm.model import Field, Relationship
77
from labelbox.schema.invite import Invite, InviteLimit, ProjectRole
88
from labelbox.schema.user import User
@@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs):
4545
projects = Relationship.ToMany("Project", True)
4646
webhooks = Relationship.ToMany("Webhook", False)
4747

48-
@beta
48+
@experimental
4949
def invite_user(
5050
self,
5151
email: str,
@@ -62,46 +62,29 @@ def invite_user(
6262
Returns:
6363
Invite for the user
6464
65-
Creates or updates users. This function shouldn't directly be called.
66-
Use `Organization.invite_user`
67-
68-
Note: that there is a really unclear foreign key error if you use an unsupported role.
69-
- This means that the `Roles` object is not getting the right ids
65+
Notes:
66+
This function is currently experimental and has a few limitations that will be resolved in future releases
67+
1. There is a really unclear foreign key error if you use an unsupported role.
68+
- This means that the `Roles` object is not getting the right ids
69+
2. Multiple invites can be sent for the same email. This can only be resolved in the UI for now.
70+
- Future releases of the SDK will support the ability to query and revoke invites to solve this problem (and/or checking on the backend)
71+
3. Some server side response are unclear (e.g. if the user invites themself `None` is returned which the SDK raises as a `LabelboxError` )
7072
"""
71-
remaining_invites = self.invite_limit().remaining
72-
if remaining_invites == 0:
73-
raise LabelboxError(
74-
"Invite(s) cannot be sent because you do not have enough available seats in your organization. "
75-
"Please upgrade your account, revoke pending invitations or remove other users."
76-
)
77-
78-
if not isinstance(role, Role):
79-
raise TypeError(f"role must be Role type. Found {role}")
8073

8174
if project_roles and role.name != "NONE":
8275
raise ValueError(
8376
"Project roles cannot be set for a user with organization level permissions. Found role name `{role.name}`, expected `NONE`"
8477
)
8578

86-
_project_roles = [] if project_roles is None else project_roles
87-
for project_role in _project_roles:
88-
if not isinstance(project_role, ProjectRole):
89-
raise TypeError(
90-
f"project_roles must be a list of `ProjectRole`s. Found {project_role}"
91-
)
92-
93-
if self.client.get_user().email == email:
94-
raise ValueError("Cannot update your own role")
95-
9679
data_param = "data"
9780
query_str = """mutation createInvitesPyApi($%s: [CreateInviteInput!]){
98-
createInvites(data: $%s){ invite { id createdAt organizationRoleName inviteeEmail}}}""" % (
99-
data_param, data_param)
81+
createInvites(data: $%s){ invite { id createdAt organizationRoleName inviteeEmail inviter { %s } }}}""" % (
82+
data_param, data_param, query.results_query_part(User))
10083

10184
projects = [{
10285
"projectId": project_role.project.uid,
10386
"projectRoleId": project_role.role.uid
104-
} for project_role in _project_roles]
87+
} for project_role in project_roles or []]
10588

10689
res = self.client.execute(query_str, {
10790
data_param: [{
@@ -112,11 +95,13 @@ def invite_user(
11295
"projects": projects
11396
}]
11497
},
115-
beta=True)
98+
experimental=True)
11699
invite_response = res['createInvites'][0]['invite']
100+
if not invite_response:
101+
raise LabelboxError(f"Unable to send invite for email {email}")
117102
return Invite(self.client, invite_response)
118103

119-
@beta
104+
@experimental
120105
def invite_limit(self) -> InviteLimit:
121106
""" Retrieve invite limits for the org
122107
This already accounts for users currently in the org
@@ -130,7 +115,7 @@ def invite_limit(self) -> InviteLimit:
130115
res = self.client.execute("""query InvitesLimitPyApi($%s: ID!) {
131116
invitesLimit(where: {id: $%s}) { used limit remaining }
132117
}""" % (org_id_param, org_id_param), {org_id_param: self.uid},
133-
beta=True)
118+
experimental=True)
134119
return InviteLimit(
135120
**{utils.snake_case(k): v for k, v in res['invitesLimit'].items()})
136121

@@ -142,9 +127,6 @@ def remove_user(self, user: User):
142127
user (User): The user to delete from the org
143128
"""
144129

145-
if not isinstance(user, User):
146-
raise TypeError(f"Expected user to be of type User, found {user}")
147-
148130
user_id_param = "userId"
149131
self.client.execute(
150132
"""mutation DeleteMemberPyApi($%s: ID!) {

0 commit comments

Comments
 (0)