lbox-clients 1.1.2__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- lbox/call_info.py +55 -0
- lbox/exceptions.py +201 -0
- lbox/request_client.py +385 -0
- lbox_clients-1.1.2.dist-info/METADATA +38 -0
- lbox_clients-1.1.2.dist-info/RECORD +6 -0
- lbox_clients-1.1.2.dist-info/WHEEL +4 -0
lbox/call_info.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
import inspect
|
2
|
+
import re
|
3
|
+
import sys
|
4
|
+
from typing import TypedDict
|
5
|
+
|
6
|
+
|
7
|
+
def python_version_info():
|
8
|
+
version_info = sys.version_info
|
9
|
+
|
10
|
+
return f"{version_info.major}.{version_info.minor}.{version_info.micro}-{version_info.releaselevel}"
|
11
|
+
|
12
|
+
|
13
|
+
LABELBOX_CALL_PATTERN = re.compile(r"/labelbox/")
|
14
|
+
TEST_FILE_PATTERN = re.compile(r".*test.*\.py$")
|
15
|
+
|
16
|
+
|
17
|
+
class _RequestInfo(TypedDict):
|
18
|
+
prefix: str
|
19
|
+
class_name: str
|
20
|
+
method_name: str
|
21
|
+
|
22
|
+
|
23
|
+
def call_info():
|
24
|
+
method_name: str = "Unknown"
|
25
|
+
prefix = ""
|
26
|
+
class_name = ""
|
27
|
+
skip_methods = ["wrapper", "__init__", "execute"]
|
28
|
+
skip_classes = ["PaginatedCollection", "_CursorPagination", "_OffsetPagination"]
|
29
|
+
|
30
|
+
try:
|
31
|
+
call_info = None
|
32
|
+
for stack in reversed(inspect.stack()):
|
33
|
+
if LABELBOX_CALL_PATTERN.search(stack.filename):
|
34
|
+
call_info = stack
|
35
|
+
method_name: str = call_info.function
|
36
|
+
class_name = call_info.frame.f_locals.get(
|
37
|
+
"self", None
|
38
|
+
).__class__.__name__
|
39
|
+
if method_name not in skip_methods:
|
40
|
+
if class_name not in skip_classes:
|
41
|
+
if TEST_FILE_PATTERN.search(call_info.filename):
|
42
|
+
prefix = "test:"
|
43
|
+
else:
|
44
|
+
if class_name == "NoneType":
|
45
|
+
class_name = ""
|
46
|
+
break
|
47
|
+
|
48
|
+
except Exception:
|
49
|
+
pass
|
50
|
+
return _RequestInfo(prefix=prefix, class_name=class_name, method_name=method_name)
|
51
|
+
|
52
|
+
|
53
|
+
def call_info_as_str():
|
54
|
+
info: _RequestInfo = call_info()
|
55
|
+
return f"{info['prefix']}{info['class_name']}:{info['method_name']}"
|
lbox/exceptions.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
|
4
|
+
class LabelboxError(Exception):
|
5
|
+
"""Base class for exceptions."""
|
6
|
+
|
7
|
+
def __init__(self, message, cause=None):
|
8
|
+
"""
|
9
|
+
Args:
|
10
|
+
message (str): Informative message about the exception.
|
11
|
+
cause (Exception): The cause of the exception (an Exception
|
12
|
+
raised by Python or another library). Optional.
|
13
|
+
"""
|
14
|
+
super().__init__(message, cause)
|
15
|
+
self.message = message
|
16
|
+
self.cause = cause
|
17
|
+
|
18
|
+
def __str__(self):
|
19
|
+
exception_message = self.message
|
20
|
+
if self.cause is not None:
|
21
|
+
exception_message += " (caused by: %s)" % self.cause
|
22
|
+
return exception_message
|
23
|
+
|
24
|
+
|
25
|
+
class AuthenticationError(LabelboxError):
|
26
|
+
"""Raised when an API key fails authentication."""
|
27
|
+
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
class AuthorizationError(LabelboxError):
|
32
|
+
"""Raised when a user is unauthorized to perform the given request."""
|
33
|
+
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class ResourceNotFoundError(LabelboxError):
|
38
|
+
"""Exception raised when a given resource is not found."""
|
39
|
+
|
40
|
+
def __init__(self, db_object_type=None, params=None, message=None):
|
41
|
+
"""Constructor for the ResourceNotFoundException class.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
db_object_type (type): A subtype of labelbox.schema.DbObject.
|
45
|
+
params (dict): A dictionary of parameters identifying the sought resource.
|
46
|
+
message (str): An optional message to include in the exception.
|
47
|
+
"""
|
48
|
+
if message is not None:
|
49
|
+
super().__init__(message)
|
50
|
+
else:
|
51
|
+
super().__init__(
|
52
|
+
"Resource '%s' not found for params: %r"
|
53
|
+
% (db_object_type.type_name(), params)
|
54
|
+
)
|
55
|
+
self.db_object_type = db_object_type
|
56
|
+
self.params = params
|
57
|
+
|
58
|
+
|
59
|
+
class ResourceConflict(LabelboxError):
|
60
|
+
"""Exception raised when a given resource conflicts with another."""
|
61
|
+
|
62
|
+
pass
|
63
|
+
|
64
|
+
|
65
|
+
class ValidationFailedError(LabelboxError):
|
66
|
+
"""Exception raised for when a GraphQL query fails validation (query cost,
|
67
|
+
etc.) E.g. a query that is too expensive, or depth is too deep.
|
68
|
+
"""
|
69
|
+
|
70
|
+
pass
|
71
|
+
|
72
|
+
|
73
|
+
class InternalServerError(LabelboxError):
|
74
|
+
"""Nondescript prisma or 502 related errors.
|
75
|
+
|
76
|
+
Meant to be retryable.
|
77
|
+
|
78
|
+
TODO: these errors need better messages from platform
|
79
|
+
"""
|
80
|
+
|
81
|
+
pass
|
82
|
+
|
83
|
+
|
84
|
+
class InvalidQueryError(LabelboxError):
|
85
|
+
"""Indicates a malconstructed or unsupported query (either by GraphQL in
|
86
|
+
general or by Labelbox specifically). This can be the result of either client
|
87
|
+
or server side query validation."""
|
88
|
+
|
89
|
+
pass
|
90
|
+
|
91
|
+
|
92
|
+
class UnprocessableEntityError(LabelboxError):
|
93
|
+
"""Indicates that a resource could not be created in the server side
|
94
|
+
due to a validation or transaction error"""
|
95
|
+
|
96
|
+
pass
|
97
|
+
|
98
|
+
|
99
|
+
class ResourceCreationError(LabelboxError):
|
100
|
+
"""Indicates that a resource could not be created in the server side
|
101
|
+
due to a validation or transaction error"""
|
102
|
+
|
103
|
+
pass
|
104
|
+
|
105
|
+
|
106
|
+
class NetworkError(LabelboxError):
|
107
|
+
"""Raised when an HTTPError occurs."""
|
108
|
+
|
109
|
+
def __init__(self, cause):
|
110
|
+
super().__init__(str(cause), cause)
|
111
|
+
self.cause = cause
|
112
|
+
|
113
|
+
|
114
|
+
class TimeoutError(LabelboxError):
|
115
|
+
"""Raised when a request times-out."""
|
116
|
+
|
117
|
+
pass
|
118
|
+
|
119
|
+
|
120
|
+
class InvalidAttributeError(LabelboxError):
|
121
|
+
"""Raised when a field (name or Field instance) is not valid or found
|
122
|
+
for a specific DB object type."""
|
123
|
+
|
124
|
+
def __init__(self, db_object_type, field):
|
125
|
+
super().__init__(
|
126
|
+
"Field(s) '%r' not valid on DB type '%s'"
|
127
|
+
% (field, db_object_type.type_name())
|
128
|
+
)
|
129
|
+
self.db_object_type = db_object_type
|
130
|
+
self.field = field
|
131
|
+
|
132
|
+
|
133
|
+
class ApiLimitError(LabelboxError):
|
134
|
+
"""Raised when the user performs too many requests in a short period
|
135
|
+
of time."""
|
136
|
+
|
137
|
+
pass
|
138
|
+
|
139
|
+
|
140
|
+
class MalformedQueryException(Exception):
|
141
|
+
"""Raised when the user submits a malformed query."""
|
142
|
+
|
143
|
+
pass
|
144
|
+
|
145
|
+
|
146
|
+
class UuidError(LabelboxError):
|
147
|
+
"""Raised when there are repeat Uuid's in bulk import request."""
|
148
|
+
|
149
|
+
pass
|
150
|
+
|
151
|
+
|
152
|
+
class InconsistentOntologyException(Exception):
|
153
|
+
pass
|
154
|
+
|
155
|
+
|
156
|
+
class MALValidationError(LabelboxError):
|
157
|
+
"""Raised when user input is invalid for MAL imports."""
|
158
|
+
|
159
|
+
pass
|
160
|
+
|
161
|
+
|
162
|
+
class OperationNotAllowedException(Exception):
|
163
|
+
"""Raised when user does not have permissions to a resource or has exceeded usage limit"""
|
164
|
+
|
165
|
+
pass
|
166
|
+
|
167
|
+
|
168
|
+
class OperationNotSupportedException(Exception):
|
169
|
+
"""Raised when sdk does not support requested operation"""
|
170
|
+
|
171
|
+
pass
|
172
|
+
|
173
|
+
|
174
|
+
class ConfidenceNotSupportedException(Exception):
|
175
|
+
"""Raised when confidence is specified for unsupported annotation type"""
|
176
|
+
|
177
|
+
|
178
|
+
class CustomMetricsNotSupportedException(Exception):
|
179
|
+
"""Raised when custom_metrics is specified for unsupported annotation type"""
|
180
|
+
|
181
|
+
|
182
|
+
class ProcessingWaitTimeout(Exception):
|
183
|
+
"""Raised when waiting for the data rows to be processed takes longer than allowed"""
|
184
|
+
|
185
|
+
|
186
|
+
def error_message_for_unparsed_graphql_error(error_string: str) -> str:
|
187
|
+
"""
|
188
|
+
Since our client only parses certain graphql errors, this function is used to
|
189
|
+
extract the error message from the error string when the error is not
|
190
|
+
parsed by the client.
|
191
|
+
"""
|
192
|
+
# Regex to find the message content
|
193
|
+
pattern = r"'message': '([^']+)'"
|
194
|
+
# Search for the pattern in the error string
|
195
|
+
match = re.search(pattern, error_string)
|
196
|
+
if match:
|
197
|
+
error_content = match.group(1)
|
198
|
+
else:
|
199
|
+
error_content = "Unknown error"
|
200
|
+
|
201
|
+
return error_content
|
lbox/request_client.py
ADDED
@@ -0,0 +1,385 @@
|
|
1
|
+
# for the Labelbox Python SDK
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from types import MappingProxyType
|
7
|
+
from typing import Callable, Dict, Optional
|
8
|
+
|
9
|
+
import requests
|
10
|
+
import requests.exceptions
|
11
|
+
from google.api_core import retry
|
12
|
+
from lbox import exceptions
|
13
|
+
from lbox.call_info import call_info_as_str, python_version_info # type: ignore
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
_LABELBOX_API_KEY = "LABELBOX_API_KEY"
|
18
|
+
|
19
|
+
|
20
|
+
class RequestClient:
|
21
|
+
"""A Labelbox request client.
|
22
|
+
|
23
|
+
Contains info necessary for connecting to a Labelbox server (URL,
|
24
|
+
authentication key).
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
sdk_version,
|
30
|
+
api_key=None,
|
31
|
+
endpoint="https://api.labelbox.com/graphql",
|
32
|
+
enable_experimental=False,
|
33
|
+
app_url="https://app.labelbox.com",
|
34
|
+
rest_endpoint="https://api.labelbox.com/api/v1",
|
35
|
+
):
|
36
|
+
"""Creates and initializes a RequestClient.
|
37
|
+
This class executes graphql and rest requests to the Labelbox server.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
api_key (str): API key. If None, the key is obtained from the "LABELBOX_API_KEY" environment variable.
|
41
|
+
endpoint (str): URL of the Labelbox server to connect to.
|
42
|
+
enable_experimental (bool): Indicates whether or not to use experimental features
|
43
|
+
app_url (str) : host url for all links to the web app
|
44
|
+
Raises:
|
45
|
+
exceptions.AuthenticationError: If no `api_key`
|
46
|
+
is provided as an argument or via the environment
|
47
|
+
variable.
|
48
|
+
"""
|
49
|
+
if api_key is None:
|
50
|
+
if _LABELBOX_API_KEY not in os.environ:
|
51
|
+
raise exceptions.AuthenticationError("Labelbox API key not provided")
|
52
|
+
api_key = os.environ[_LABELBOX_API_KEY]
|
53
|
+
self.api_key = api_key
|
54
|
+
|
55
|
+
self.enable_experimental = enable_experimental
|
56
|
+
if enable_experimental:
|
57
|
+
logger.info("Experimental features have been enabled")
|
58
|
+
|
59
|
+
logger.info("Initializing Labelbox client at '%s'", endpoint)
|
60
|
+
self.app_url = app_url
|
61
|
+
self.endpoint = endpoint
|
62
|
+
self.rest_endpoint = rest_endpoint
|
63
|
+
self.sdk_version = sdk_version
|
64
|
+
self._sdk_method = None
|
65
|
+
self._connection: requests.Session = self._init_connection()
|
66
|
+
|
67
|
+
def _init_connection(self) -> requests.Session:
|
68
|
+
connection = requests.Session() # using default connection pool size of 10
|
69
|
+
connection.headers.update(self._default_headers())
|
70
|
+
|
71
|
+
return connection
|
72
|
+
|
73
|
+
@property
|
74
|
+
def headers(self) -> MappingProxyType:
|
75
|
+
return self._connection.headers
|
76
|
+
|
77
|
+
@property
|
78
|
+
def sdk_method(self):
|
79
|
+
return self._sdk_method
|
80
|
+
|
81
|
+
@sdk_method.setter
|
82
|
+
def sdk_method(self, value):
|
83
|
+
self._sdk_method = value
|
84
|
+
|
85
|
+
def _default_headers(self):
|
86
|
+
return {
|
87
|
+
"Authorization": "Bearer %s" % self.api_key,
|
88
|
+
"Accept": "application/json",
|
89
|
+
"Content-Type": "application/json",
|
90
|
+
"X-User-Agent": f"python-sdk {self.sdk_version}",
|
91
|
+
"X-Python-Version": f"{python_version_info()}",
|
92
|
+
}
|
93
|
+
|
94
|
+
@retry.Retry(
|
95
|
+
predicate=retry.if_exception_type(
|
96
|
+
exceptions.InternalServerError,
|
97
|
+
exceptions.TimeoutError,
|
98
|
+
)
|
99
|
+
)
|
100
|
+
def execute(
|
101
|
+
self,
|
102
|
+
query=None,
|
103
|
+
params=None,
|
104
|
+
data=None,
|
105
|
+
files=None,
|
106
|
+
timeout=60.0,
|
107
|
+
experimental=False,
|
108
|
+
error_log_key="message",
|
109
|
+
raise_return_resource_not_found=False,
|
110
|
+
error_handlers: Optional[
|
111
|
+
Dict[str, Callable[[requests.models.Response], None]]
|
112
|
+
] = None,
|
113
|
+
):
|
114
|
+
"""Sends a request to the server for the execution of the
|
115
|
+
given query.
|
116
|
+
|
117
|
+
Checks the response for errors and wraps errors
|
118
|
+
in appropriate `exceptions.LabelboxError` subtypes.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
query (str): The query to execute.
|
122
|
+
params (dict): Query parameters referenced within the query.
|
123
|
+
data (str): json string containing the query to execute
|
124
|
+
files (dict): file arguments for request
|
125
|
+
timeout (float): Max allowed time for query execution,
|
126
|
+
in seconds.
|
127
|
+
raise_return_resource_not_found: By default the client relies on the caller to raise the correct exception when a resource is not found.
|
128
|
+
If this is set to True, the client will raise a ResourceNotFoundError exception automatically.
|
129
|
+
This simplifies processing.
|
130
|
+
We recommend to use it only of api returns a clear and well-formed error when a resource not found for a given query.
|
131
|
+
error_handlers (dict): A dictionary mapping graphql error code to handler functions.
|
132
|
+
Allows a caller to handle specific errors reporting in a custom way or produce more user-friendly readable messages.
|
133
|
+
|
134
|
+
Example - custom error handler:
|
135
|
+
>>> def _raise_readable_errors(self, response):
|
136
|
+
>>> errors = response.json().get('errors', [])
|
137
|
+
>>> if errors:
|
138
|
+
>>> message = errors[0].get(
|
139
|
+
>>> 'message', json.dumps([{
|
140
|
+
>>> "errorMessage": "Unknown error"
|
141
|
+
>>> }]))
|
142
|
+
>>> errors = json.loads(message)
|
143
|
+
>>> error_messages = [error['errorMessage'] for error in errors]
|
144
|
+
>>> else:
|
145
|
+
>>> error_messages = ["Uknown error"]
|
146
|
+
>>> raise LabelboxError(". ".join(error_messages))
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
dict, parsed JSON response.
|
150
|
+
Raises:
|
151
|
+
exceptions.AuthenticationError: If authentication
|
152
|
+
failed.
|
153
|
+
exceptions.InvalidQueryError: If `query` is not
|
154
|
+
syntactically or semantically valid (checked server-side).
|
155
|
+
exceptions.ApiLimitError: If the server API limit was
|
156
|
+
exceeded. See "How to import data" in the online documentation
|
157
|
+
to see API limits.
|
158
|
+
exceptions.TimeoutError: If response was not received
|
159
|
+
in `timeout` seconds.
|
160
|
+
exceptions.NetworkError: If an unknown error occurred
|
161
|
+
most likely due to connection issues.
|
162
|
+
exceptions.LabelboxError: If an unknown error of any
|
163
|
+
kind occurred.
|
164
|
+
ValueError: If query and data are both None.
|
165
|
+
"""
|
166
|
+
logger.debug("Query: %s, params: %r, data %r", query, params, data)
|
167
|
+
|
168
|
+
# Convert datetimes to UTC strings.
|
169
|
+
def convert_value(value):
|
170
|
+
if isinstance(value, datetime):
|
171
|
+
value = value.astimezone(timezone.utc)
|
172
|
+
value = value.strftime("%Y-%m-%dT%H:%M:%SZ")
|
173
|
+
return value
|
174
|
+
|
175
|
+
if query is not None:
|
176
|
+
if params is not None:
|
177
|
+
params = {key: convert_value(value) for key, value in params.items()}
|
178
|
+
data = json.dumps({"query": query, "variables": params}).encode("utf-8")
|
179
|
+
elif data is None:
|
180
|
+
raise ValueError("query and data cannot both be none")
|
181
|
+
|
182
|
+
endpoint = (
|
183
|
+
self.endpoint
|
184
|
+
if not experimental
|
185
|
+
else self.endpoint.replace("/graphql", "/_gql")
|
186
|
+
)
|
187
|
+
|
188
|
+
try:
|
189
|
+
headers = self._connection.headers.copy()
|
190
|
+
if files:
|
191
|
+
del headers["Content-Type"]
|
192
|
+
del headers["Accept"]
|
193
|
+
headers["X-SDK-Method"] = (
|
194
|
+
self.sdk_method if self.sdk_method else call_info_as_str()
|
195
|
+
)
|
196
|
+
|
197
|
+
request = requests.Request(
|
198
|
+
"POST",
|
199
|
+
endpoint,
|
200
|
+
headers=headers,
|
201
|
+
data=data,
|
202
|
+
files=files if files else None,
|
203
|
+
)
|
204
|
+
|
205
|
+
prepped: requests.PreparedRequest = request.prepare()
|
206
|
+
|
207
|
+
settings = self._connection.merge_environment_settings(
|
208
|
+
prepped.url, {}, None, None, None
|
209
|
+
)
|
210
|
+
response = self._connection.send(prepped, timeout=timeout, **settings)
|
211
|
+
logger.debug("Response: %s", response.text)
|
212
|
+
except requests.exceptions.Timeout as e:
|
213
|
+
raise exceptions.TimeoutError(str(e))
|
214
|
+
except requests.exceptions.RequestException as e:
|
215
|
+
logger.error("Unknown error: %s", str(e))
|
216
|
+
raise exceptions.NetworkError(e)
|
217
|
+
except Exception as e:
|
218
|
+
raise exceptions.LabelboxError(
|
219
|
+
"Unknown error during Client.query(): " + str(e), e
|
220
|
+
)
|
221
|
+
|
222
|
+
if (
|
223
|
+
200 <= response.status_code < 300
|
224
|
+
or response.status_code < 500
|
225
|
+
or response.status_code >= 600
|
226
|
+
):
|
227
|
+
try:
|
228
|
+
r_json = response.json()
|
229
|
+
except Exception:
|
230
|
+
raise exceptions.LabelboxError(
|
231
|
+
"Failed to parse response as JSON: %s" % response.text
|
232
|
+
)
|
233
|
+
else:
|
234
|
+
if (
|
235
|
+
"upstream connect error or disconnect/reset before headers"
|
236
|
+
in response.text
|
237
|
+
):
|
238
|
+
raise exceptions.InternalServerError("Connection reset")
|
239
|
+
elif response.status_code == 502:
|
240
|
+
error_502 = "502 Bad Gateway"
|
241
|
+
raise exceptions.InternalServerError(error_502)
|
242
|
+
elif 500 <= response.status_code < 600:
|
243
|
+
error_500 = f"Internal server http error {response.status_code}"
|
244
|
+
raise exceptions.InternalServerError(error_500)
|
245
|
+
|
246
|
+
errors = r_json.get("errors", [])
|
247
|
+
|
248
|
+
def check_errors(keywords, *path):
|
249
|
+
"""Helper that looks for any of the given `keywords` in any of
|
250
|
+
current errors on paths (like error[path][component][to][keyword]).
|
251
|
+
"""
|
252
|
+
for error in errors:
|
253
|
+
obj = error
|
254
|
+
for path_elem in path:
|
255
|
+
obj = obj.get(path_elem, {})
|
256
|
+
if obj in keywords:
|
257
|
+
return error
|
258
|
+
return None
|
259
|
+
|
260
|
+
def get_error_status_code(error: dict) -> int:
|
261
|
+
try:
|
262
|
+
return int(error["extensions"].get("exception").get("status"))
|
263
|
+
except Exception:
|
264
|
+
return 500
|
265
|
+
|
266
|
+
if check_errors(["AUTHENTICATION_ERROR"], "extensions", "code") is not None:
|
267
|
+
raise exceptions.AuthenticationError("Invalid API key")
|
268
|
+
|
269
|
+
authorization_error = check_errors(
|
270
|
+
["AUTHORIZATION_ERROR"], "extensions", "code"
|
271
|
+
)
|
272
|
+
if authorization_error is not None:
|
273
|
+
raise exceptions.AuthorizationError(authorization_error["message"])
|
274
|
+
|
275
|
+
validation_error = check_errors(
|
276
|
+
["GRAPHQL_VALIDATION_FAILED"], "extensions", "code"
|
277
|
+
)
|
278
|
+
|
279
|
+
if validation_error is not None:
|
280
|
+
message = validation_error["message"]
|
281
|
+
if message == "Query complexity limit exceeded":
|
282
|
+
raise exceptions.ValidationFailedError(message)
|
283
|
+
else:
|
284
|
+
raise exceptions.InvalidQueryError(message)
|
285
|
+
|
286
|
+
graphql_error = check_errors(["GRAPHQL_PARSE_FAILED"], "extensions", "code")
|
287
|
+
if graphql_error is not None:
|
288
|
+
raise exceptions.InvalidQueryError(graphql_error["message"])
|
289
|
+
|
290
|
+
# Check if API limit was exceeded
|
291
|
+
response_msg = r_json.get("message", "")
|
292
|
+
|
293
|
+
if response_msg.startswith("You have exceeded"):
|
294
|
+
raise exceptions.ApiLimitError(response_msg)
|
295
|
+
|
296
|
+
resource_not_found_error = check_errors(
|
297
|
+
["RESOURCE_NOT_FOUND"], "extensions", "code"
|
298
|
+
)
|
299
|
+
if resource_not_found_error is not None:
|
300
|
+
if raise_return_resource_not_found:
|
301
|
+
raise exceptions.ResourceNotFoundError(
|
302
|
+
message=resource_not_found_error["message"]
|
303
|
+
)
|
304
|
+
else:
|
305
|
+
# Return None and let the caller methods raise an exception
|
306
|
+
# as they already know which resource type and ID was requested
|
307
|
+
return None
|
308
|
+
|
309
|
+
resource_conflict_error = check_errors(
|
310
|
+
["RESOURCE_CONFLICT"], "extensions", "code"
|
311
|
+
)
|
312
|
+
if resource_conflict_error is not None:
|
313
|
+
raise exceptions.ResourceConflict(resource_conflict_error["message"])
|
314
|
+
|
315
|
+
malformed_request_error = check_errors(
|
316
|
+
["MALFORMED_REQUEST"], "extensions", "code"
|
317
|
+
)
|
318
|
+
|
319
|
+
error_code = "MALFORMED_REQUEST"
|
320
|
+
if malformed_request_error is not None:
|
321
|
+
if error_handlers and error_code in error_handlers:
|
322
|
+
handler = error_handlers[error_code]
|
323
|
+
handler(response)
|
324
|
+
return None
|
325
|
+
raise exceptions.MalformedQueryException(
|
326
|
+
malformed_request_error[error_log_key]
|
327
|
+
)
|
328
|
+
|
329
|
+
# A lot of different error situations are now labeled serverside
|
330
|
+
# as INTERNAL_SERVER_ERROR, when they are actually client errors.
|
331
|
+
# TODO: fix this in the server API
|
332
|
+
internal_server_error = check_errors(
|
333
|
+
["INTERNAL_SERVER_ERROR"], "extensions", "code"
|
334
|
+
)
|
335
|
+
error_code = "INTERNAL_SERVER_ERROR"
|
336
|
+
|
337
|
+
if internal_server_error is not None:
|
338
|
+
if error_handlers and error_code in error_handlers:
|
339
|
+
handler = error_handlers[error_code]
|
340
|
+
handler(response)
|
341
|
+
return None
|
342
|
+
message = internal_server_error.get("message")
|
343
|
+
error_status_code = get_error_status_code(internal_server_error)
|
344
|
+
if error_status_code == 400:
|
345
|
+
raise exceptions.InvalidQueryError(message)
|
346
|
+
elif error_status_code == 422:
|
347
|
+
raise exceptions.UnprocessableEntityError(message)
|
348
|
+
elif error_status_code == 426:
|
349
|
+
raise exceptions.OperationNotAllowedException(message)
|
350
|
+
elif error_status_code == 500:
|
351
|
+
raise exceptions.LabelboxError(message)
|
352
|
+
else:
|
353
|
+
raise exceptions.InternalServerError(message)
|
354
|
+
|
355
|
+
not_allowed_error = check_errors(
|
356
|
+
["OPERATION_NOT_ALLOWED"], "extensions", "code"
|
357
|
+
)
|
358
|
+
if not_allowed_error is not None:
|
359
|
+
message = not_allowed_error.get("message")
|
360
|
+
raise exceptions.OperationNotAllowedException(message)
|
361
|
+
|
362
|
+
if len(errors) > 0:
|
363
|
+
logger.warning("Unparsed errors on query execution: %r", errors)
|
364
|
+
messages = list(
|
365
|
+
map(
|
366
|
+
lambda x: {
|
367
|
+
"message": x["message"],
|
368
|
+
"code": x["extensions"]["code"],
|
369
|
+
},
|
370
|
+
errors,
|
371
|
+
)
|
372
|
+
)
|
373
|
+
raise exceptions.LabelboxError("Unknown error: %s" % str(messages))
|
374
|
+
|
375
|
+
# if we do return a proper error code, and didn't catch this above
|
376
|
+
# reraise
|
377
|
+
# this mainly catches a 401 for API access disabled for free tier
|
378
|
+
# TODO: need to unify API errors to handle things more uniformly
|
379
|
+
# in the SDK
|
380
|
+
if response.status_code != requests.codes.ok:
|
381
|
+
message = f"{response.status_code} {response.reason}"
|
382
|
+
cause = r_json.get("message")
|
383
|
+
raise exceptions.LabelboxError(message, cause)
|
384
|
+
|
385
|
+
return r_json["data"]
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: lbox-clients
|
3
|
+
Version: 1.1.2
|
4
|
+
Summary: This module contains client sdk uses to conntect to the Labelbox API and backends
|
5
|
+
Project-URL: Homepage, https://labelbox.com/
|
6
|
+
Project-URL: Documentation, https://labelbox-python.readthedocs.io/en/latest/
|
7
|
+
Project-URL: Repository, https://github.com/Labelbox/labelbox-python
|
8
|
+
Project-URL: Issues, https://github.com/Labelbox/labelbox-python/issues
|
9
|
+
Project-URL: Changelog, https://github.com/Labelbox/labelbox-python/blob/develop/libs/labelbox/CHANGELOG.md
|
10
|
+
Author-email: Labelbox <engineering@labelbox.com>
|
11
|
+
Keywords: ai,edu,labelbox,labeling,llm,machinelearning,ml
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
13
|
+
Classifier: Intended Audience :: Developers
|
14
|
+
Classifier: Intended Audience :: Education
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
25
|
+
Requires-Python: >=3.9
|
26
|
+
Requires-Dist: google-api-core>=1.22.1
|
27
|
+
Requires-Dist: requests>=2.22.0
|
28
|
+
Description-Content-Type: text/markdown
|
29
|
+
|
30
|
+
# lbox-clients
|
31
|
+
|
32
|
+
This is an example module which can be cloned and reused to develop modules under the `lbox` namespace.
|
33
|
+
|
34
|
+
## Module Status: Experimental
|
35
|
+
|
36
|
+
**TLDR: This module may be removed or altered at any given time and there is no offical support.**
|
37
|
+
|
38
|
+
Please see [here](https://docs.labelbox.com/docs/product-release-phases) for the formal definition of `Experimental`.
|
@@ -0,0 +1,6 @@
|
|
1
|
+
lbox/call_info.py,sha256=XU1XPugM5k1ifqUj0YP1Igj8-2CrKfbQE3GCaQyP3nY,1668
|
2
|
+
lbox/exceptions.py,sha256=PHdvyHjXK3mSF8MlRW9cy6HBlqMxbH1PcfPjL_QGdiU,5353
|
3
|
+
lbox/request_client.py,sha256=Vqb57Gk6OGmlk6Q6hSabyV-UE5HCXWTeCGiSexi_gDk,15160
|
4
|
+
lbox_clients-1.1.2.dist-info/METADATA,sha256=QRHIM0AW0B9KdqjtqANCkQdfXKak8_2_b_vG7jS9RVQ,1816
|
5
|
+
lbox_clients-1.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
6
|
+
lbox_clients-1.1.2.dist-info/RECORD,,
|