Skip to content

Commit e9d0325

Browse files
rllinrllin
authored andcommitted
[BACKEND-946] [BACKEND-947] [BACKEND-948] pass thru errors, retry, catch bad annotation types (#55)
* catch bad inputs * print response when fail * update labeling frontend test to work with any project even if it has more than the default editors * yapf * Update client.py * catch actual errors * loosen requirements + catch actual error for reraise * print 400s * catchall for other errors * retry * ignore missing improrts * update version and changelog
1 parent 674dbd0 commit e9d0325

File tree

8 files changed

+67
-12
lines changed

8 files changed

+67
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Version 2.4.5 (2020-08-04)
4+
### Added
5+
* retry capabilities for common flaky API failures
6+
* protection against improper types passed into `Project.upload_anntations`
7+
* pass thru API error messages when possible
8+
39
## Version 2.4.3 (2020-08-04)
410

511
### Added

labelbox/client.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
from typing import Tuple
77

8+
from google.api_core import retry
89
import requests
910
import requests.exceptions
1011

@@ -60,6 +61,8 @@ def __init__(self,
6061
'Authorization': 'Bearer %s' % api_key
6162
}
6263

64+
@retry.Retry(predicate=retry.if_exception_type(
65+
labelbox.exceptions.InternalServerError))
6366
def execute(self, query, params=None, timeout=10.0):
6467
""" Sends a request to the server for the execution of the
6568
given query. Checks the response for errors and wraps errors
@@ -121,12 +124,15 @@ def convert_value(value):
121124
"Unknown error during Client.query(): " + str(e), e)
122125

123126
try:
124-
response = response.json()
127+
r_json = response.json()
125128
except:
129+
error_502 = '502 Bad Gateway'
130+
if error_502 in response.text:
131+
raise labelbox.exceptions.InternalServerError(error_502)
126132
raise labelbox.exceptions.LabelboxError(
127133
"Failed to parse response as JSON: %s" % response.text)
128134

129-
errors = response.get("errors", [])
135+
errors = r_json.get("errors", [])
130136

131137
def check_errors(keywords, *path):
132138
""" Helper that looks for any of the given `keywords` in any of
@@ -166,16 +172,32 @@ def check_errors(keywords, *path):
166172
graphql_error["message"])
167173

168174
# Check if API limit was exceeded
169-
response_msg = response.get("message", "")
175+
response_msg = r_json.get("message", "")
170176
if response_msg.startswith("You have exceeded"):
171177
raise labelbox.exceptions.ApiLimitError(response_msg)
172178

179+
prisma_error = check_errors(["INTERNAL_SERVER_ERROR"], "extensions",
180+
"code")
181+
if prisma_error:
182+
raise labelbox.exceptions.InternalServerError(
183+
prisma_error["message"])
184+
173185
if len(errors) > 0:
174186
logger.warning("Unparsed errors on query execution: %r", errors)
175187
raise labelbox.exceptions.LabelboxError("Unknown error: %s" %
176188
str(errors))
177189

178-
return response["data"]
190+
# if we do return a proper error code, and didn't catch this above
191+
# reraise
192+
# this mainly catches a 401 for API access disabled for free tier
193+
# TODO: need to unify API errors to handle things more uniformly
194+
# in the SDK
195+
if response.status_code != requests.codes.ok:
196+
message = f"{response.status_code} {response.reason}"
197+
cause = r_json.get('message')
198+
raise labelbox.exceptions.LabelboxError(message, cause)
199+
200+
return r_json["data"]
179201

180202
def upload_file(self, path: str) -> str:
181203
"""Uploads given path to local file.

labelbox/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ class ValidationFailedError(LabelboxError):
4848
pass
4949

5050

51+
class InternalServerError(LabelboxError):
52+
"""Nondescript prisma or 502 related errors.
53+
54+
Meant to be retryable.
55+
56+
TODO: these errors need better messages from platform
57+
"""
58+
pass
59+
60+
5161
class InvalidQueryError(LabelboxError):
5262
""" Indicates a malconstructed or unsupported query (either by GraphQL in
5363
general or by Labelbox specifically). This can be the result of either client

labelbox/schema/bulk_import_request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ def create_from_objects(cls, client, project_id: str, name: str,
222222
"""
223223
_validate_ndjson(predictions)
224224
data_str = ndjson.dumps(predictions)
225+
if not data_str:
226+
raise ValueError('annotations cannot be empty')
227+
225228
data = data_str.encode('utf-8')
226229
file_name = _make_file_name(project_id, name)
227230
request_data = _make_request_data(project_id, name, len(data_str),

labelbox/schema/project.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,13 +410,16 @@ def _is_url_valid(url: Union[str, Path]) -> bool:
410410
file=path,
411411
validate_file=True,
412412
)
413-
else:
413+
elif isinstance(annotations, Iterable):
414414
return BulkImportRequest.create_from_objects(
415415
client=self.client,
416416
project_id=self.uid,
417417
name=name,
418418
predictions=annotations, # type: ignore
419419
)
420+
else:
421+
raise ValueError(
422+
f'Invalid annotations given of type: {type(annotations)}')
420423

421424

422425
class LabelingParameterOverride(DbObject):

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ ignore_missing_imports = True
33

44
[mypy-ndjson.*]
55
ignore_missing_imports = True
6+
7+
[mypy-google.*]
8+
ignore_missing_imports = True

setup.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55

66
setuptools.setup(
77
name="labelbox",
8-
version="2.4.4",
8+
version="2.4.5",
99
author="Labelbox",
1010
author_email="[email protected]",
1111
description="Labelbox Python API",
1212
long_description=long_description,
1313
long_description_content_type="text/markdown",
1414
url="https://labelbox.com",
1515
packages=setuptools.find_packages(),
16-
install_requires=["backoff==1.10.0", "ndjson==0.3.1", "requests==2.22.0"],
16+
install_requires=[
17+
"backoff==1.10.0",
18+
"ndjson==0.3.1",
19+
"requests>=2.22.0",
20+
"google-api-core>=1.22.1",
21+
],
1722
classifiers=[
1823
'Development Status :: 3 - Alpha',
1924
'License :: OSI Approved :: Apache Software License',

tests/integration/test_labeling_frontend.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
def test_get_labeling_frontends(client):
55
frontends = list(client.get_labeling_frontends())
6-
assert len(frontends) == 1, frontends
6+
assert len(frontends) >= 1, (
7+
'Projects should have at least one frontend by default.')
78

89
# Test filtering
9-
single = list(
10-
client.get_labeling_frontends(where=LabelingFrontend.iframe_url_path ==
11-
frontends[0].iframe_url_path))
12-
assert len(single) == 1, single
10+
target_frontend = frontends[0]
11+
filtered_frontends = client.get_labeling_frontends(
12+
where=LabelingFrontend.iframe_url_path ==
13+
target_frontend.iframe_url_path)
14+
for frontend in filtered_frontends:
15+
assert target_frontend == frontend
1316

1417

1518
def test_labeling_frontend_connecting_to_project(project):

0 commit comments

Comments
 (0)