Skip to content

Commit 57dfd13

Browse files
♻️ update HTTP error handling (#176)
1 parent 4cee09c commit 57dfd13

File tree

7 files changed

+251
-26
lines changed

7 files changed

+251
-26
lines changed

examples/display_cropping.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import numpy as np
1717

1818
from mindee import Client, product
19+
from mindee.parsing.common.predict_response import PredictResponse
1920

2021

2122
def relative_to_pixel_pos(polygon, image_h: int, image_w: int) -> List[Tuple[int, int]]:
@@ -66,7 +67,7 @@ def show_image_crops(file_path: str, cropping: list):
6667
input_doc = mindee_client.source_from_path(image_path)
6768

6869
# Parse the document by passing the appropriate type
69-
# api_response = mindee_client.parse(input_doc, product.CropperV1)
70+
api_response: PredictResponse = mindee_client.parse(product.CropperV1, input_doc)
7071

7172
# Display
72-
# show_image_crops(image_path, api_response.pages[0].cropping)
73+
show_image_crops(image_path, api_response.document.inference.pages[0].cropping)

mindee/client.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
from pathlib import Path
32
from time import sleep
43
from typing import BinaryIO, Dict, Optional, Type, Union
@@ -14,7 +13,7 @@
1413
)
1514
from mindee.logger import logger
1615
from mindee.mindee_http.endpoint import CustomEndpoint, Endpoint
17-
from mindee.mindee_http.error import HTTPException
16+
from mindee.mindee_http.error import handle_error
1817
from mindee.mindee_http.mindee_api import MindeeApi
1918
from mindee.parsing.common.async_predict_response import AsyncPredictResponse
2019
from mindee.parsing.common.inference import Inference, TypeInference
@@ -292,8 +291,10 @@ def _make_request(
292291
dict_response = response.json()
293292

294293
if not response.ok:
295-
raise HTTPException(
296-
f"API {response.status_code} HTTP error: {json.dumps(dict_response)}"
294+
raise handle_error(
295+
str(product_class.endpoint_name),
296+
dict_response,
297+
response.status_code,
297298
)
298299

299300
return PredictResponse(product_class, dict_response)
@@ -323,8 +324,10 @@ def _predict_async(
323324
dict_response = response.json()
324325

325326
if not response.ok:
326-
raise HTTPException(
327-
f"API {response.status_code} HTTP error: {json.dumps(dict_response)}"
327+
raise handle_error(
328+
str(product_class.endpoint_name),
329+
dict_response,
330+
response.status_code,
328331
)
329332

330333
return AsyncPredictResponse(product_class, dict_response)
@@ -348,8 +351,11 @@ def _get_queued_document(
348351
or queue_response.status_code < 200
349352
or queue_response.status_code > 302
350353
):
351-
raise HTTPException(
352-
f"API {queue_response.status_code} HTTP error: {json.dumps(queue_response.json())}"
354+
dict_response = queue_response.json()
355+
raise handle_error(
356+
str(product_class.endpoint_name),
357+
dict_response,
358+
queue_response.status_code,
353359
)
354360

355361
return AsyncPredictResponse(product_class, queue_response.json())

mindee/mindee_http/error.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,102 @@
1-
class HTTPException(RuntimeError):
1+
from typing import Union
2+
3+
from mindee.parsing.common.string_dict import StringDict
4+
5+
6+
class MindeeHTTPException(RuntimeError):
27
"""An exception relating to HTTP calls."""
8+
9+
status_code: int
10+
api_code: str
11+
api_details: str
12+
api_message: str
13+
14+
def __init__(self, http_error: StringDict, url: str, code: int) -> None:
15+
"""
16+
Base exception for HTTP calls.
17+
18+
:param http_error: formatted & parsed error
19+
:param url: url/endpoint the exception was raised on
20+
:param code: HTTP code for the error
21+
"""
22+
self.status_code = code
23+
self.api_code = http_error["code"] if "code" in http_error else None
24+
self.api_details = http_error["details"] if "details" in http_error else None
25+
self.api_message = http_error["message"] if "message" in http_error else None
26+
super().__init__(
27+
f"{url} {self.status_code} HTTP error: {self.api_details} - {self.api_message}"
28+
)
29+
30+
31+
class MindeeHTTPClientException(MindeeHTTPException):
32+
"""API Client HTTP exception."""
33+
34+
35+
class MindeeHTTPServerException(MindeeHTTPException):
36+
"""API Server HTTP exception."""
37+
38+
39+
def create_error_obj(response: Union[StringDict, str]) -> StringDict:
40+
"""
41+
Creates an error object based on a requests' payload.
42+
43+
:param response: response as sent by the server, as a dict.
44+
In _very_ rare instances, this can be an html string.
45+
"""
46+
if not isinstance(response, str):
47+
if "api_request" in response and "error" in response["api_request"]:
48+
return response["api_request"]["error"]
49+
raise RuntimeError(f"Could not build specific HTTP exception from '{response}'")
50+
error_dict = {}
51+
if "Maximum pdf pages" in response:
52+
error_dict = {
53+
"code": "TooManyPages",
54+
"message": "Maximum amound of pdf pages reached.",
55+
"details": response,
56+
}
57+
elif "Max file size is" in response:
58+
error_dict = {
59+
"code": "FileTooLarge",
60+
"message": "Maximum file size reached.",
61+
"details": response,
62+
}
63+
elif "Invalid file type" in response:
64+
error_dict = {
65+
"code": "InvalidFiletype",
66+
"message": "Invalid file type.",
67+
"details": response,
68+
}
69+
elif "Gateway timeout" in response:
70+
error_dict = {
71+
"code": "RequestTimeout",
72+
"message": "Request timed out.",
73+
"details": response,
74+
}
75+
elif "Too Many Requests" in response:
76+
error_dict = {
77+
"code": "TooManyRequests",
78+
"message": "Too Many Requests.",
79+
"details": response,
80+
}
81+
else:
82+
error_dict = {
83+
"code": "UnknownError",
84+
"message": "Server sent back an unexpected reply.",
85+
"details": response,
86+
}
87+
return error_dict
88+
89+
90+
def handle_error(url: str, response: StringDict, code: int) -> MindeeHTTPException:
91+
"""
92+
Creates an appropriate HTTP error exception, based on retrieved HTTP error code.
93+
94+
:param url: url of the product
95+
:param response: StringDict
96+
"""
97+
error_obj = create_error_obj(response)
98+
if 400 <= code <= 499:
99+
return MindeeHTTPClientException(error_obj, url, code)
100+
if 500 <= code <= 599:
101+
return MindeeHTTPServerException(error_obj, url, code)
102+
return MindeeHTTPException(error_obj, url, code)

mindee/parsing/common/api_response.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class ApiResponse(ABC):
1212
Serves as a base class for responses to both synchronous and asynchronous calls.
1313
"""
1414

15+
api_request: ApiRequest
16+
"""Results of the request sent to the API."""
1517
raw_http: StringDict
1618
"""Raw request sent by the server, as string."""
1719

tests/mindee_http/test_error.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import json
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from mindee import Client, product
7+
from mindee.input.sources import PathInput
8+
from mindee.mindee_http.error import (
9+
MindeeHTTPClientException,
10+
MindeeHTTPServerException,
11+
handle_error,
12+
)
13+
from tests.test_inputs import FILE_TYPES_DIR
14+
from tests.utils import clear_envvars, dummy_envvars
15+
16+
ERROR_DATA_DIR = Path("./tests/data/errors")
17+
18+
19+
@pytest.fixture
20+
def empty_client(monkeypatch) -> Client:
21+
clear_envvars(monkeypatch)
22+
return Client()
23+
24+
25+
@pytest.fixture
26+
def dummy_client(monkeypatch) -> Client:
27+
dummy_envvars(monkeypatch)
28+
return Client("dummy")
29+
30+
31+
@pytest.fixture
32+
def dummy_file(monkeypatch) -> PathInput:
33+
clear_envvars(monkeypatch)
34+
c = Client(api_key="dummy-client")
35+
return c.source_from_path(FILE_TYPES_DIR / "pdf" / "blank.pdf")
36+
37+
38+
def test_http_client_error(dummy_client: Client, dummy_file: PathInput):
39+
with pytest.raises(MindeeHTTPClientException):
40+
dummy_client.parse(product.InvoiceV4, dummy_file)
41+
42+
43+
def test_http_enqueue_client_error(dummy_client: Client, dummy_file: PathInput):
44+
with pytest.raises(MindeeHTTPClientException):
45+
dummy_client.enqueue(product.InvoiceV4, dummy_file)
46+
47+
48+
def test_http_parse_client_error(dummy_client: Client, dummy_file: PathInput):
49+
with pytest.raises(MindeeHTTPClientException):
50+
dummy_client.parse_queued(product.InvoiceV4, "dummy-queue-id")
51+
52+
53+
def test_http_enqueue_and_parse_client_error(
54+
dummy_client: Client, dummy_file: PathInput
55+
):
56+
with pytest.raises(MindeeHTTPClientException):
57+
dummy_client.enqueue_and_parse(product.InvoiceV4, dummy_file)
58+
59+
60+
def test_http_400_error():
61+
error_ref = open(ERROR_DATA_DIR / "error_400_no_details.json")
62+
error_obj = json.load(error_ref)
63+
error_400 = handle_error("dummy-url", error_obj, 400)
64+
with pytest.raises(MindeeHTTPClientException):
65+
raise error_400
66+
assert error_400.status_code == 400
67+
assert error_400.api_code == "SomeCode"
68+
assert error_400.api_message == "Some scary message here"
69+
assert error_400.api_details is None
70+
71+
72+
def test_http_401_error():
73+
error_ref = open(ERROR_DATA_DIR / "error_401_invalid_token.json")
74+
error_obj = json.load(error_ref)
75+
error_401 = handle_error("dummy-url", error_obj, 401)
76+
with pytest.raises(MindeeHTTPClientException):
77+
raise error_401
78+
assert error_401.status_code == 401
79+
assert error_401.api_code == "Unauthorized"
80+
assert error_401.api_message == "Authorization required"
81+
assert error_401.api_details == "Invalid token provided"
82+
83+
84+
def test_http_429_error():
85+
error_ref = open(ERROR_DATA_DIR / "error_429_too_many_requests.json")
86+
error_obj = json.load(error_ref)
87+
error_429 = handle_error("dummy-url", error_obj, 429)
88+
with pytest.raises(MindeeHTTPClientException):
89+
raise error_429
90+
assert error_429.status_code == 429
91+
assert error_429.api_code == "TooManyRequests"
92+
assert error_429.api_message == "Too many requests"
93+
assert error_429.api_details == "Too Many Requests."
94+
95+
96+
def test_http_500_error():
97+
error_ref = open(ERROR_DATA_DIR / "error_500_inference_fail.json")
98+
error_obj = json.load(error_ref)
99+
error_500 = handle_error("dummy-url", error_obj, 500)
100+
with pytest.raises(MindeeHTTPServerException):
101+
raise error_500
102+
assert error_500.status_code == 500
103+
assert error_500.api_code == "failure"
104+
assert error_500.api_message == "Inference failed"
105+
assert error_500.api_details == "Can not run prediction: "
106+
107+
108+
def test_http_500_html_error():
109+
error_ref_contents = open(ERROR_DATA_DIR / "error_50x.html").read()
110+
error_500 = handle_error("dummy-url", error_ref_contents, 500)
111+
with pytest.raises(MindeeHTTPServerException):
112+
raise error_500
113+
assert error_500.status_code == 500
114+
assert error_500.api_code == "UnknownError"
115+
assert error_500.api_message == "Server sent back an unexpected reply."
116+
assert error_500.api_details == error_ref_contents

tests/test_cli.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from mindee.cli import MindeeParser
6-
from mindee.mindee_http.error import HTTPException
6+
from mindee.mindee_http.error import MindeeHTTPException
77
from tests.utils import clear_envvars
88

99

@@ -71,7 +71,7 @@ def ots_doc_fetch(monkeypatch):
7171

7272

7373
def test_cli_custom_doc(custom_doc):
74-
with pytest.raises(HTTPException):
74+
with pytest.raises(MindeeHTTPException):
7575
parser = MindeeParser(parsed_args=custom_doc)
7676
parser.call_endpoint()
7777

@@ -83,7 +83,7 @@ def test_cli_invoice(ots_doc):
8383
parser = MindeeParser(parsed_args=ots_doc)
8484
parser.call_endpoint()
8585
ots_doc.api_key = "dummy"
86-
with pytest.raises(HTTPException):
86+
with pytest.raises(MindeeHTTPException):
8787
parser = MindeeParser(parsed_args=ots_doc)
8888
parser.call_endpoint()
8989

@@ -95,7 +95,7 @@ def test_cli_receipt(ots_doc):
9595
parser = MindeeParser(parsed_args=ots_doc)
9696
parser.call_endpoint()
9797
ots_doc.api_key = "dummy"
98-
with pytest.raises(HTTPException):
98+
with pytest.raises(MindeeHTTPException):
9999
parser = MindeeParser(parsed_args=ots_doc)
100100
parser.call_endpoint()
101101

@@ -107,7 +107,7 @@ def test_cli_financial_doc(ots_doc):
107107
parser = MindeeParser(parsed_args=ots_doc)
108108
parser.call_endpoint()
109109
ots_doc.api_key = "dummy"
110-
with pytest.raises(HTTPException):
110+
with pytest.raises(MindeeHTTPException):
111111
parser = MindeeParser(parsed_args=ots_doc)
112112
parser.call_endpoint()
113113

@@ -119,7 +119,7 @@ def test_cli_passport(ots_doc):
119119
parser = MindeeParser(parsed_args=ots_doc)
120120
parser.call_endpoint()
121121
ots_doc.api_key = "dummy"
122-
with pytest.raises(HTTPException):
122+
with pytest.raises(MindeeHTTPException):
123123
parser = MindeeParser(parsed_args=ots_doc)
124124
parser.call_endpoint()
125125

@@ -131,7 +131,7 @@ def test_cli_us_bank_check(ots_doc):
131131
parser = MindeeParser(parsed_args=ots_doc)
132132
parser.call_endpoint()
133133
ots_doc.api_key = "dummy"
134-
with pytest.raises(HTTPException):
134+
with pytest.raises(MindeeHTTPException):
135135
parser = MindeeParser(parsed_args=ots_doc)
136136
parser.call_endpoint()
137137

@@ -143,7 +143,7 @@ def test_cli_invoice_splitter_enqueue(ots_doc_enqueue_and_parse):
143143
parser = MindeeParser(parsed_args=ots_doc_enqueue_and_parse)
144144
parser.call_endpoint()
145145
ots_doc_enqueue_and_parse.api_key = "dummy"
146-
with pytest.raises(HTTPException):
146+
with pytest.raises(MindeeHTTPException):
147147
parser = MindeeParser(parsed_args=ots_doc_enqueue_and_parse)
148148
parser.call_endpoint()
149149

@@ -155,6 +155,6 @@ def test_cli_invoice_splitter_enqueue(ots_doc_enqueue_and_parse):
155155
# parser = MindeeParser(parsed_args=ots_doc_fetch)
156156
# parser.call_endpoint()
157157
# ots_doc_fetch.api_key = "dummy"
158-
# with pytest.raises(HTTPException):
158+
# with pytest.raises(MindeeHTTPException):
159159
# parser = MindeeParser(parsed_args=ots_doc_fetch)
160160
# parser.call_endpoint()

0 commit comments

Comments
 (0)