diff --git a/README.md b/README.md index 59cacd5c..0f95e1e3 100644 --- a/README.md +++ b/README.md @@ -195,32 +195,6 @@ result = mindee_client.parse( ) ``` -## Further Reading -Complete details on the working of the library are available in the following guides: - -* [Getting started](https://developers.mindee.com/docs/python-getting-started) -* [Python Command Line Interface (CLI)](https://developers.mindee.com/docs/python-cli) -* [Python Generated](https://developers.mindee.com/docs/generated-api-python) -* [Python Custom APIs (Deprecated - API Builder)](https://developers.mindee.com/docs/python-api-builder) -* [Python Invoice OCR](https://developers.mindee.com/docs/python-invoice-ocr) -* [Python International Id OCR](https://developers.mindee.com/docs/python-international-id-ocr) -* [Python Resume OCR](https://developers.mindee.com/docs/python-resume-ocr) -* [Python Receipt OCR](https://developers.mindee.com/docs/python-receipt-ocr) -* [Python Financial Document OCR](https://developers.mindee.com/docs/python-financial-document-ocr) -* [Python Passport OCR](https://developers.mindee.com/docs/python-passport-ocr) -* [Python Proof of Address OCR](https://developers.mindee.com/docs/python-proof-of-address-ocr) -* [Python US Driver License OCR](https://developers.mindee.com/docs/python-eu-driver-license-ocr) -* [Python FR Bank Account Detail OCR](https://developers.mindee.com/docs/python-fr-bank-account-details-ocr) -* [Python FR Carte Grise OCR](https://developers.mindee.com/docs/python-fr-carte-grise-ocr) -* [Python FR Health Card OCR](https://developers.mindee.com/docs/python-fr-health-card-ocr) -* [Python FR ID Card OCR](https://developers.mindee.com/docs/python-fr-carte-nationale-didentite-ocr) -* [Python US Bank Check OCR](https://developers.mindee.com/docs/python-us-bank-check-ocr) -* [Python US W9 OCR](https://developers.mindee.com/docs/python-us-w9-ocr) -* [Python Barcode Reader API](https://developers.mindee.com/docs/python-barcode-reader-ocr) -* [Python Cropper API](https://developers.mindee.com/docs/python-cropper-ocr) -* [Python Invoice Splitter API](https://developers.mindee.com/docs/python-invoice-splitter-api) -* [Python Multi Receipts Detector API](https://developers.mindee.com/docs/python-multi-receipts-detector-ocr) - You can view the source code on [GitHub](https://github.com/mindee/mindee-api-python). You can also take a look at the diff --git a/docs/extras/code_samples/default_v2.txt b/docs/extras/code_samples/default_v2.txt index 5c9b3969..fdb63ddf 100644 --- a/docs/extras/code_samples/default_v2.txt +++ b/docs/extras/code_samples/default_v2.txt @@ -19,7 +19,7 @@ params = InferenceParameters( input_source = mindee_client.source_from_path(input_path) # Upload the file -response = mindee_client.enqueue_and_parse( +response = mindee_client.enqueue_and_get_inference( input_source, params ) diff --git a/docs/extras/guide/getting_started.md b/docs/extras/guide/getting_started.md index 7b9936a3..e93ef9b2 100644 --- a/docs/extras/guide/getting_started.md +++ b/docs/extras/guide/getting_started.md @@ -219,7 +219,7 @@ custom_endpoint = mindee_client.create_endpoint( "my-account-name", # "my-version" # optional ) -result = mindee_client.enqueue_and_parse(product.GeneratedV1, input_doc, endpoint=custom_endpoint) +result = mindee_client.enqueue_and_get_inference(product.GeneratedV1, input_doc, endpoint=custom_endpoint) ``` This is because the `GeneratedV1` class is enough to handle the return processing, but the actual endpoint needs to be specified. diff --git a/mindee/client_v2.py b/mindee/client_v2.py index 48502f1a..0b757e50 100644 --- a/mindee/client_v2.py +++ b/mindee/client_v2.py @@ -1,5 +1,5 @@ from time import sleep -from typing import Optional, Union +from typing import Optional from mindee.client_mixin import ClientMixin from mindee.error.mindee_error import MindeeError @@ -14,6 +14,7 @@ is_valid_get_response, is_valid_post_response, ) +from mindee.parsing.v2.common_response import CommonStatus from mindee.parsing.v2.inference_response import InferenceResponse from mindee.parsing.v2.job_response import JobResponse @@ -37,7 +38,7 @@ def __init__(self, api_key: Optional[str] = None) -> None: self.api_key = api_key self.mindee_api = MindeeApiV2(api_key) - def enqueue( + def enqueue_inference( self, input_source: LocalInputSource, params: InferenceParameters ) -> JobResponse: """ @@ -49,39 +50,52 @@ def enqueue( :param params: Parameters to set when sending a file. :return: A valid inference response. """ - logger.debug("Enqueuing document to '%s'", params.model_id) + logger.debug("Enqueuing inference using model: %s", params.model_id) - response = self.mindee_api.predict_async_req_post( - input_source=input_source, options=params + response = self.mindee_api.req_post_inference_enqueue( + input_source=input_source, params=params ) dict_response = response.json() if not is_valid_post_response(response): handle_error_v2(dict_response) - return JobResponse(dict_response) - def parse_queued( - self, - queue_id: str, - ) -> Union[InferenceResponse, JobResponse]: + def get_job(self, job_id: str) -> JobResponse: """ - Parses a queued document. + Get the status of an inference that was previously enqueued. + + Can be used for polling. - :param queue_id: queue_id received from the API. + :param job_id: UUID of the job to retrieve. + :return: A job response. """ - logger.debug("Fetching from queue '%s'.", queue_id) + logger.debug("Fetching job: %s", job_id) - response = self.mindee_api.get_inference_from_queue(queue_id) + response = self.mindee_api.req_get_job(job_id) if not is_valid_get_response(response): handle_error_v2(response.json()) + dict_response = response.json() + return JobResponse(dict_response) + + def get_inference(self, inference_id: str) -> InferenceResponse: + """ + Get the result of an inference that was previously enqueued. + + The inference will only be available after it has finished processing. + :param inference_id: UUID of the inference to retrieve. + :return: An inference response. + """ + logger.debug("Fetching inference: %s", inference_id) + + response = self.mindee_api.req_get_inference(inference_id) + if not is_valid_get_response(response): + handle_error_v2(response.json()) dict_response = response.json() - if "job" in dict_response: - return JobResponse(dict_response) return InferenceResponse(dict_response) - def enqueue_and_parse( + def enqueue_and_get_inference( self, input_source: LocalInputSource, params: InferenceParameters ) -> InferenceResponse: """ @@ -101,40 +115,28 @@ def enqueue_and_parse( params.polling_options.delay_sec, params.polling_options.max_retries, ) - queue_result = self.enqueue(input_source, params) + enqueue_response = self.enqueue_inference(input_source, params) logger.debug( - "Successfully enqueued document with job id: %s", queue_result.job.id + "Successfully enqueued inference with job id: %s", enqueue_response.job.id ) sleep(params.polling_options.initial_delay_sec) - retry_counter = 1 - poll_results = self.parse_queued( - queue_result.job.id, - ) - while retry_counter < params.polling_options.max_retries: - if not isinstance(poll_results, JobResponse): - break - if poll_results.job.status == "Failed": - if poll_results.job.error: - detail = poll_results.job.error.detail + try_counter = 0 + while try_counter < params.polling_options.max_retries: + job_response = self.get_job(enqueue_response.job.id) + if job_response.job.status == CommonStatus.FAILED.value: + if job_response.job.error: + detail = job_response.job.error.detail else: detail = "No error detail available." raise MindeeError( - f"Parsing failed for job {poll_results.job.id}: {detail}" + f"Parsing failed for job {job_response.job.id}: {detail}" ) - logger.debug( - "Polling server for parsing result with job id: %s", - queue_result.job.id, - ) - retry_counter += 1 + if job_response.job.status == CommonStatus.PROCESSED.value: + return self.get_inference(job_response.job.id) + try_counter += 1 sleep(params.polling_options.delay_sec) - poll_results = self.parse_queued(queue_result.job.id) - - if not isinstance(poll_results, InferenceResponse): - raise MindeeError( - f"Couldn't retrieve document after {retry_counter} tries." - ) - return poll_results + raise MindeeError(f"Couldn't retrieve document after {try_counter + 1} tries.") @staticmethod def load_inference(local_response: LocalResponse) -> InferenceResponse: diff --git a/mindee/input/inference_parameters.py b/mindee/input/inference_parameters.py index ad3849bc..b8608f6c 100644 --- a/mindee/input/inference_parameters.py +++ b/mindee/input/inference_parameters.py @@ -13,7 +13,7 @@ class InferenceParameters: rag: bool = False """If set to `True`, will enable Retrieval-Augmented Generation.""" alias: Optional[str] = None - """Optional alias for the file.""" + """Use an alias to link the file to your own DB. If empty, no alias will be used.""" webhook_ids: Optional[List[str]] = None """IDs of webhooks to propagate the API response to.""" polling_options: Optional[PollingOptions] = None diff --git a/mindee/mindee_http/mindee_api_v2.py b/mindee/mindee_http/mindee_api_v2.py index f989395a..0c5c1e62 100644 --- a/mindee/mindee_http/mindee_api_v2.py +++ b/mindee/mindee_http/mindee_api_v2.py @@ -67,27 +67,27 @@ def set_from_env(self) -> None: func(env_val) logger.debug("Value was set from env: %s", name) - def predict_async_req_post( - self, input_source: LocalInputSource, options: InferenceParameters + def req_post_inference_enqueue( + self, input_source: LocalInputSource, params: InferenceParameters ) -> requests.Response: """ Make an asynchronous request to POST a document for prediction on the V2 API. :param input_source: Input object. - :param options: Options for the enqueueing of the document. + :param params: Options for the enqueueing of the document. :return: requests response. """ - data = {"model_id": options.model_id} + data = {"model_id": params.model_id} url = f"{self.url_root}/inferences/enqueue" - if options.rag: + if params.rag: data["rag"] = "true" - if options.webhook_ids and len(options.webhook_ids) > 0: - data["webhook_ids"] = ",".join(options.webhook_ids) - if options.alias and len(options.alias): - data["alias"] = options.alias + if params.webhook_ids and len(params.webhook_ids) > 0: + data["webhook_ids"] = ",".join(params.webhook_ids) + if params.alias and len(params.alias): + data["alias"] = params.alias - files = {"file": input_source.read_contents(options.close_file)} + files = {"file": input_source.read_contents(params.close_file)} response = requests.post( url=url, files=files, @@ -95,17 +95,30 @@ def predict_async_req_post( data=data, timeout=self.request_timeout, ) - return response - def get_inference_from_queue(self, queue_id: str) -> requests.Response: + def req_get_job(self, job_id: str) -> requests.Response: + """ + Sends a request matching a given queue_id. Returns either a Job or a Document. + + :param job_id: Job ID, returned by the enqueue request. + """ + return requests.get( + f"{self.url_root}/jobs/{job_id}", + headers=self.base_headers, + timeout=self.request_timeout, + allow_redirects=False, + ) + + def req_get_inference(self, inference_id: str) -> requests.Response: """ Sends a request matching a given queue_id. Returns either a Job or a Document. - :param queue_id: queue_id received from the API + :param inference_id: Inference ID, returned by the job request. """ return requests.get( - f"{self.url_root}/jobs/{queue_id}", + f"{self.url_root}/inferences/{inference_id}", headers=self.base_headers, timeout=self.request_timeout, + allow_redirects=False, ) diff --git a/mindee/parsing/v2/common_response.py b/mindee/parsing/v2/common_response.py index 90fbe811..d51789e0 100644 --- a/mindee/parsing/v2/common_response.py +++ b/mindee/parsing/v2/common_response.py @@ -1,9 +1,18 @@ import json +from enum import Enum from mindee.logger import logger from mindee.parsing.common.string_dict import StringDict +class CommonStatus(str, Enum): + """Response status.""" + + PROCESSING = "Processing" + FAILED = "Failed" + PROCESSED = "Processed" + + class CommonResponse: """Base class for V1 & V2 responses.""" diff --git a/tests/data b/tests/data index 9ee7c180..a6b8e649 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 9ee7c18088018c8ceeab8ba705b17dc2038d56d8 +Subproject commit a6b8e649700413e6b32d886c1362eb6c75e094e8 diff --git a/tests/test_client_v2.py b/tests/test_client_v2.py index 03ab357d..22b36bbb 100644 --- a/tests/test_client_v2.py +++ b/tests/test_client_v2.py @@ -2,11 +2,12 @@ import pytest -from mindee import ClientV2, InferenceParameters, LocalResponse +from mindee import ClientV2, InferenceParameters, InferenceResponse, LocalResponse from mindee.error.mindee_error import MindeeApiV2Error from mindee.error.mindee_http_error_v2 import MindeeHTTPErrorV2 from mindee.input import LocalInputSource, PathInput from mindee.mindee_http.base_settings import USER_AGENT +from mindee.parsing.v2.inference import Inference from mindee.parsing.v2.job import Job from mindee.parsing.v2.job_response import JobResponse from tests.test_inputs import FILE_TYPES_DIR, V2_DATA_DIR @@ -21,7 +22,7 @@ def env_client(monkeypatch) -> ClientV2: @pytest.fixture def custom_base_url_client(monkeypatch) -> ClientV2: - class _FakePostResp: + class _FakePostRespError: status_code = 400 # any non-2xx will do ok = False @@ -29,25 +30,32 @@ def json(self): # Shape must match what handle_error_v2 expects return {"status": -1, "detail": "forced failure from test"} - class _FakeGetResp: + class _FakeOkProcessingJobResp: status_code = 200 ok = True def json(self): - return { - "job": { - "id": "12345678-1234-1234-1234-123456789ABC", - "model_id": "87654321-4321-4321-4321-CBA987654321", - "filename": "default_sample.jpg", - "alias": "dummy-alias.jpg", - "created_at": "2025-07-03T14:27:58.974451", - "status": "Processing", - "polling_url": "https://api-v2.mindee.net/v2/jobs/12345678-1234-1234-1234-123456789ABC", - "result_url": None, - "webhooks": [], - "error": None, - } - } + data_file = V2_DATA_DIR / "job" / "ok_processing.json" + with data_file.open("r", encoding="utf-8") as fh: + return json.load(fh) + + @property + def content(self) -> bytes: + """ + Raw (bytes) payload, mimicking `requests.Response.content`. + """ + return json.dumps(self.json()).encode("utf-8") + + class _FakeOkGetInferenceResp: + status_code = 200 + ok = True + + def json(self): + data_file = ( + V2_DATA_DIR / "products" / "financial_document" / "complete.json" + ) + with data_file.open("r", encoding="utf-8") as fh: + return json.load(fh) @property def content(self) -> bytes: @@ -58,21 +66,30 @@ def content(self) -> bytes: monkeypatch.setenv("MINDEE_V2_BASE_URL", "https://dummy-url") - def _fake_post_error(*args, **kwargs): - return _FakePostResp() + def _fake_error_post_inference_enqueue(*args, **kwargs): + return _FakePostRespError() - def _fake_get_error(*args, **kwargs): - return _FakeGetResp() + def _fake_ok_get_job(*args, **kwargs): + return _FakeOkProcessingJobResp() + + def _fake_ok_get_inference(*args, **kwargs): + return _FakeOkGetInferenceResp() monkeypatch.setattr( - "mindee.mindee_http.mindee_api_v2.requests.post", - _fake_post_error, + "mindee.mindee_http.mindee_api_v2.MindeeApiV2.req_post_inference_enqueue", + _fake_error_post_inference_enqueue, raising=True, ) monkeypatch.setattr( - "mindee.mindee_http.mindee_api_v2.requests.get", - _fake_get_error, + "mindee.mindee_http.mindee_api_v2.MindeeApiV2.req_get_job", + _fake_ok_get_job, + raising=True, + ) + + monkeypatch.setattr( + "mindee.mindee_http.mindee_api_v2.MindeeApiV2.req_get_inference", + _fake_ok_get_inference, raising=True, ) @@ -96,7 +113,9 @@ def test_enqueue_path_with_env_token(custom_base_url_client): f"{FILE_TYPES_DIR}/receipt.jpg" ) with pytest.raises(MindeeHTTPErrorV2): - custom_base_url_client.enqueue(input_doc, InferenceParameters("dummy-model")) + custom_base_url_client.enqueue_inference( + input_doc, InferenceParameters("dummy-model") + ) @pytest.mark.v2 @@ -105,24 +124,42 @@ def test_enqueue_and_parse_path_with_env_token(custom_base_url_client): f"{FILE_TYPES_DIR}/receipt.jpg" ) with pytest.raises(MindeeHTTPErrorV2): - custom_base_url_client.enqueue_and_parse( + custom_base_url_client.enqueue_and_get_inference( input_doc, InferenceParameters("dummy-model") ) +def _assert_findoc_inference(response: InferenceResponse): + # There are already detailed tests of the inference object. + # Here we are just testing whether the client can load OK. + assert isinstance(response, InferenceResponse) + assert isinstance(response.inference, Inference) + assert response.inference.id + assert response.inference.model.id + assert len(response.inference.result.fields) > 1 + + @pytest.mark.v2 def test_loads_from_prediction(env_client): input_inference = LocalResponse( V2_DATA_DIR / "products" / "financial_document" / "complete.json" ) - prediction = env_client.load_inference(input_inference) - assert prediction.inference.model.id == "12345678-1234-1234-1234-123456789abc" + response = env_client.load_inference(input_inference) + _assert_findoc_inference(response) + + +@pytest.mark.v2 +def test_get_inference(custom_base_url_client): + response = custom_base_url_client.get_inference( + "12345678-1234-1234-1234-123456789ABC" + ) + _assert_findoc_inference(response) @pytest.mark.v2 def test_error_handling(custom_base_url_client): with pytest.raises(MindeeHTTPErrorV2) as e: - custom_base_url_client.enqueue( + custom_base_url_client.enqueue_inference( PathInput( V2_DATA_DIR / "products" / "financial_document" / "default_sample.jpg" ), @@ -132,10 +169,9 @@ def test_error_handling(custom_base_url_client): assert e.detail == "forced failure from test" +@pytest.mark.v2 def test_enqueue(custom_base_url_client): - response = custom_base_url_client.parse_queued( - "12345678-1234-1234-1234-123456789ABC" - ) + response = custom_base_url_client.get_job("12345678-1234-1234-1234-123456789ABC") assert isinstance(response, JobResponse) assert isinstance(response.job, Job) assert response.job.id == "12345678-1234-1234-1234-123456789ABC" diff --git a/tests/test_client_v2_integration.py b/tests/test_client_v2_integration.py index 79b4fb3c..514dbd2c 100644 --- a/tests/test_client_v2_integration.py +++ b/tests/test_client_v2_integration.py @@ -42,7 +42,9 @@ def test_parse_file_empty_multiple_pages_must_succeed( input_doc = v2_client.source_from_path(input_path) options = InferenceParameters(findoc_model_id) - response: InferenceResponse = v2_client.enqueue_and_parse(input_doc, options) + response: InferenceResponse = v2_client.enqueue_and_get_inference( + input_doc, options + ) assert response is not None assert response.inference is not None @@ -68,7 +70,9 @@ def test_parse_file_filled_single_page_must_succeed( input_doc = v2_client.source_from_path(input_path) options = InferenceParameters(findoc_model_id) - response: InferenceResponse = v2_client.enqueue_and_parse(input_doc, options) + response: InferenceResponse = v2_client.enqueue_and_get_inference( + input_doc, options + ) assert response is not None assert response.inference is not None @@ -98,7 +102,7 @@ def test_invalid_uuid_must_throw_error_422(v2_client: ClientV2) -> None: options = InferenceParameters("INVALID MODEL ID") with pytest.raises(MindeeHTTPErrorV2) as exc_info: - v2_client.enqueue(input_doc, options) + v2_client.enqueue_inference(input_doc, options) exc: MindeeHTTPErrorV2 = exc_info.value assert exc.status == 422 diff --git a/tests/v2/test_inference_response.py b/tests/v2/test_inference_response.py index d3971c68..c5e32f6a 100644 --- a/tests/v2/test_inference_response.py +++ b/tests/v2/test_inference_response.py @@ -1,9 +1,9 @@ import json +from pathlib import Path from typing import Tuple import pytest -from mindee import ClientV2, LocalResponse from mindee.parsing.v2.inference import Inference from mindee.parsing.v2.inference_file import InferenceFile from mindee.parsing.v2.inference_model import InferenceModel @@ -14,22 +14,32 @@ from tests.test_inputs import V2_DATA_DIR -def _get_samples(name: str) -> Tuple[dict, str]: - with (V2_DATA_DIR / "inference" / f"{name}.json").open("r", encoding="utf-8") as fh: +def _get_samples(json_path: Path, rst_path: Path) -> Tuple[dict, str]: + with json_path.open("r", encoding="utf-8") as fh: json_sample = json.load(fh) try: - with (V2_DATA_DIR / "inference" / f"{name}.rst").open( - "r", encoding="utf-8" - ) as fh: + with rst_path.open("r", encoding="utf-8") as fh: rst_sample = fh.read() except FileNotFoundError: rst_sample = "" return json_sample, rst_sample +def _get_inference_samples(name: str) -> Tuple[dict, str]: + json_path = V2_DATA_DIR / "inference" / f"{name}.json" + rst_path = V2_DATA_DIR / "inference" / f"{name}.rst" + return _get_samples(json_path, rst_path) + + +def _get_product_samples(product, name: str) -> Tuple[dict, str]: + json_path = V2_DATA_DIR / "products" / product / f"{name}.json" + rst_path = V2_DATA_DIR / "products" / product / f"{name}.rst" + return _get_samples(json_path, rst_path) + + @pytest.mark.v2 def test_deep_nested_fields(): - json_sample, rst_sample = _get_samples("deep_nested_fields") + json_sample, rst_sample = _get_inference_samples("deep_nested_fields") inference_result = InferenceResponse(json_sample) assert isinstance(inference_result.inference, Inference) assert isinstance( @@ -99,7 +109,7 @@ def test_deep_nested_fields(): @pytest.mark.v2 def test_standard_field_types(): - json_sample, rst_sample = _get_samples("standard_field_types") + json_sample, rst_sample = _get_inference_samples("standard_field_types") inference_result = InferenceResponse(json_sample) assert isinstance(inference_result.inference, Inference) assert isinstance( @@ -119,7 +129,7 @@ def test_standard_field_types(): @pytest.mark.v2 def test_raw_texts(): - json_sample, rst_sample = _get_samples("raw_texts") + json_sample, rst_sample = _get_inference_samples("raw_texts") inference_result = InferenceResponse(json_sample) assert isinstance(inference_result.inference, Inference) @@ -134,30 +144,30 @@ def test_raw_texts(): @pytest.mark.v2 def test_full_inference_response(): - client_v2 = ClientV2("dummy") - load_response = client_v2.load_inference( - LocalResponse(V2_DATA_DIR / "products" / "financial_document" / "complete.json") - ) + json_sample, rst_sample = _get_product_samples("financial_document", "complete") + inference_result = InferenceResponse(json_sample) - assert isinstance(load_response.inference, Inference) - assert load_response.inference.id == "12345678-1234-1234-1234-123456789abc" - assert isinstance(load_response.inference.result.fields.date, SimpleField) - assert load_response.inference.result.fields.date.value == "2019-11-02" - assert isinstance(load_response.inference.result.fields.taxes, ListField) - assert isinstance(load_response.inference.result.fields.taxes.items[0], ObjectField) + assert isinstance(inference_result.inference, Inference) + assert inference_result.inference.id == "12345678-1234-1234-1234-123456789abc" + assert isinstance(inference_result.inference.result.fields.date, SimpleField) + assert inference_result.inference.result.fields.date.value == "2019-11-02" + assert isinstance(inference_result.inference.result.fields.taxes, ListField) + assert isinstance( + inference_result.inference.result.fields.taxes.items[0], ObjectField + ) assert ( - load_response.inference.result.fields.customer_address.fields.city.value + inference_result.inference.result.fields.customer_address.fields.city.value == "New York" ) assert ( - load_response.inference.result.fields.taxes.items[0].fields["base"].value + inference_result.inference.result.fields.taxes.items[0].fields["base"].value == 31.5 ) - assert isinstance(load_response.inference.model, InferenceModel) - assert load_response.inference.model.id == "12345678-1234-1234-1234-123456789abc" + assert isinstance(inference_result.inference.model, InferenceModel) + assert inference_result.inference.model.id == "12345678-1234-1234-1234-123456789abc" - assert isinstance(load_response.inference.file, InferenceFile) - assert load_response.inference.file.name == "complete.jpg" - assert not load_response.inference.file.alias - assert not load_response.inference.result.options + assert isinstance(inference_result.inference.file, InferenceFile) + assert inference_result.inference.file.name == "complete.jpg" + assert not inference_result.inference.file.alias + assert not inference_result.inference.result.options