Skip to content

Commit

Permalink
[BACKEND-946] [BACKEND-947] [BACKEND-948] pass thru errors, retry, ca…
Browse files Browse the repository at this point in the history
…tch 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
  • Loading branch information
rllin authored and rllin committed Sep 1, 2020
1 parent 674dbd0 commit e9d0325
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 12 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Version 2.4.5 (2020-08-04)
### Added
* retry capabilities for common flaky API failures
* protection against improper types passed into `Project.upload_anntations`
* pass thru API error messages when possible

## Version 2.4.3 (2020-08-04)

### Added
Expand Down
30 changes: 26 additions & 4 deletions labelbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
from typing import Tuple

from google.api_core import retry
import requests
import requests.exceptions

Expand Down Expand Up @@ -60,6 +61,8 @@ def __init__(self,
'Authorization': 'Bearer %s' % api_key
}

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

try:
response = response.json()
r_json = response.json()
except:
error_502 = '502 Bad Gateway'
if error_502 in response.text:
raise labelbox.exceptions.InternalServerError(error_502)
raise labelbox.exceptions.LabelboxError(
"Failed to parse response as JSON: %s" % response.text)

errors = response.get("errors", [])
errors = r_json.get("errors", [])

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

# Check if API limit was exceeded
response_msg = response.get("message", "")
response_msg = r_json.get("message", "")
if response_msg.startswith("You have exceeded"):
raise labelbox.exceptions.ApiLimitError(response_msg)

prisma_error = check_errors(["INTERNAL_SERVER_ERROR"], "extensions",
"code")
if prisma_error:
raise labelbox.exceptions.InternalServerError(
prisma_error["message"])

if len(errors) > 0:
logger.warning("Unparsed errors on query execution: %r", errors)
raise labelbox.exceptions.LabelboxError("Unknown error: %s" %
str(errors))

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

return r_json["data"]

def upload_file(self, path: str) -> str:
"""Uploads given path to local file.
Expand Down
10 changes: 10 additions & 0 deletions labelbox/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ class ValidationFailedError(LabelboxError):
pass


class InternalServerError(LabelboxError):
"""Nondescript prisma or 502 related errors.
Meant to be retryable.
TODO: these errors need better messages from platform
"""
pass


class InvalidQueryError(LabelboxError):
""" Indicates a malconstructed or unsupported query (either by GraphQL in
general or by Labelbox specifically). This can be the result of either client
Expand Down
3 changes: 3 additions & 0 deletions labelbox/schema/bulk_import_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ def create_from_objects(cls, client, project_id: str, name: str,
"""
_validate_ndjson(predictions)
data_str = ndjson.dumps(predictions)
if not data_str:
raise ValueError('annotations cannot be empty')

data = data_str.encode('utf-8')
file_name = _make_file_name(project_id, name)
request_data = _make_request_data(project_id, name, len(data_str),
Expand Down
5 changes: 4 additions & 1 deletion labelbox/schema/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,16 @@ def _is_url_valid(url: Union[str, Path]) -> bool:
file=path,
validate_file=True,
)
else:
elif isinstance(annotations, Iterable):
return BulkImportRequest.create_from_objects(
client=self.client,
project_id=self.uid,
name=name,
predictions=annotations, # type: ignore
)
else:
raise ValueError(
f'Invalid annotations given of type: {type(annotations)}')


class LabelingParameterOverride(DbObject):
Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ ignore_missing_imports = True

[mypy-ndjson.*]
ignore_missing_imports = True

[mypy-google.*]
ignore_missing_imports = True
9 changes: 7 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@

setuptools.setup(
name="labelbox",
version="2.4.4",
version="2.4.5",
author="Labelbox",
author_email="[email protected]",
description="Labelbox Python API",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://labelbox.com",
packages=setuptools.find_packages(),
install_requires=["backoff==1.10.0", "ndjson==0.3.1", "requests==2.22.0"],
install_requires=[
"backoff==1.10.0",
"ndjson==0.3.1",
"requests>=2.22.0",
"google-api-core>=1.22.1",
],
classifiers=[
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: Apache Software License',
Expand Down
13 changes: 8 additions & 5 deletions tests/integration/test_labeling_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

def test_get_labeling_frontends(client):
frontends = list(client.get_labeling_frontends())
assert len(frontends) == 1, frontends
assert len(frontends) >= 1, (
'Projects should have at least one frontend by default.')

# Test filtering
single = list(
client.get_labeling_frontends(where=LabelingFrontend.iframe_url_path ==
frontends[0].iframe_url_path))
assert len(single) == 1, single
target_frontend = frontends[0]
filtered_frontends = client.get_labeling_frontends(
where=LabelingFrontend.iframe_url_path ==
target_frontend.iframe_url_path)
for frontend in filtered_frontends:
assert target_frontend == frontend


def test_labeling_frontend_connecting_to_project(project):
Expand Down

0 comments on commit e9d0325

Please sign in to comment.