diff --git a/mindee/error/mindee_http_error_v2.py b/mindee/error/mindee_http_error_v2.py index 05954004..99ba40da 100644 --- a/mindee/error/mindee_http_error_v2.py +++ b/mindee/error/mindee_http_error_v2.py @@ -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: @@ -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)) diff --git a/mindee/parsing/v2/__init__.py b/mindee/parsing/v2/__init__.py index 47557c9e..fe275352 100644 --- a/mindee/parsing/v2/__init__.py +++ b/mindee/parsing/v2/__init__.py @@ -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 @@ -16,4 +17,5 @@ "InferenceResult", "JobResponse", "ErrorResponse", + "ErrorItem", ] diff --git a/mindee/parsing/v2/error_item.py b/mindee/parsing/v2/error_item.py new file mode 100644 index 00000000..8d9aad96 --- /dev/null +++ b/mindee/parsing/v2/error_item.py @@ -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"] diff --git a/mindee/parsing/v2/error_response.py b/mindee/parsing/v2/error_response.py index b9f7660e..0a0191b3 100644 --- a/mindee/parsing/v2/error_response.py +++ b/mindee/parsing/v2/error_response.py @@ -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}" diff --git a/mindee/parsing/v2/inference_result.py b/mindee/parsing/v2/inference_result.py index 0a89ef24..8359fb71 100644 --- a/mindee/parsing/v2/inference_result.py +++ b/mindee/parsing/v2/inference_result.py @@ -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 @@ -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}" diff --git a/mindee/parsing/v2/rag_metadata.py b/mindee/parsing/v2/rag_metadata.py new file mode 100644 index 00000000..04f875af --- /dev/null +++ b/mindee/parsing/v2/rag_metadata.py @@ -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"] diff --git a/tests/v2/parsing/test_inference_response.py b/tests/v2/parsing/test_inference_response.py index a5b8bff7..9a5f9b9d 100644 --- a/tests/v2/parsing/test_inference_response.py +++ b/tests/v2/parsing/test_inference_response.py @@ -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 @@ -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") diff --git a/tests/v2/parsing/test_job_response.py b/tests/v2/parsing/test_job_response.py new file mode 100644 index 00000000..a7f52d3e --- /dev/null +++ b/tests/v2/parsing/test_job_response.py @@ -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) diff --git a/tests/v2/test_client.py b/tests/v2/test_client.py index a9e69b27..fb70db24 100644 --- a/tests/v2/test_client.py +++ b/tests/v2/test_client.py @@ -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 diff --git a/tests/v2/test_client_integration.py b/tests/v2/test_client_integration.py index cabce8db..173ad627 100644 --- a/tests/v2/test_client_integration.py +++ b/tests/v2/test_client_integration.py @@ -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 @@ -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 @@ -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()