diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 72bd7e424..6e3859658 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -28,6 +28,21 @@ def url_join(root_url: str, path: str): return urljoin(root_url.rstrip('/') + '/', path.lstrip('/')) +class OpenEoApiError(Exception): + """ + Error returned by OpenEO API according to https://open-eo.github.io/openeo-api/errors/ + """ + + def __init__(self, http_status_code: int = None, + code: str = 'unknown', message: str = 'unknown error', id: str = None, url: str = None): + self.http_status_code = http_status_code + self.code = code + self.message = message + self.id = id + self.url = url + super().__init__("[{s}] {c}: {m}".format(s=self.http_status_code, c=self.code, m=self.message)) + + class RestApiConnection: """Base connection class implementing generic REST API request functionality""" @@ -59,10 +74,28 @@ def request(self, method: str, path: str, headers: dict = None, auth: AuthBase = **kwargs ) if check_status: - # TODO: raise a custom OpenEO branded exception? - resp.raise_for_status() + # TODO: option to specify the list/range of expected status codes? + if resp.status_code >= 400: + self._raise_api_error(resp) return resp + def _raise_api_error(self, response: requests.Response): + """Convert API error response to Python exception""" + try: + # Try parsing the error info according to spec and wrap it in an exception. + info = response.json() + exception = OpenEoApiError( + http_status_code=response.status_code, + code=info.get("code", "unknown"), + message=info.get("message", "unknown error"), + id=info.get("id"), + url=info.get("url"), + ) + except Exception: + # When parsing went wrong: give minimal information. + exception = OpenEoApiError(http_status_code=response.status_code, message=response.text) + raise exception + def get(self, path, stream=False, auth: AuthBase = None, **kwargs) -> Response: """ Do GET request to REST API. @@ -422,6 +455,7 @@ def parse_json_response(self, response: requests.Response): self._handle_error_response(response) def _handle_error_response(self, response): + # TODO replace this with `_raise_api_error` if response.status_code == 502: from requests.exceptions import ProxyError raise ProxyError("The proxy returned an error, this could be due to a timeout.") diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index ff3ca9666..2d348a72e 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -4,7 +4,7 @@ import requests_mock from openeo.rest.auth.auth import NullAuth, BearerAuth -from openeo.rest.connection import Connection, RestApiConnection, connect +from openeo.rest.connection import Connection, RestApiConnection, connect, OpenEoApiError API_URL = "https://oeo.net/" @@ -72,6 +72,34 @@ def test_connect_with_session(): ) +def test_api_error(requests_mock): + conn = Connection(API_URL) + requests_mock.get('https://oeo.net/collections/foobar', status_code=404, json={ + "code": "CollectionNotFound", "message": "No such things as a collection 'foobar'", "id": "54321" + }) + with pytest.raises(OpenEoApiError) as exc_info: + conn.describe_collection("foobar") + exc = exc_info.value + assert exc.http_status_code == 404 + assert exc.code == "CollectionNotFound" + assert exc.message == "No such things as a collection 'foobar'" + assert exc.id == "54321" + assert exc.url is None + + +def test_api_error_non_json(requests_mock): + conn = Connection(API_URL) + requests_mock.get('https://oeo.net/collections/foobar', status_code=500, text="olapola") + with pytest.raises(OpenEoApiError) as exc_info: + conn.describe_collection("foobar") + exc = exc_info.value + assert exc.http_status_code == 500 + assert exc.code == "unknown" + assert exc.message == "olapola" + assert exc.id is None + assert exc.url is None + + def test_authenticate_basic(requests_mock): conn = Connection(API_URL)