Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions mindee/error/mindee_http_error_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,42 @@
from typing import Optional

from mindee.parsing.common.string_dict import StringDict
from mindee.parsing.v2 import ErrorItem, ErrorResponse


class MindeeHTTPErrorV2(RuntimeError):
class MindeeHTTPErrorV2(RuntimeError, ErrorResponse):
"""An exception relating to HTTP calls."""

status: int
detail: Optional[str]

def __init__(self, status: int, detail: Optional[str]) -> None:
def __init__(self, response: ErrorResponse) -> None:
"""
Base exception for HTTP calls.

:param status: HTTP code for the error
:param detail: Error details.
:param response:
"""
self.status = status
self.detail = detail
super().__init__(f"HTTP error {status} - {detail}")
self.status = response.status
self.title = response.title
self.code = response.code
self.detail = response.detail
self.errors: list[ErrorItem] = response.errors
super().__init__(
f"HTTP {self.status} - {self.title} :: {self.code} - {self.detail}"
)


class MindeeHTTPUnknownErrorV2(MindeeHTTPErrorV2):
"""HTTP error with unknown status code."""

def __init__(self, detail: Optional[str]) -> None:
super().__init__(-1, f"Couldn't deserialize server error. Found: {detail}")
super().__init__(
ErrorResponse(
{
"status": -1,
"code": "000-000",
"title": "Unknown Error",
"detail": f"Couldn't deserialize server error. Found: {detail}",
}
)
)


def handle_error_v2(raw_response: StringDict) -> None:
Expand All @@ -38,7 +49,4 @@ def handle_error_v2(raw_response: StringDict) -> None:
"""
if "status" not in raw_response or "detail" not in raw_response:
raise MindeeHTTPUnknownErrorV2(json.dumps(raw_response, indent=2))
raise MindeeHTTPErrorV2(
raw_response["status"],
raw_response["detail"],
)
raise MindeeHTTPErrorV2(ErrorResponse(raw_response))
2 changes: 2 additions & 0 deletions mindee/parsing/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from mindee.parsing.v2.error_item import ErrorItem
from mindee.parsing.v2.error_response import ErrorResponse
from mindee.parsing.v2.inference import Inference
from mindee.parsing.v2.inference_active_options import InferenceActiveOptions
Expand All @@ -16,4 +17,5 @@
"InferenceResult",
"JobResponse",
"ErrorResponse",
"ErrorItem",
]
16 changes: 16 additions & 0 deletions mindee/parsing/v2/error_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import Optional

from mindee.parsing.common.string_dict import StringDict


class ErrorItem:
"""Explicit details on a problem."""

pointer: Optional[str]
"""A JSON Pointer to the location of the body property."""
detail: str
"""Explicit information on the issue."""

def __init__(self, raw_response: StringDict):
self.pointer = raw_response.get("pointer", None)
self.detail = raw_response["detail"]
27 changes: 21 additions & 6 deletions mindee/parsing/v2/error_response.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
from typing import List

from mindee.parsing.common.string_dict import StringDict
from mindee.parsing.v2.error_item import ErrorItem


class ErrorResponse:
"""Error response info."""
"""Error response detailing a problem. The format adheres to RFC 9457."""

detail: str
"""Detail relevant to the error."""
status: int
"""Http error code."""
"""The HTTP status code returned by the server."""
detail: str
"""A human-readable explanation specific to the occurrence of the problem."""
title: str
"""A short, human-readable summary of the problem."""
code: str
"""A machine-readable code specific to the occurrence of the problem."""
errors: List[ErrorItem]
"""A list of explicit error details."""

def __init__(self, raw_response: StringDict):
self.detail = raw_response["detail"]
self.status = raw_response["status"]
self.detail = raw_response["detail"]
self.title = raw_response["title"]
self.code = raw_response["code"]
try:
self.errors = [ErrorItem(error) for error in raw_response["errors"]]
except KeyError:
self.errors = []

def __str__(self):
return f"HTTP Status: {self.status} - {self.detail}"
return f"HTTP {self.status} - {self.title} :: {self.code} - {self.detail}"
5 changes: 5 additions & 0 deletions mindee/parsing/v2/inference_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from mindee.parsing.common.string_dict import StringDict
from mindee.parsing.v2.field.inference_fields import InferenceFields
from mindee.parsing.v2.rag_metadata import RagMetadata
from mindee.parsing.v2.raw_text import RawText


Expand All @@ -12,11 +13,15 @@ class InferenceResult:
"""Fields contained in the inference."""
raw_text: Optional[RawText] = None
"""Potential options retrieved alongside the inference."""
rag: Optional[RagMetadata] = None
"""RAG metadata."""

def __init__(self, raw_response: StringDict) -> None:
self.fields = InferenceFields(raw_response["fields"])
if raw_response.get("raw_text"):
self.raw_text = RawText(raw_response["raw_text"])
if raw_response.get("rag"):
self.rag = RagMetadata(raw_response["rag"])

def __str__(self) -> str:
out_str = f"Fields\n======{self.fields}"
Expand Down
12 changes: 12 additions & 0 deletions mindee/parsing/v2/rag_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional

from mindee.parsing.common.string_dict import StringDict


class RagMetadata:
"""Metadata about the RAG operation."""

retrieved_document_id: Optional[str]

def __init__(self, raw_response: StringDict):
self.retrieved_document_id = raw_response["retrieved_document_id"]
21 changes: 21 additions & 0 deletions tests/v2/parsing/test_inference_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from mindee.parsing.v2.inference import Inference
from mindee.parsing.v2.inference_file import InferenceFile
from mindee.parsing.v2.inference_model import InferenceModel
from mindee.parsing.v2.rag_metadata import RagMetadata
from tests.utils import V2_DATA_DIR


Expand Down Expand Up @@ -198,6 +199,26 @@ def test_raw_texts():
)


@pytest.mark.v2
def test_rag_metadata_when_matched():
"""RAG metadata when matched."""
json_sample, _ = _get_inference_samples("rag_matched")
response = InferenceResponse(json_sample)
rag = response.inference.result.rag
assert isinstance(rag, RagMetadata)
assert rag.retrieved_document_id == "12345abc-1234-1234-1234-123456789abc"


@pytest.mark.v2
def test_rag_metadata_when_not_matched():
"""RAG metadata when not matched."""
json_sample, _ = _get_inference_samples("rag_not_matched")
response = InferenceResponse(json_sample)
rag = response.inference.result.rag
assert isinstance(rag, RagMetadata)
assert rag.retrieved_document_id is None


@pytest.mark.v2
def test_full_inference_response():
json_sample, rst_sample = _get_product_samples("financial_document", "complete")
Expand Down
39 changes: 39 additions & 0 deletions tests/v2/parsing/test_job_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json

import pytest

from mindee import JobResponse
from mindee.parsing.v2 import ErrorItem, ErrorResponse
from tests.utils import V2_DATA_DIR


def _get_job_samples(json_file: str) -> dict:
json_path = V2_DATA_DIR / "job" / json_file
with json_path.open("r", encoding="utf-8") as fh:
json_sample = json.load(fh)
return json_sample


@pytest.mark.v2
def test_should_load_when_status_is_processing():
"""Should load when status is Processing."""
json_sample = _get_job_samples("ok_processing.json")
response = JobResponse(json_sample)

assert response.job is not None
assert response.job.error is None


@pytest.mark.v2
def test_should_load_with_422_error():
"""Should load with 422 error."""
json_sample = _get_job_samples("fail_422.json")
response = JobResponse(json_sample)

assert response.job is not None
assert isinstance(response.job.error, ErrorResponse)
assert response.job.error.status == 422
assert response.job.error.code.startswith("422-")
assert isinstance(response.job.error.errors, list)
assert len(response.job.error.errors) == 1
assert isinstance(response.job.error.errors[0], ErrorItem)
7 changes: 6 additions & 1 deletion tests/v2/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ class _FakePostRespError:

def json(self):
# Shape must match what handle_error_v2 expects
return {"status": -1, "detail": "forced failure from test"}
return {
"status": 0,
"code": "000-000",
"title": "From Test",
"detail": "forced failure from test",
}

class _FakeOkProcessingJobResp:
status_code = 200
Expand Down
9 changes: 9 additions & 0 deletions tests/v2/test_client_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def test_invalid_uuid_must_throw_error(v2_client: ClientV2) -> None:

exc: MindeeHTTPErrorV2 = exc_info.value
assert exc.status == 422
assert exc.title is not None
assert exc.code.startswith("422-")
assert isinstance(exc.errors, list)


@pytest.mark.integration
Expand All @@ -197,6 +200,9 @@ def test_unknown_model_must_throw_error(v2_client: ClientV2) -> None:

exc: MindeeHTTPErrorV2 = exc_info.value
assert exc.status == 404
assert exc.title is not None
assert exc.code.startswith("404-")
assert isinstance(exc.errors, list)


@pytest.mark.integration
Expand Down Expand Up @@ -227,6 +233,9 @@ def test_unknown_webhook_ids_must_throw_error(

exc: MindeeHTTPErrorV2 = exc_info.value
assert exc.status == 422
assert exc.title is not None
assert exc.code.startswith("422-")
assert isinstance(exc.errors, list)
assert "no matching webhooks" in exc.detail.lower()


Expand Down