Skip to content

Commit b3e7f1d

Browse files
authored
✨ add error response rst output (#463)
1 parent 7464b0a commit b3e7f1d

80 files changed

Lines changed: 411 additions & 407 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/_test-integrations.yml

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ on:
1010
permissions:
1111
contents: read
1212

13+
env:
14+
MINDEE_API_KEY: ${{ secrets.MINDEE_API_KEY_SE_TESTS }}
15+
WORKFLOW_ID: ${{ secrets.WORKFLOW_ID_SE_TESTS }}
16+
MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }}
17+
MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }}
18+
MINDEE_V2_SE_TESTS_BLANK_PDF_URL: ${{ secrets.MINDEE_V2_SE_TESTS_BLANK_PDF_URL }}
19+
MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }}
20+
MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }}
21+
MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }}
22+
MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }}
23+
1324
jobs:
1425
pytest:
1526
name: Run Integration Tests
@@ -46,16 +57,6 @@ jobs:
4657
python -m pip install pip
4758
pip install -e '.[test]'
4859
- name: Run Integration Testing
49-
env:
50-
MINDEE_API_KEY: ${{ secrets.MINDEE_API_KEY_SE_TESTS }}
51-
WORKFLOW_ID: ${{ secrets.WORKFLOW_ID_SE_TESTS }}
52-
MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }}
53-
MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }}
54-
MINDEE_V2_SE_TESTS_BLANK_PDF_URL: ${{ secrets.MINDEE_V2_SE_TESTS_BLANK_PDF_URL }}
55-
MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }}
56-
MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }}
57-
MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }}
58-
MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }}
5960
run: |
6061
pytest --cov mindee -m integration
6162
@@ -105,15 +106,5 @@ jobs:
105106
pip install -e '.[test]'
106107
shell: bash
107108
- name: Run Integration Testing
108-
env:
109-
MINDEE_API_KEY: ${{ secrets.MINDEE_API_KEY_SE_TESTS }}
110-
WORKFLOW_ID: ${{ secrets.WORKFLOW_ID_SE_TESTS }}
111-
MINDEE_V2_API_KEY: ${{ secrets.MINDEE_V2_SE_TESTS_API_KEY }}
112-
MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_FINDOC_MODEL_ID }}
113-
MINDEE_V2_SE_TESTS_BLANK_PDF_URL: ${{ secrets.MINDEE_V2_SE_TESTS_BLANK_PDF_URL }}
114-
MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CLASSIFICATION_MODEL_ID }}
115-
MINDEE_V2_SE_TESTS_CROP_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_CROP_MODEL_ID }}
116-
MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_SPLIT_MODEL_ID }}
117-
MINDEE_V2_SE_TESTS_OCR_MODEL_ID: ${{ secrets.MINDEE_V2_SE_TESTS_OCR_MODEL_ID }}
118109
run: |
119110
pytest -m "integration and not pypdfium2 and not pillow"

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* :sparkles: add simple fields typed accessors
1818
* :sparkles: allow passing a webhook to the CLI
1919
* :recycle: update internals and bump dependencies
20-
2120
### Fixes
2221
* :bug: :boom: rework extraction methods
2322
* :bug: :boom: harmonize Crop and Split

mindee/v2/error/mindee_http_error_v2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import json
22

33
from mindee.parsing.common.string_dict import StringDict
4-
from mindee.v2.parsing import ErrorItem, ErrorResponse
4+
from mindee.v2.parsing.error import ErrorItem, ErrorResponse, IErrorResponse
55

66

7-
class MindeeHTTPErrorV2(RuntimeError, ErrorResponse):
7+
class MindeeHTTPErrorV2(RuntimeError, IErrorResponse):
88
"""An exception relating to HTTP calls."""
99

1010
def __init__(self, response: ErrorResponse) -> None:

mindee/v2/mindee_http/mindee_api_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def req_get_search_models(
228228
self, name: str | None, model_type: str | None
229229
) -> SearchResponse:
230230
"""
231-
Deprecated. Use `search` instead.
231+
Deprecated. Use `req_search` instead.
232232
"""
233233
get_caller: Callable
234234
if self.http_client is None or self.http_client.is_closed:
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from mindee.v2.parsing.error.error_item import ErrorItem
22
from mindee.v2.parsing.error.error_response import ErrorResponse
3+
from mindee.v2.parsing.error.ierror_response import IErrorResponse
34

4-
__all__ = ["ErrorItem", "ErrorResponse"]
5+
__all__ = ["ErrorItem", "ErrorResponse", "IErrorResponse"]
Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1+
from mindee.parsing.common import CommonResponse
12
from mindee.parsing.common.string_dict import StringDict
23
from mindee.v2.parsing.error.error_item import ErrorItem
4+
from mindee.v2.parsing.error.ierror_response import IErrorResponse
35

46

5-
class ErrorResponse:
7+
class ErrorResponse(CommonResponse, IErrorResponse):
68
"""Error response detailing a problem. The format adheres to RFC 9457."""
79

8-
status: int
9-
"""The HTTP status code returned by the server."""
10-
detail: str
11-
"""A human-readable explanation specific to the occurrence of the problem."""
12-
title: str
13-
"""A short, human-readable summary of the problem."""
14-
code: str
15-
"""A machine-readable code specific to the occurrence of the problem."""
16-
errors: list[ErrorItem]
17-
"""A list of explicit error details."""
18-
1910
def __init__(self, raw_response: StringDict):
11+
super().__init__(raw_response)
2012
self.status = raw_response["status"]
2113
self.detail = raw_response["detail"]
2214
self.title = raw_response["title"]
@@ -26,5 +18,29 @@ def __init__(self, raw_response: StringDict):
2618
except KeyError:
2719
self.errors = []
2820

29-
def __str__(self):
30-
return f"HTTP {self.status} - {self.title} :: {self.code} - {self.detail}"
21+
def __str__(self) -> str:
22+
"""To make the error prettier to display."""
23+
24+
result = [
25+
"Error Details",
26+
"=============",
27+
f":HTTP Status: {self.status}",
28+
f":Title: {self.title}",
29+
f":Code: {self.code}",
30+
f":Detail: {self.detail}",
31+
]
32+
33+
if self.errors:
34+
result.append("")
35+
result.append("Error Items")
36+
result.append("-----------")
37+
38+
for i, error in enumerate(self.errors):
39+
result.append(f"**Error {i + 1}:**")
40+
result.append(f" :Pointer: {getattr(error, 'pointer', '')}")
41+
result.append(f" :Detail: {getattr(error, 'detail', '')}")
42+
43+
if i < len(self.errors) - 1:
44+
result.append("")
45+
46+
return "\n".join(result) + "\n"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Protocol
2+
3+
from mindee.v2.parsing.error.error_item import ErrorItem
4+
5+
6+
class IErrorResponse(Protocol):
7+
"""Error response detailing a problem. The format adheres to RFC 9457."""
8+
9+
status: int
10+
"""The HTTP status code returned by the server."""
11+
12+
detail: str
13+
"""A human-readable explanation specific to the occurrence of the problem."""
14+
15+
title: str
16+
"""A short, human-readable summary of the problem."""
17+
18+
code: str
19+
"""A machine-readable code specific to the occurrence of the problem."""
20+
21+
errors: list[ErrorItem]
22+
"""A list of explicit details on the problem."""

tests/input/test_apply_page_options.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
PathInput,
1212
)
1313
from mindee.input.page_options import KEEP_ONLY, REMOVE, PageOptions
14-
from tests.utils import FILE_TYPES_DIR, V1_PRODUCT_DATA_DIR
14+
from tests.utils import FILE_TYPES_PATH, V1_PRODUCT_PATH
1515

1616
pdfium = pytest.importorskip("pypdfium2")
1717

@@ -22,7 +22,9 @@ def _assert_page_options(input_source: LocalInputSource, numb_pages: int):
2222
# I.e., each page is read and rendered as a rasterized image.
2323
# These images are then compared as raw byte sequences.
2424
cut_pdf = pdfium.PdfDocument(input_source.file_object)
25-
pdf = pdfium.PdfDocument(FILE_TYPES_DIR / "pdf" / f"multipage_cut-{numb_pages}.pdf")
25+
pdf = pdfium.PdfDocument(
26+
FILE_TYPES_PATH / "pdf" / f"multipage_cut-{numb_pages}.pdf"
27+
)
2628
for idx in range(len(pdf)):
2729
pdf_page = pdf.get_page(idx)
2830
pdf_page_render = pdfium.PdfPage.render(pdf_page)
@@ -35,14 +37,14 @@ def _assert_page_options(input_source: LocalInputSource, numb_pages: int):
3537

3638

3739
def test_pdf_reconstruct_ok():
38-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
40+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
3941
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=range(5))
4042
assert isinstance(input_source.file_object, io.BytesIO)
4143

4244

4345
@pytest.mark.parametrize("numb_pages", [1, 2, 3])
4446
def test_process_pdf_cut_n_pages(numb_pages: int):
45-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
47+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
4648
assert input_source.page_count == 12
4749
input_source.process_pdf(
4850
behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0, -2, -1][:numb_pages]
@@ -53,7 +55,7 @@ def test_process_pdf_cut_n_pages(numb_pages: int):
5355

5456
@pytest.mark.parametrize("numb_pages", [1, 2, 3])
5557
def test_apply_pages_pdf_cut_n_pages(numb_pages: int):
56-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
58+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
5759
assert input_source.page_count == 12
5860
input_source.apply_page_options(
5961
PageOptions(on_min_pages=2, page_indexes=[0, -2, -1][:numb_pages])
@@ -63,7 +65,7 @@ def test_apply_pages_pdf_cut_n_pages(numb_pages: int):
6365

6466

6567
def test_pdf_keep_5_first_pages():
66-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
68+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
6769
assert input_source.page_count == 12
6870
input_source.process_pdf(
6971
behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0, 1, 2, 3, 4]
@@ -72,7 +74,7 @@ def test_pdf_keep_5_first_pages():
7274

7375

7476
def test_pdf_keep_invalid_pages():
75-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
77+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
7678
assert input_source.page_count == 12
7779
input_source.process_pdf(
7880
behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0, 1, 17]
@@ -81,7 +83,7 @@ def test_pdf_keep_invalid_pages():
8183

8284

8385
def test_pdf_remove_5_last_pages():
84-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
86+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
8587
assert input_source.is_pdf() is True
8688
input_source.process_pdf(
8789
behavior=REMOVE, on_min_pages=2, page_indexes=[-5, -4, -3, -2, -1]
@@ -90,7 +92,7 @@ def test_pdf_remove_5_last_pages():
9092

9193

9294
def test_pdf_remove_5_first_pages():
93-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
95+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
9496
assert input_source.is_pdf() is True
9597
input_source.process_pdf(
9698
behavior=REMOVE, on_min_pages=2, page_indexes=list(range(5))
@@ -99,14 +101,14 @@ def test_pdf_remove_5_first_pages():
99101

100102

101103
def test_pdf_remove_invalid_pages():
102-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
104+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
103105
assert input_source.is_pdf() is True
104106
input_source.process_pdf(behavior=REMOVE, on_min_pages=2, page_indexes=[16])
105107
assert input_source.page_count == 12
106108

107109

108110
def test_pdf_keep_no_pages():
109-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
111+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
110112
assert input_source.is_pdf() is True
111113
# empty page indexes
112114
with pytest.raises(RuntimeError):
@@ -119,7 +121,7 @@ def test_pdf_keep_no_pages():
119121

120122

121123
def test_pdf_remove_all_pages():
122-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "multipage.pdf")
124+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "multipage.pdf")
123125
assert input_source.is_pdf() is True
124126
with pytest.raises(RuntimeError):
125127
input_source.process_pdf(
@@ -128,37 +130,37 @@ def test_pdf_remove_all_pages():
128130

129131

130132
def test_pdf_input_from_file():
131-
with open(FILE_TYPES_DIR / "pdf" / "multipage.pdf", "rb") as fp:
133+
with open(FILE_TYPES_PATH / "pdf" / "multipage.pdf", "rb") as fp:
132134
input_source = FileInput(fp)
133135
assert input_source.is_pdf() is True
134136
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0])
135137
assert input_source.page_count == 1
136138

137139

138140
def test_pdf_input_from_base64():
139-
with open(V1_PRODUCT_DATA_DIR / "invoices" / "invoice_10p.txt") as fp:
141+
with open(V1_PRODUCT_PATH / "invoices" / "invoice_10p.txt") as fp:
140142
input_source = Base64Input(fp.read(), filename="invoice_10p.pdf")
141143
assert input_source.is_pdf() is True
142144
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0])
143145
assert input_source.page_count == 1
144146

145147

146148
def test_pdf_input_from_bytes():
147-
with open(V1_PRODUCT_DATA_DIR / "invoices" / "invoice_10p.pdf", "rb") as fp:
149+
with open(V1_PRODUCT_PATH / "invoices" / "invoice_10p.pdf", "rb") as fp:
148150
input_source = BytesInput(fp.read(), filename="invoice_10p.pdf")
149151
assert input_source.is_pdf() is True
150152
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0])
151153
assert input_source.page_count == 1
152154

153155

154156
def test_pdf_blank_check():
155-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "blank.pdf")
157+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "blank.pdf")
156158
with pytest.raises(MindeeError):
157159
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0])
158160

159-
input_source = PathInput(FILE_TYPES_DIR / "pdf" / "blank_1.pdf")
161+
input_source = PathInput(FILE_TYPES_PATH / "pdf" / "blank_1.pdf")
160162
with pytest.raises(MindeeError):
161163
input_source.process_pdf(behavior=KEEP_ONLY, on_min_pages=2, page_indexes=[0])
162164

163-
input_not_blank = PathInput(FILE_TYPES_DIR / "pdf" / "not_blank_image_only.pdf")
165+
input_not_blank = PathInput(FILE_TYPES_PATH / "pdf" / "not_blank_image_only.pdf")
164166
assert input_not_blank.page_count == 1

0 commit comments

Comments
 (0)