-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♻️ update HTTP error handling (#176)
- Loading branch information
1 parent
130edd5
commit 60568fd
Showing
7 changed files
with
251 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,102 @@ | ||
class HTTPException(RuntimeError): | ||
from typing import Union | ||
|
||
from mindee.parsing.common.string_dict import StringDict | ||
|
||
|
||
class MindeeHTTPException(RuntimeError): | ||
"""An exception relating to HTTP calls.""" | ||
|
||
status_code: int | ||
api_code: str | ||
api_details: str | ||
api_message: str | ||
|
||
def __init__(self, http_error: StringDict, url: str, code: int) -> None: | ||
""" | ||
Base exception for HTTP calls. | ||
:param http_error: formatted & parsed error | ||
:param url: url/endpoint the exception was raised on | ||
:param code: HTTP code for the error | ||
""" | ||
self.status_code = code | ||
self.api_code = http_error["code"] if "code" in http_error else None | ||
self.api_details = http_error["details"] if "details" in http_error else None | ||
self.api_message = http_error["message"] if "message" in http_error else None | ||
super().__init__( | ||
f"{url} {self.status_code} HTTP error: {self.api_details} - {self.api_message}" | ||
) | ||
|
||
|
||
class MindeeHTTPClientException(MindeeHTTPException): | ||
"""API Client HTTP exception.""" | ||
|
||
|
||
class MindeeHTTPServerException(MindeeHTTPException): | ||
"""API Server HTTP exception.""" | ||
|
||
|
||
def create_error_obj(response: Union[StringDict, str]) -> StringDict: | ||
""" | ||
Creates an error object based on a requests' payload. | ||
:param response: response as sent by the server, as a dict. | ||
In _very_ rare instances, this can be an html string. | ||
""" | ||
if not isinstance(response, str): | ||
if "api_request" in response and "error" in response["api_request"]: | ||
return response["api_request"]["error"] | ||
raise RuntimeError(f"Could not build specific HTTP exception from '{response}'") | ||
error_dict = {} | ||
if "Maximum pdf pages" in response: | ||
error_dict = { | ||
"code": "TooManyPages", | ||
"message": "Maximum amound of pdf pages reached.", | ||
"details": response, | ||
} | ||
elif "Max file size is" in response: | ||
error_dict = { | ||
"code": "FileTooLarge", | ||
"message": "Maximum file size reached.", | ||
"details": response, | ||
} | ||
elif "Invalid file type" in response: | ||
error_dict = { | ||
"code": "InvalidFiletype", | ||
"message": "Invalid file type.", | ||
"details": response, | ||
} | ||
elif "Gateway timeout" in response: | ||
error_dict = { | ||
"code": "RequestTimeout", | ||
"message": "Request timed out.", | ||
"details": response, | ||
} | ||
elif "Too Many Requests" in response: | ||
error_dict = { | ||
"code": "TooManyRequests", | ||
"message": "Too Many Requests.", | ||
"details": response, | ||
} | ||
else: | ||
error_dict = { | ||
"code": "UnknownError", | ||
"message": "Server sent back an unexpected reply.", | ||
"details": response, | ||
} | ||
return error_dict | ||
|
||
|
||
def handle_error(url: str, response: StringDict, code: int) -> MindeeHTTPException: | ||
""" | ||
Creates an appropriate HTTP error exception, based on retrieved HTTP error code. | ||
:param url: url of the product | ||
:param response: StringDict | ||
""" | ||
error_obj = create_error_obj(response) | ||
if 400 <= code <= 499: | ||
return MindeeHTTPClientException(error_obj, url, code) | ||
if 500 <= code <= 599: | ||
return MindeeHTTPServerException(error_obj, url, code) | ||
return MindeeHTTPException(error_obj, url, code) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import json | ||
from pathlib import Path | ||
|
||
import pytest | ||
|
||
from mindee import Client, product | ||
from mindee.input.sources import PathInput | ||
from mindee.mindee_http.error import ( | ||
MindeeHTTPClientException, | ||
MindeeHTTPServerException, | ||
handle_error, | ||
) | ||
from tests.test_inputs import FILE_TYPES_DIR | ||
from tests.utils import clear_envvars, dummy_envvars | ||
|
||
ERROR_DATA_DIR = Path("./tests/data/errors") | ||
|
||
|
||
@pytest.fixture | ||
def empty_client(monkeypatch) -> Client: | ||
clear_envvars(monkeypatch) | ||
return Client() | ||
|
||
|
||
@pytest.fixture | ||
def dummy_client(monkeypatch) -> Client: | ||
dummy_envvars(monkeypatch) | ||
return Client("dummy") | ||
|
||
|
||
@pytest.fixture | ||
def dummy_file(monkeypatch) -> PathInput: | ||
clear_envvars(monkeypatch) | ||
c = Client(api_key="dummy-client") | ||
return c.source_from_path(FILE_TYPES_DIR / "pdf" / "blank.pdf") | ||
|
||
|
||
def test_http_client_error(dummy_client: Client, dummy_file: PathInput): | ||
with pytest.raises(MindeeHTTPClientException): | ||
dummy_client.parse(product.InvoiceV4, dummy_file) | ||
|
||
|
||
def test_http_enqueue_client_error(dummy_client: Client, dummy_file: PathInput): | ||
with pytest.raises(MindeeHTTPClientException): | ||
dummy_client.enqueue(product.InvoiceV4, dummy_file) | ||
|
||
|
||
def test_http_parse_client_error(dummy_client: Client, dummy_file: PathInput): | ||
with pytest.raises(MindeeHTTPClientException): | ||
dummy_client.parse_queued(product.InvoiceV4, "dummy-queue-id") | ||
|
||
|
||
def test_http_enqueue_and_parse_client_error( | ||
dummy_client: Client, dummy_file: PathInput | ||
): | ||
with pytest.raises(MindeeHTTPClientException): | ||
dummy_client.enqueue_and_parse(product.InvoiceV4, dummy_file) | ||
|
||
|
||
def test_http_400_error(): | ||
error_ref = open(ERROR_DATA_DIR / "error_400_no_details.json") | ||
error_obj = json.load(error_ref) | ||
error_400 = handle_error("dummy-url", error_obj, 400) | ||
with pytest.raises(MindeeHTTPClientException): | ||
raise error_400 | ||
assert error_400.status_code == 400 | ||
assert error_400.api_code == "SomeCode" | ||
assert error_400.api_message == "Some scary message here" | ||
assert error_400.api_details is None | ||
|
||
|
||
def test_http_401_error(): | ||
error_ref = open(ERROR_DATA_DIR / "error_401_invalid_token.json") | ||
error_obj = json.load(error_ref) | ||
error_401 = handle_error("dummy-url", error_obj, 401) | ||
with pytest.raises(MindeeHTTPClientException): | ||
raise error_401 | ||
assert error_401.status_code == 401 | ||
assert error_401.api_code == "Unauthorized" | ||
assert error_401.api_message == "Authorization required" | ||
assert error_401.api_details == "Invalid token provided" | ||
|
||
|
||
def test_http_429_error(): | ||
error_ref = open(ERROR_DATA_DIR / "error_429_too_many_requests.json") | ||
error_obj = json.load(error_ref) | ||
error_429 = handle_error("dummy-url", error_obj, 429) | ||
with pytest.raises(MindeeHTTPClientException): | ||
raise error_429 | ||
assert error_429.status_code == 429 | ||
assert error_429.api_code == "TooManyRequests" | ||
assert error_429.api_message == "Too many requests" | ||
assert error_429.api_details == "Too Many Requests." | ||
|
||
|
||
def test_http_500_error(): | ||
error_ref = open(ERROR_DATA_DIR / "error_500_inference_fail.json") | ||
error_obj = json.load(error_ref) | ||
error_500 = handle_error("dummy-url", error_obj, 500) | ||
with pytest.raises(MindeeHTTPServerException): | ||
raise error_500 | ||
assert error_500.status_code == 500 | ||
assert error_500.api_code == "failure" | ||
assert error_500.api_message == "Inference failed" | ||
assert error_500.api_details == "Can not run prediction: " | ||
|
||
|
||
def test_http_500_html_error(): | ||
error_ref_contents = open(ERROR_DATA_DIR / "error_50x.html").read() | ||
error_500 = handle_error("dummy-url", error_ref_contents, 500) | ||
with pytest.raises(MindeeHTTPServerException): | ||
raise error_500 | ||
assert error_500.status_code == 500 | ||
assert error_500.api_code == "UnknownError" | ||
assert error_500.api_message == "Server sent back an unexpected reply." | ||
assert error_500.api_details == error_ref_contents |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.