From 83c8b9fed5e09d2682a05621de0c6141edcd9590 Mon Sep 17 00:00:00 2001 From: Sebastian <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:07:56 +0200 Subject: [PATCH 01/14] :arrow_up: update dependencies (#178) --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 45e156ba..450b04f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ # # pip-compile # + certifi==2023.7.22 # via requests charset-normalizer==3.3.0 @@ -18,11 +19,11 @@ packaging==23.2 # via pikepdf pikepdf==6.2.9 # via mindee (setup.py) -pillow==10.0.1 +pillow==10.1.0 # via pikepdf pytz==2023.3.post1 # via mindee (setup.py) requests==2.31.0 # via mindee (setup.py) -urllib3==2.0.6 +urllib3==2.0.7 # via requests From 494985b602bee18f20f63c1617e4a62d9afddebc Mon Sep 17 00:00:00 2001 From: Sebastian <130448732+sebastianMindee@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:38:40 +0200 Subject: [PATCH 02/14] :bookmark: version 3.13.2 (#179) --- CHANGELOG.md | 5 +++++ mindee/version | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e2fe09..cb456cd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Mindee Python API Library Changelog +## v3.13.2 - 2023-10-18 +### Changes +* :arrow_up: update `urllib` & `pillow` dependencies + + ## v3.13.1 - 2023-10-03 ### Changes * :arrow_up: update `urllib`, `charset-normalizer` & `package` dependencies diff --git a/mindee/version b/mindee/version index 7918c7ab..97c68419 100644 --- a/mindee/version +++ b/mindee/version @@ -1 +1 @@ -3.13.1 \ No newline at end of file +3.13.2 \ No newline at end of file From 8e35c5f646e6677c859c47bbc362a6a309f49dff Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Tue, 17 Oct 2023 16:51:52 +0200 Subject: [PATCH 03/14] fix display_cropping script --- examples/display_cropping.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/display_cropping.py b/examples/display_cropping.py index 1e931859..c1412f34 100644 --- a/examples/display_cropping.py +++ b/examples/display_cropping.py @@ -13,6 +13,7 @@ from typing import List, Tuple import cv2 +from mindee.parsing.common.predict_response import PredictResponse import numpy as np from mindee import Client, product From 7d50b8c444928dd6a1d1001c77b884d8d224aa7f Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Tue, 17 Oct 2023 17:40:41 +0200 Subject: [PATCH 04/14] add better error handling --- examples/display_cropping.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/display_cropping.py b/examples/display_cropping.py index c1412f34..1e931859 100644 --- a/examples/display_cropping.py +++ b/examples/display_cropping.py @@ -13,7 +13,6 @@ from typing import List, Tuple import cv2 -from mindee.parsing.common.predict_response import PredictResponse import numpy as np from mindee import Client, product From 88e6d14c2591aa0b5d3c2ab148482695504b3763 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Tue, 17 Oct 2023 18:11:19 +0200 Subject: [PATCH 05/14] :recycle: update error handling system --- tests/mindee_http/test_error.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/mindee_http/test_error.py b/tests/mindee_http/test_error.py index 791d13b2..38cce1fd 100644 --- a/tests/mindee_http/test_error.py +++ b/tests/mindee_http/test_error.py @@ -1,6 +1,5 @@ import json from pathlib import Path - import pytest from mindee import Client, product From c4c1e2825551c64e7e65974e95380d720f0f8dc2 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Tue, 17 Oct 2023 18:12:33 +0200 Subject: [PATCH 06/14] fix pre-commit --- tests/mindee_http/test_error.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mindee_http/test_error.py b/tests/mindee_http/test_error.py index 38cce1fd..791d13b2 100644 --- a/tests/mindee_http/test_error.py +++ b/tests/mindee_http/test_error.py @@ -1,5 +1,6 @@ import json from pathlib import Path + import pytest from mindee import Client, product From 9244acd8a4d349d2b1c8daece5ea3c1efb8e9d48 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 11:24:18 +0200 Subject: [PATCH 07/14] add feedback & fetch (untested) --- mindee/__init__.py | 1 + mindee/client.py | 39 ++++++++++++- mindee/mindee_http/base_endpoint.py | 15 +++++ mindee/mindee_http/endpoint.py | 67 ++++++++++++++-------- mindee/parsing/common/__init__.py | 1 + mindee/parsing/common/feedback_response.py | 12 ++++ mindee/parsing/common/predict_response.py | 6 +- 7 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 mindee/mindee_http/base_endpoint.py create mode 100644 mindee/parsing/common/feedback_response.py diff --git a/mindee/__init__.py b/mindee/__init__.py index af37bd29..bf129c3f 100644 --- a/mindee/__init__.py +++ b/mindee/__init__.py @@ -2,4 +2,5 @@ from mindee.client import Client, PageOptions from mindee.parsing.common.api_response import ApiResponse from mindee.parsing.common.async_predict_response import AsyncPredictResponse, Job +from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.predict_response import PredictResponse diff --git a/mindee/client.py b/mindee/client.py index 01053ae0..7aac9a88 100644 --- a/mindee/client.py +++ b/mindee/client.py @@ -16,8 +16,10 @@ from mindee.mindee_http.error import handle_error from mindee.mindee_http.mindee_api import MindeeApi from mindee.parsing.common.async_predict_response import AsyncPredictResponse +from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.inference import Inference, TypeInference from mindee.parsing.common.predict_response import PredictResponse +from mindee.parsing.common.string_dict import StringDict OTS_OWNER = "mindee" @@ -175,7 +177,7 @@ def parse_queued( :param product_class: The document class to use. The response object will be instantiated based on this parameter. - :param queue_id: queue_id received from the API + :param queue_id: queue_id received from the API. :param endpoint: For custom endpoints, an endpoint has to be given. """ if not endpoint: @@ -275,6 +277,39 @@ def enqueue_and_parse( return poll_results + def send_feedback( + self, product_class: Type[Inference], document_id: str, feedback: StringDict + ) -> FeedbackResponse: + """ + Send a feedback for a document. + + :param product_class: The document class to use. + The response object will be instantiated based on this parameter. + + :param document_id: The id of the document to send feedback to. + :param feedback: Feedback to send. + """ + endpoint = self._initialize_ots_endpoint(product_class) + + feedback_response = endpoint.document_feedback_req_put(document_id, feedback) + return FeedbackResponse(feedback_response.json()) + + def get_document( + self, product_class: Type[Inference], document_id: str + ) -> PredictResponse: + """ + Fetch prediction results from a document already processed. + + :param product_class: The document class to use. + The response object will be instantiated based on this parameter. + + :param document_id: The id of the document to send feedback to. + """ + endpoint = self._initialize_ots_endpoint(product_class) + + response = endpoint.document_req_get(document_id) + return PredictResponse(product_class, response.json()) + def _make_request( self, product_class: Type[Inference], @@ -360,7 +395,7 @@ def _get_queued_document( return AsyncPredictResponse(product_class, queue_response.json()) - def _initialize_ots_endpoint(self, product_class) -> Endpoint: + def _initialize_ots_endpoint(self, product_class: type[Inference]) -> Endpoint: if product_class.__name__ == "CustomV1": raise TypeError("Missing endpoint specifications for custom build.") endpoint_info: Dict[str, str] = product_class.get_endpoint_info(product_class) diff --git a/mindee/mindee_http/base_endpoint.py b/mindee/mindee_http/base_endpoint.py new file mode 100644 index 00000000..4f2619c8 --- /dev/null +++ b/mindee/mindee_http/base_endpoint.py @@ -0,0 +1,15 @@ +from abc import ABC + +from mindee.mindee_http.mindee_api import MindeeApi + + +class BaseEndpoint(ABC): + """Base endpoint for the Mindee API.""" + + def __init__(self, settings: MindeeApi) -> None: + """ + Base API endpoint class for all endpoints. + + :param settings: Settings relating to all endpoints + """ + self.settings = settings diff --git a/mindee/mindee_http/endpoint.py b/mindee/mindee_http/endpoint.py index a4f9b059..d8ec69d3 100644 --- a/mindee/mindee_http/endpoint.py +++ b/mindee/mindee_http/endpoint.py @@ -1,12 +1,15 @@ from typing import Union import requests +from requests import Response from mindee.input.sources import LocalInputSource, UrlInputSource +from mindee.mindee_http.base_endpoint import BaseEndpoint from mindee.mindee_http.mindee_api import MindeeApi +from mindee.parsing.common.string_dict import StringDict -class Endpoint: +class Endpoint(BaseEndpoint): """Generic API endpoint for a product.""" def __init__( @@ -19,10 +22,10 @@ def __init__( :param url_name: name of the product as it appears in the URL :param version: interface version """ + super().__init__(settings) self.owner = owner self.url_name = url_name self.version = version - self.settings = settings def predict_req_post( self, @@ -113,20 +116,53 @@ def document_queue_req_get(self, queue_id: str) -> requests.Response: This performs a cropping operation on the server and will increase response time. """ - response = requests.get( + return requests.get( f"{self.settings.url_root}/documents/queue/{queue_id}", headers=self.settings.base_headers, timeout=self.settings.request_timeout, ) - return response - def openapi_get_req(self): + def openapi_get_req(self) -> Response: """Get the OpenAPI specification of the product.""" - response = requests.get( + return requests.get( f"{self.settings.url_root}/openapi.json", headers=self.settings.base_headers, timeout=self.settings.request_timeout, ) + + def document_feedback_req_put( + self, document_id: str, feedback: StringDict + ) -> Response: + """ + Send a feedback. + + :param document_id: ID of the document to send feedback to. + :param feedback: Feedback object to send. + """ + return requests.put( + f"{self.settings.url_root}/documents/{document_id}/feedback", + headers=self.settings.base_headers, + data=feedback, + timeout=self.settings.request_timeout, + ) + + def document_req_get(self, document_id: str) -> requests.Response: + """ + Make a request to GET annotations for a document. + + :param document_id: ID of the document + """ + params = { + "include_annotations": True, + "include_candidates": True, + "global_orientation": True, + } + response = requests.get( + f"{self.settings.url_root}/documents/{document_id}", + headers=self.settings.base_headers, + params=params, + timeout=self.settings.request_timeout, + ) return response @@ -177,25 +213,6 @@ def training_async_req_post( ) return response - def document_req_get(self, document_id: str) -> requests.Response: - """ - Make a request to GET annotations for a document. - - :param document_id: ID of the document - """ - params = { - "include_annotations": True, - "include_candidates": True, - "global_orientation": True, - } - response = requests.get( - f"{self.settings.url_root}/documents/{document_id}", - headers=self.settings.base_headers, - params=params, - timeout=self.settings.request_timeout, - ) - return response - def document_req_del(self, document_id: str) -> requests.Response: """ Make a request to DELETE a document. diff --git a/mindee/parsing/common/__init__.py b/mindee/parsing/common/__init__.py index 544fd225..7d5bf8ed 100644 --- a/mindee/parsing/common/__init__.py +++ b/mindee/parsing/common/__init__.py @@ -3,6 +3,7 @@ from mindee.parsing.common.async_predict_response import AsyncPredictResponse from mindee.parsing.common.document import Document from mindee.parsing.common.extras import CropperExtra, Extras +from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.inference import Inference, TypeInference from mindee.parsing.common.job import Job from mindee.parsing.common.ocr.mvision_v1 import MVisionV1 diff --git a/mindee/parsing/common/feedback_response.py b/mindee/parsing/common/feedback_response.py new file mode 100644 index 00000000..3c78c57d --- /dev/null +++ b/mindee/parsing/common/feedback_response.py @@ -0,0 +1,12 @@ +from mindee.parsing.common.api_response import ApiResponse +from mindee.parsing.common.string_dict import StringDict + + +class FeedbackResponse(ApiResponse): + """Wrapper for feedback response.""" + feedback: StringDict + + def __init__(self, server_response: StringDict)-> None: + """Feedback endpoint""" + super().__init__(server_response) + self.feedback = server_response["feedback"] diff --git a/mindee/parsing/common/predict_response.py b/mindee/parsing/common/predict_response.py index 2986e57e..5cc37aaa 100644 --- a/mindee/parsing/common/predict_response.py +++ b/mindee/parsing/common/predict_response.py @@ -1,4 +1,4 @@ -from typing import Generic +from typing import Generic, Type from mindee.parsing.common.api_response import ApiResponse, StringDict from mindee.parsing.common.document import Document @@ -15,7 +15,9 @@ class PredictResponse(Generic[TypeInference], ApiResponse): document: Document """The document object, properly parsed after being retrieved from the server.""" - def __init__(self, prediction_type, raw_response: StringDict) -> None: + def __init__( + self, prediction_type: Type[TypeInference], raw_response: StringDict + ) -> None: """ Container for the raw API response and the parsed document. From aedd44123bfc59356425ab5d8461b412fed93ac6 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 11:39:58 +0200 Subject: [PATCH 08/14] add very basic tests --- mindee/client.py | 4 ++++ tests/test_client.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/mindee/client.py b/mindee/client.py index 7aac9a88..4cf65547 100644 --- a/mindee/client.py +++ b/mindee/client.py @@ -289,6 +289,8 @@ def send_feedback( :param document_id: The id of the document to send feedback to. :param feedback: Feedback to send. """ + if not document_id or len(document_id) == 0: + raise RuntimeError("Invalid document_id.") endpoint = self._initialize_ots_endpoint(product_class) feedback_response = endpoint.document_feedback_req_put(document_id, feedback) @@ -305,6 +307,8 @@ def get_document( :param document_id: The id of the document to send feedback to. """ + if not document_id or len(document_id) == 0: + raise RuntimeError("Invalid document_id.") endpoint = self._initialize_ots_endpoint(product_class) response = endpoint.document_req_get(document_id) diff --git a/tests/test_client.py b/tests/test_client.py index 1d128e7e..c95b911f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,6 +34,16 @@ def test_parse_path_without_token(empty_client: Client): empty_client.parse(product.ReceiptV4, input_doc) +def test_fetch_without_id(empty_client: Client): + with pytest.raises(RuntimeError): + empty_client.get_document(product.ReceiptV4, "") + + +def test_feedback_without_id(empty_client: Client): + with pytest.raises(RuntimeError): + empty_client.send_feedback(product.ReceiptV4, "", {}) + + def test_parse_path_with_env_token(env_client: Client): with pytest.raises(MindeeHTTPException): input_doc = env_client.source_from_path(FILE_TYPES_DIR / "pdf" / "blank.pdf") From 09ce79e2e2d307716e2eb24c055593b005ff995b Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 11:58:44 +0200 Subject: [PATCH 09/14] add feedback & fetch to CLI, untested --- mindee/cli.py | 154 +++++++++++++-------- mindee/mindee_http/base_endpoint.py | 2 +- mindee/parsing/common/feedback_response.py | 7 +- 3 files changed, 98 insertions(+), 65 deletions(-) diff --git a/mindee/cli.py b/mindee/cli.py index 106cd833..b70597be 100644 --- a/mindee/cli.py +++ b/mindee/cli.py @@ -9,6 +9,7 @@ from mindee.input.sources import LocalInputSource, UrlInputSource from mindee.parsing.common.async_predict_response import AsyncPredictResponse from mindee.parsing.common.document import Document, serialize_for_json +from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.inference import Inference, TypeInference from mindee.parsing.common.predict_response import PredictResponse @@ -163,22 +164,24 @@ def __init__( def call_endpoint(self) -> None: """Calls the proper type of endpoint according to given command.""" - # if self.parsed_args.parse_type == "parse": - self.call_parse() - - # else: - # self.call_fetch() - - # def call_fetch(self) -> None: - # """Fetches an API's for a previously enqueued document.""" - # response: AsyncPredictResponse = self._parse_queued() - # if self.parsed_args.output_type == "raw": - # print(response.raw_http) - # else: - # if not hasattr(response, "document") or response.document is None: - # print(response.job) - # else: - # print(response.document) + if self.parsed_args.parse_type == "parse": + self.call_parse() + elif self.parsed_args.parse_type == "feedback": + self.call_feedback() + else: + self.call_fetch() + + def call_fetch(self) -> None: + """Fetches an API's for a previously enqueued document.""" + response: PredictResponse = self._get_doc() + if self.parsed_args.output_type == "raw": + print(response.raw_http) + print(response.document) + + def call_feedback(self) -> None: + """Sends feedback to an API.""" + response: FeedbackResponse = self._send_feedback() + print(response.raw_http) def call_parse(self) -> None: """Calls an endpoint with the appropriate method, and displays the results.""" @@ -251,20 +254,18 @@ def _parse_async(self) -> AsyncPredictResponse: endpoint=custom_endpoint, ) - # def _parse_queued(self) -> AsyncPredictResponse: - # """Fetches a queue's result from a document's id.""" - # custom_endpoint: Optional[Endpoint] = None - # if self.parsed_args.product_name == "custom": - # self.client.create_endpoint( - # self.parsed_args.endpoint_name, - # self.parsed_args.account_name, - # self.parsed_args.api_version, - # ) - # return self.client.parse_queued( - # self.document_info.doc_class, - # self.parsed_args.queue_id, - # custom_endpoint, - # ) + def _get_doc(self) -> PredictResponse: + """Fetches a previous parsing's result from a document's id.""" + return self.client.get_document( + self.document_info.doc_class, self.parsed_args.document_id + ) + + def _send_feedback(self) -> FeedbackResponse: + return self.client.send_feedback( + self.document_info.doc_class, + self.parsed_args.document_id, + self.parsed_args.feedback, + ) def _doc_str(self, output_type: str, doc_response: Document) -> str: if output_type == "parsed": @@ -273,29 +274,21 @@ def _doc_str(self, output_type: str, doc_response: Document) -> str: def _set_args(self) -> Namespace: """Parse command line arguments.""" - # call_parser = self.parser.add_subparsers( - # dest="parse_type", - # required=True, - # ) - # parse_subparser = call_parser.add_parser("parse") - # fetch_subparser = call_parser.add_parser("fetch") - - # parse_product_subparsers = parse_subparser.add_subparsers( - # dest="product_name", - # required=True, - # ) parse_product_subparsers = self.parser.add_subparsers( dest="product_name", required=True, ) - # fetch_product_subparsers = fetch_subparser.add_subparsers( - # dest="product_name", - # required=True, - # ) - for name, info in DOCUMENTS.items(): - parse_subp = parse_product_subparsers.add_parser(name, help=info.help) + parse_subparser = parse_product_subparsers.add_parser(name, help=info.help) + + call_parser = parse_subparser.add_subparsers( + dest="parse_type", + ) + parse_subp = call_parser.add_parser("parse") + fetch_subp = call_parser.add_parser("fetch") + feedback_subp = call_parser.add_parser("feedback") + self._add_main_options(parse_subp) self._add_sending_options(parse_subp) self._add_display_options(parse_subp) @@ -309,6 +302,9 @@ def _set_args(self) -> Namespace: action="store_true", help="include full document text in response", ) + self._add_main_options(feedback_subp) + self._add_doc_id_option(feedback_subp) + self._add_feedback_options(feedback_subp) if info.is_async and info.is_sync: parse_subp.add_argument( @@ -321,16 +317,19 @@ def _set_args(self) -> Namespace: default=False, ) - # if info.is_async: - # fetch_subp = fetch_product_subparsers.add_parser(name, help=info.help) - # self._add_main_options(fetch_subp) - # self._add_display_options(fetch_subp) - # self._add_fetch_options(fetch_subp) + self._add_main_options(fetch_subp) + self._add_display_options(fetch_subp) + self._add_doc_id_option(fetch_subp) parsed_args = self.parser.parse_args() return parsed_args def _add_main_options(self, parser: ArgumentParser) -> None: + """ + Adds main options for most parsings. + + :param parser: current parser/subparser. + """ parser.add_argument( "-k", "--key", @@ -341,7 +340,11 @@ def _add_main_options(self, parser: ArgumentParser) -> None: ) def _add_display_options(self, parser: ArgumentParser) -> None: - """Adds options related to output/display of a document (parse, parse-queued).""" + """ + Adds options related to output/display of a document (parse, parse-queued). + + :param parser: current parser/subparser. + """ parser.add_argument( "-o", "--output-type", @@ -354,8 +357,28 @@ def _add_display_options(self, parser: ArgumentParser) -> None: "- parsed: the validated and parsed data fields\n", ) + def _add_feedback_options(self, parser: ArgumentParser) -> None: + """ + Add options related to feedbacks. + + :param parser: current parser/subparser. + """ + parser.add_argument( + "-f", + "--feedback", + dest="feedback", + type=json.loads, + required=True, + help="""Feedback on the information to send back. +This parameter is a dictionary, and should not bear duplicate keys.""", + ) + def _add_sending_options(self, parser: ArgumentParser) -> None: - """Adds options for sending requests (parse, enqueue).""" + """ + Adds options for sending requests (parse, enqueue). + + :param parser: current parser/subparser. + """ parser.add_argument( "-i", "--input-type", @@ -386,15 +409,23 @@ def _add_sending_options(self, parser: ArgumentParser) -> None: ) parser.add_argument(dest="path", help="Full path to the file") - # def _add_fetch_options(self, parser: ArgumentParser): - # """Adds an option to provide the queue ID for an async document.""" - # parser.add_argument( - # dest="queue_id", - # help="Async queue ID for a document (required)", - # ) + def _add_doc_id_option(self, parser: ArgumentParser): + """ + Adds an option to provide the queue ID for an async document. + + :param parser: current parser/subparser. + """ + parser.add_argument( + dest="document_id", + help="Async queue ID for a document (required)", + ) def _add_custom_options(self, parser: ArgumentParser): - """Adds options to custom-type documents.""" + """ + Adds options to custom-type documents. + + :param parser: current parser/subparser. + """ parser.add_argument( "-a", "--account", @@ -418,6 +449,7 @@ def _add_custom_options(self, parser: ArgumentParser): ) def _get_input_doc(self) -> Union[LocalInputSource, UrlInputSource]: + """Loads an input document.""" if self.parsed_args.input_type == "file": with open(self.parsed_args.path, "rb", buffering=30) as file_handle: return self.client.source_from_file(file_handle) diff --git a/mindee/mindee_http/base_endpoint.py b/mindee/mindee_http/base_endpoint.py index 4f2619c8..af4e30ac 100644 --- a/mindee/mindee_http/base_endpoint.py +++ b/mindee/mindee_http/base_endpoint.py @@ -5,7 +5,7 @@ class BaseEndpoint(ABC): """Base endpoint for the Mindee API.""" - + def __init__(self, settings: MindeeApi) -> None: """ Base API endpoint class for all endpoints. diff --git a/mindee/parsing/common/feedback_response.py b/mindee/parsing/common/feedback_response.py index 3c78c57d..54ab67d2 100644 --- a/mindee/parsing/common/feedback_response.py +++ b/mindee/parsing/common/feedback_response.py @@ -4,9 +4,10 @@ class FeedbackResponse(ApiResponse): """Wrapper for feedback response.""" + feedback: StringDict - - def __init__(self, server_response: StringDict)-> None: - """Feedback endpoint""" + + def __init__(self, server_response: StringDict) -> None: + """Feedback endpoint.""" super().__init__(server_response) self.feedback = server_response["feedback"] From 14872eaca3864dda06852054114096e530744695 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 14:14:24 +0200 Subject: [PATCH 10/14] misc fixes + CLI update --- docs/parsing/common.rst | 9 +++++++ mindee/cli.py | 59 ++++++++++------------------------------- mindee/client.py | 33 ++++++++++++++++++++--- tests/test_cli.py | 37 +++++++++++++++----------- 4 files changed, 74 insertions(+), 64 deletions(-) diff --git a/docs/parsing/common.rst b/docs/parsing/common.rst index 5009afbd..1ddd7b39 100644 --- a/docs/parsing/common.rst +++ b/docs/parsing/common.rst @@ -56,6 +56,15 @@ Job :members: +Miscellaneous Parsing +===================== + +FeedbackResponse +---------------- +.. autoclass:: mindee.parsing.common.feedback_response.FeedbackResponse + :members: + + OCR Extraction ============== diff --git a/mindee/cli.py b/mindee/cli.py index b70597be..1dc3550f 100644 --- a/mindee/cli.py +++ b/mindee/cli.py @@ -9,7 +9,6 @@ from mindee.input.sources import LocalInputSource, UrlInputSource from mindee.parsing.common.async_predict_response import AsyncPredictResponse from mindee.parsing.common.document import Document, serialize_for_json -from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.inference import Inference, TypeInference from mindee.parsing.common.predict_response import PredictResponse @@ -156,8 +155,8 @@ def __init__( self.parser = parser if parser else ArgumentParser(description="Mindee_API") self.parsed_args = parsed_args if parsed_args else self._set_args() self.client = client if client else Client(api_key=self.parsed_args.api_key) - # if self.parsed_args.parse_type == "parse": - self.input_doc = input_doc if input_doc else self._get_input_doc() + if self.parsed_args.parse_type == "parse": + self.input_doc = input_doc if input_doc else self._get_input_doc() self.document_info = ( document_info if document_info else DOCUMENTS[self.parsed_args.product_name] ) @@ -166,22 +165,25 @@ def call_endpoint(self) -> None: """Calls the proper type of endpoint according to given command.""" if self.parsed_args.parse_type == "parse": self.call_parse() - elif self.parsed_args.parse_type == "feedback": - self.call_feedback() else: self.call_fetch() def call_fetch(self) -> None: """Fetches an API's for a previously enqueued document.""" - response: PredictResponse = self._get_doc() + custom_endpoint: Optional[Endpoint] = None + if self.parsed_args.product_name == "custom": + custom_endpoint = self.client.create_endpoint( + self.parsed_args.endpoint_name, + self.parsed_args.account_name, + self.parsed_args.api_version, + ) + response: PredictResponse = self.client.get_document( + self.document_info.doc_class, self.parsed_args.document_id, custom_endpoint + ) if self.parsed_args.output_type == "raw": print(response.raw_http) - print(response.document) - - def call_feedback(self) -> None: - """Sends feedback to an API.""" - response: FeedbackResponse = self._send_feedback() - print(response.raw_http) + else: + print(response.document) def call_parse(self) -> None: """Calls an endpoint with the appropriate method, and displays the results.""" @@ -254,19 +256,6 @@ def _parse_async(self) -> AsyncPredictResponse: endpoint=custom_endpoint, ) - def _get_doc(self) -> PredictResponse: - """Fetches a previous parsing's result from a document's id.""" - return self.client.get_document( - self.document_info.doc_class, self.parsed_args.document_id - ) - - def _send_feedback(self) -> FeedbackResponse: - return self.client.send_feedback( - self.document_info.doc_class, - self.parsed_args.document_id, - self.parsed_args.feedback, - ) - def _doc_str(self, output_type: str, doc_response: Document) -> str: if output_type == "parsed": return json.dumps(doc_response, indent=2, default=serialize_for_json) @@ -287,7 +276,6 @@ def _set_args(self) -> Namespace: ) parse_subp = call_parser.add_parser("parse") fetch_subp = call_parser.add_parser("fetch") - feedback_subp = call_parser.add_parser("feedback") self._add_main_options(parse_subp) self._add_sending_options(parse_subp) @@ -302,9 +290,6 @@ def _set_args(self) -> Namespace: action="store_true", help="include full document text in response", ) - self._add_main_options(feedback_subp) - self._add_doc_id_option(feedback_subp) - self._add_feedback_options(feedback_subp) if info.is_async and info.is_sync: parse_subp.add_argument( @@ -357,22 +342,6 @@ def _add_display_options(self, parser: ArgumentParser) -> None: "- parsed: the validated and parsed data fields\n", ) - def _add_feedback_options(self, parser: ArgumentParser) -> None: - """ - Add options related to feedbacks. - - :param parser: current parser/subparser. - """ - parser.add_argument( - "-f", - "--feedback", - dest="feedback", - type=json.loads, - required=True, - help="""Feedback on the information to send back. -This parameter is a dictionary, and should not bear duplicate keys.""", - ) - def _add_sending_options(self, parser: ArgumentParser) -> None: """ Adds options for sending requests (parse, enqueue). diff --git a/mindee/client.py b/mindee/client.py index 4cf65547..4448fed2 100644 --- a/mindee/client.py +++ b/mindee/client.py @@ -278,7 +278,11 @@ def enqueue_and_parse( return poll_results def send_feedback( - self, product_class: Type[Inference], document_id: str, feedback: StringDict + self, + product_class: Type[Inference], + document_id: str, + feedback: StringDict, + endpoint: Optional[Endpoint] = None, ) -> FeedbackResponse: """ Send a feedback for a document. @@ -288,16 +292,28 @@ def send_feedback( :param document_id: The id of the document to send feedback to. :param feedback: Feedback to send. + :param endpoint: For custom endpoints, an endpoint has to be given. """ if not document_id or len(document_id) == 0: raise RuntimeError("Invalid document_id.") - endpoint = self._initialize_ots_endpoint(product_class) + if not endpoint: + endpoint = self._initialize_ots_endpoint(product_class) feedback_response = endpoint.document_feedback_req_put(document_id, feedback) + if not feedback_response.ok: + raise handle_error( + str(product_class.endpoint_name), + feedback_response.json(), + feedback_response.status_code, + ) + return FeedbackResponse(feedback_response.json()) def get_document( - self, product_class: Type[Inference], document_id: str + self, + product_class: Type[Inference], + document_id: str, + endpoint: Optional[Endpoint] = None, ) -> PredictResponse: """ Fetch prediction results from a document already processed. @@ -306,12 +322,21 @@ def get_document( The response object will be instantiated based on this parameter. :param document_id: The id of the document to send feedback to. + :param endpoint: For custom endpoints, an endpoint has to be given. """ if not document_id or len(document_id) == 0: raise RuntimeError("Invalid document_id.") - endpoint = self._initialize_ots_endpoint(product_class) + if not endpoint: + endpoint = self._initialize_ots_endpoint(product_class) response = endpoint.document_req_get(document_id) + if not response.ok: + raise handle_error( + str(product_class.endpoint_name), + response.json(), + response.status_code, + ) + return PredictResponse(product_class, response.json()) def _make_request( diff --git a/tests/test_cli.py b/tests/test_cli.py index 31e11343..b093c8ac 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,10 @@ from argparse import Namespace +from sys import api_version import pytest from mindee.cli import MindeeParser -from mindee.mindee_http.error import MindeeHTTPException +from mindee.mindee_http.error import MindeeHTTPClientException, MindeeHTTPException from tests.utils import clear_envvars @@ -22,7 +23,7 @@ def custom_doc(monkeypatch): output_type="summary", include_words=False, path="./tests/data/file_types/pdf/blank.pdf", - # parse_type="parse", + parse_type="parse", async_parse=False, ) @@ -32,13 +33,14 @@ def ots_doc(monkeypatch): clear_envvars(monkeypatch) return Namespace( api_key="dummy", + product_name="invoice", cut_doc=False, doc_pages=3, input_type="path", output_type="summary", include_words=False, path="./tests/data/products/invoices/invoice.pdf", - # parse_type="parse", + parse_type="parse", async_parse=False, ) @@ -48,12 +50,13 @@ def ots_doc_enqueue_and_parse(monkeypatch): clear_envvars(monkeypatch) return Namespace( api_key="dummy", + product_name="invoice-splitter", cut_doc=False, doc_pages=3, input_type="path", include_words=False, path="./tests/data/products/invoice_splitter/default_sample.pdf", - # parse_type="parse", + parse_type="parse", async_parse=True, ) @@ -64,9 +67,13 @@ def ots_doc_fetch(monkeypatch): return Namespace( api_key="dummy", output_type="summary", + product_name="custom", + endpoint_name="dummy-endpoint", + account_name="dummy", + api_version="dummy", queue_id="dummy-queue-id", call_method="parse-queued", - # parse_type="fetch", + parse_type="fetch", ) @@ -148,13 +155,13 @@ def test_cli_invoice_splitter_enqueue(ots_doc_enqueue_and_parse): parser.call_endpoint() -# def test_cli_invoice_splitter_parse_queued(ots_doc_fetch): -# ots_doc_fetch.product_name = "invoice-splitter" -# ots_doc_fetch.api_key = "" -# with pytest.raises(RuntimeError): -# parser = MindeeParser(parsed_args=ots_doc_fetch) -# parser.call_endpoint() -# ots_doc_fetch.api_key = "dummy" -# with pytest.raises(MindeeHTTPException): -# parser = MindeeParser(parsed_args=ots_doc_fetch) -# parser.call_endpoint() +def test_cli_fetch(ots_doc_fetch): + ots_doc_fetch.document_id = "dummy-document-id" + ots_doc_fetch.api_key = "" + with pytest.raises(RuntimeError): + parser = MindeeParser(parsed_args=ots_doc_fetch) + parser.call_endpoint() + ots_doc_fetch.api_key = "dummy" + with pytest.raises(MindeeHTTPClientException): + parser = MindeeParser(parsed_args=ots_doc_fetch) + parser.call_endpoint() From d0d9cee66edcf3784b498eb3f6877836c6a430c9 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 14:39:54 +0200 Subject: [PATCH 11/14] fix typo --- mindee/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mindee/client.py b/mindee/client.py index 4448fed2..6bd672d4 100644 --- a/mindee/client.py +++ b/mindee/client.py @@ -424,7 +424,7 @@ def _get_queued_document( return AsyncPredictResponse(product_class, queue_response.json()) - def _initialize_ots_endpoint(self, product_class: type[Inference]) -> Endpoint: + def _initialize_ots_endpoint(self, product_class: Type[Inference]) -> Endpoint: if product_class.__name__ == "CustomV1": raise TypeError("Missing endpoint specifications for custom build.") endpoint_info: Dict[str, str] = product_class.get_endpoint_info(product_class) From 7d74f3a9f7a942fb7f62a2831e87f43e04fda662 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 18:59:02 +0200 Subject: [PATCH 12/14] fix feedback --- mindee/cli.py | 123 ++++++++++++++++++++++------ mindee/client.py | 30 ------- mindee/mindee_http/endpoint.py | 38 ++++----- tests/api/test_feedback_response.py | 12 +++ tests/api/test_response.py | 7 +- tests/data | 2 +- tests/test_cli.py | 21 +++-- tests/test_client.py | 5 -- 8 files changed, 145 insertions(+), 93 deletions(-) create mode 100644 tests/api/test_feedback_response.py diff --git a/mindee/cli.py b/mindee/cli.py index 1dc3550f..fa589c6a 100644 --- a/mindee/cli.py +++ b/mindee/cli.py @@ -9,8 +9,10 @@ from mindee.input.sources import LocalInputSource, UrlInputSource from mindee.parsing.common.async_predict_response import AsyncPredictResponse from mindee.parsing.common.document import Document, serialize_for_json +from mindee.parsing.common.feedback_response import FeedbackResponse from mindee.parsing.common.inference import Inference, TypeInference from mindee.parsing.common.predict_response import PredictResponse +from mindee.parsing.common.string_dict import StringDict @dataclass @@ -142,21 +144,29 @@ class MindeeParser: input_doc: Union[LocalInputSource, UrlInputSource] """Document to be parsed.""" product_class: Type[Inference] - """Product to parse""" + """Product to parse.""" + feedback: Optional[StringDict] + """Dict representation of a feedback.""" def __init__( self, parser: Optional[ArgumentParser] = None, parsed_args: Optional[Namespace] = None, client: Optional[Client] = None, - input_doc: Optional[Union[LocalInputSource, UrlInputSource]] = None, document_info: Optional[CommandConfig] = None, ) -> None: self.parser = parser if parser else ArgumentParser(description="Mindee_API") self.parsed_args = parsed_args if parsed_args else self._set_args() - self.client = client if client else Client(api_key=self.parsed_args.api_key) - if self.parsed_args.parse_type == "parse": - self.input_doc = input_doc if input_doc else self._get_input_doc() + self.client = ( + client + if client + else Client( + api_key=self.parsed_args.api_key + if "api_key" in self.parsed_args + else None + ) + ) + self._set_input() self.document_info = ( document_info if document_info else DOCUMENTS[self.parsed_args.product_name] ) @@ -166,10 +176,10 @@ def call_endpoint(self) -> None: if self.parsed_args.parse_type == "parse": self.call_parse() else: - self.call_fetch() + self.call_feedback() - def call_fetch(self) -> None: - """Fetches an API's for a previously enqueued document.""" + def call_feedback(self) -> None: + """Sends feedback to an API.""" custom_endpoint: Optional[Endpoint] = None if self.parsed_args.product_name == "custom": custom_endpoint = self.client.create_endpoint( @@ -177,13 +187,16 @@ def call_fetch(self) -> None: self.parsed_args.account_name, self.parsed_args.api_version, ) - response: PredictResponse = self.client.get_document( - self.document_info.doc_class, self.parsed_args.document_id, custom_endpoint + if self.feedback is None: + raise RuntimeError("Invalid feedback provided.") + + response: FeedbackResponse = self.client.send_feedback( + self.document_info.doc_class, + self.parsed_args.document_id, + self.feedback, + custom_endpoint, ) - if self.parsed_args.output_type == "raw": - print(response.raw_http) - else: - print(response.document) + print(response.raw_http) def call_parse(self) -> None: """Calls an endpoint with the appropriate method, and displays the results.""" @@ -272,10 +285,10 @@ def _set_args(self) -> Namespace: parse_subparser = parse_product_subparsers.add_parser(name, help=info.help) call_parser = parse_subparser.add_subparsers( - dest="parse_type", + dest="parse_type", required=True ) parse_subp = call_parser.add_parser("parse") - fetch_subp = call_parser.add_parser("fetch") + feedback_subp = call_parser.add_parser("feedback") self._add_main_options(parse_subp) self._add_sending_options(parse_subp) @@ -302,9 +315,22 @@ def _set_args(self) -> Namespace: default=False, ) - self._add_main_options(fetch_subp) - self._add_display_options(fetch_subp) - self._add_doc_id_option(fetch_subp) + self._add_main_options(feedback_subp) + self._add_doc_id_option(feedback_subp) + self._add_feedback_options(feedback_subp) + feedback_subp.add_argument( + "-i", + "--input-type", + dest="input_type", + choices=["path", "file", "base64", "bytes", "local"], + default="local", + help="Specify how to handle the input.\n" + "- path: open a path (default).\n" + "- file: open as a file handle.\n" + "- base64: open a base64 encoded text file.\n" + "- bytes: open the contents as raw bytes.\n" + "- local: provide the feedback as a dict-like string.", + ) parsed_args = self.parser.parse_args() return parsed_args @@ -313,7 +339,7 @@ def _add_main_options(self, parser: ArgumentParser) -> None: """ Adds main options for most parsings. - :param parser: current parser/subparser. + :param parser: current parser. """ parser.add_argument( "-k", @@ -328,7 +354,7 @@ def _add_display_options(self, parser: ArgumentParser) -> None: """ Adds options related to output/display of a document (parse, parse-queued). - :param parser: current parser/subparser. + :param parser: current parser. """ parser.add_argument( "-o", @@ -346,7 +372,7 @@ def _add_sending_options(self, parser: ArgumentParser) -> None: """ Adds options for sending requests (parse, enqueue). - :param parser: current parser/subparser. + :param parser: current parser. """ parser.add_argument( "-i", @@ -382,18 +408,34 @@ def _add_doc_id_option(self, parser: ArgumentParser): """ Adds an option to provide the queue ID for an async document. - :param parser: current parser/subparser. + :param parser: current parser. """ parser.add_argument( dest="document_id", help="Async queue ID for a document (required)", + type=str, + ) + + def _add_feedback_options(self, parser: ArgumentParser): + """ + Adds the option to give feedback manually. + + :param parser: current parser. + """ + parser.add_argument( + "-f", + "--feedback", + dest="feedback", + required=False, + type=json.loads, + help="Feedback as a string", ) def _add_custom_options(self, parser: ArgumentParser): """ Adds options to custom-type documents. - :param parser: current parser/subparser. + :param parser: current parser. """ parser.add_argument( "-a", @@ -436,6 +478,39 @@ def _get_input_doc(self) -> Union[LocalInputSource, UrlInputSource]: return self.client.source_from_url(self.parsed_args.path) return self.client.source_from_path(self.parsed_args.path) + def _get_feedback_doc(self) -> StringDict: + """Loads a feedback.""" + json_doc: StringDict = {} + if self.parsed_args.input_type == "file": + with open(self.parsed_args.path, "rb", buffering=30) as f_f: + json_doc = json.loads(f_f.read()) + elif self.parsed_args.input_type == "base64": + with open(self.parsed_args.path, "rt", encoding="ascii") as f_b64: + json_doc = json.loads(f_b64.read()) + elif self.parsed_args.input_type == "bytes": + with open(self.parsed_args.path, "rb") as f_b: + json_doc = json.loads(f_b.read()) + else: + if ( + not self.parsed_args.feedback + or not "feedback" in self.parsed_args.feedback + ): + raise RuntimeError("Invalid feedback.") + if not json_doc or "feedback" not in json_doc: + raise RuntimeError("Invalid feedback.") + return json_doc + + def _set_input(self) -> None: + """Loads an input document, or a feedback document.""" + self.feedback = None + if self.parsed_args.parse_type == "feedback": + if not self.parsed_args.feedback: + self.feedback = self._get_feedback_doc() + else: + self.feedback = self.parsed_args.feedback + else: + self.input_doc = self._get_input_doc() + def main() -> None: """Run the Command Line Interface.""" diff --git a/mindee/client.py b/mindee/client.py index 6bd672d4..c59343fb 100644 --- a/mindee/client.py +++ b/mindee/client.py @@ -309,36 +309,6 @@ def send_feedback( return FeedbackResponse(feedback_response.json()) - def get_document( - self, - product_class: Type[Inference], - document_id: str, - endpoint: Optional[Endpoint] = None, - ) -> PredictResponse: - """ - Fetch prediction results from a document already processed. - - :param product_class: The document class to use. - The response object will be instantiated based on this parameter. - - :param document_id: The id of the document to send feedback to. - :param endpoint: For custom endpoints, an endpoint has to be given. - """ - if not document_id or len(document_id) == 0: - raise RuntimeError("Invalid document_id.") - if not endpoint: - endpoint = self._initialize_ots_endpoint(product_class) - - response = endpoint.document_req_get(document_id) - if not response.ok: - raise handle_error( - str(product_class.endpoint_name), - response.json(), - response.status_code, - ) - - return PredictResponse(product_class, response.json()) - def _make_request( self, product_class: Type[Inference], diff --git a/mindee/mindee_http/endpoint.py b/mindee/mindee_http/endpoint.py index d8ec69d3..5c0a3a5e 100644 --- a/mindee/mindee_http/endpoint.py +++ b/mindee/mindee_http/endpoint.py @@ -146,25 +146,6 @@ def document_feedback_req_put( timeout=self.settings.request_timeout, ) - def document_req_get(self, document_id: str) -> requests.Response: - """ - Make a request to GET annotations for a document. - - :param document_id: ID of the document - """ - params = { - "include_annotations": True, - "include_candidates": True, - "global_orientation": True, - } - response = requests.get( - f"{self.settings.url_root}/documents/{document_id}", - headers=self.settings.base_headers, - params=params, - timeout=self.settings.request_timeout, - ) - return response - class CustomEndpoint(Endpoint): """Endpoint for all custom documents.""" @@ -243,6 +224,25 @@ def documents_req_get(self, page_id: int = 1) -> requests.Response: ) return response + def document_req_get(self, document_id: str) -> requests.Response: + """ + Make a request to GET annotations for a document. + + :param document_id: ID of the document + """ + params = { + "include_annotations": True, + "include_candidates": True, + "global_orientation": True, + } + response = requests.get( + f"{self.settings.url_root}/documents/{document_id}", + headers=self.settings.base_headers, + params=params, + timeout=self.settings.request_timeout, + ) + return response + def annotations_req_post( self, document_id: str, annotations: dict ) -> requests.Response: diff --git a/tests/api/test_feedback_response.py b/tests/api/test_feedback_response.py new file mode 100644 index 00000000..fc93e7bc --- /dev/null +++ b/tests/api/test_feedback_response.py @@ -0,0 +1,12 @@ +import json +from mindee.parsing.common.feedback_response import FeedbackResponse + + +def test_empty_feedback_response(): + response = json.load( + open("./tests/data/products/invoices/feedback_response/empty.json") + ) + feedback_response = FeedbackResponse(response) + assert feedback_response is not None + assert feedback_response.feedback["customer_address"] is None + diff --git a/tests/api/test_response.py b/tests/api/test_response.py index 3675d580..58ca304b 100644 --- a/tests/api/test_response.py +++ b/tests/api/test_response.py @@ -1,12 +1,7 @@ import json -import pytest - -from mindee.input.sources import PathInput from mindee.parsing.common.predict_response import PredictResponse -from mindee.product import ( # FinancialDocumentV1,; InvoiceV3,; PassportV1,; ReceiptV3, - ReceiptV4, -) +from mindee.product import ReceiptV4 from mindee.product.financial_document.financial_document_v1 import FinancialDocumentV1 from mindee.product.financial_document.financial_document_v1_document import ( FinancialDocumentV1Document, diff --git a/tests/data b/tests/data index 64074190..7817573e 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 6407419004f94f7e3a39e20124ee65c94e561586 +Subproject commit 7817573e455275b41c37faab0c0749d6451cd091 diff --git a/tests/test_cli.py b/tests/test_cli.py index b093c8ac..6e0caff2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import json from argparse import Namespace from sys import api_version @@ -62,8 +63,9 @@ def ots_doc_enqueue_and_parse(monkeypatch): @pytest.fixture -def ots_doc_fetch(monkeypatch): +def ots_doc_feedback(monkeypatch): clear_envvars(monkeypatch) + dummy_feedback = '{"feedback": {"dummy_field": {"value": "dummy"}}}' return Namespace( api_key="dummy", output_type="summary", @@ -73,7 +75,10 @@ def ots_doc_fetch(monkeypatch): api_version="dummy", queue_id="dummy-queue-id", call_method="parse-queued", - parse_type="fetch", + input_type="path", + path="./tests/data/file_types/pdf/blank.pdf", + parse_type="feedback", + feedback=json.loads(dummy_feedback), ) @@ -155,13 +160,13 @@ def test_cli_invoice_splitter_enqueue(ots_doc_enqueue_and_parse): parser.call_endpoint() -def test_cli_fetch(ots_doc_fetch): - ots_doc_fetch.document_id = "dummy-document-id" - ots_doc_fetch.api_key = "" +def test_cli_feedback(ots_doc_feedback): + ots_doc_feedback.document_id = "dummy-document-id" + ots_doc_feedback.api_key = "" with pytest.raises(RuntimeError): - parser = MindeeParser(parsed_args=ots_doc_fetch) + parser = MindeeParser(parsed_args=ots_doc_feedback) parser.call_endpoint() - ots_doc_fetch.api_key = "dummy" + ots_doc_feedback.api_key = "dummy" with pytest.raises(MindeeHTTPClientException): - parser = MindeeParser(parsed_args=ots_doc_fetch) + parser = MindeeParser(parsed_args=ots_doc_feedback) parser.call_endpoint() diff --git a/tests/test_client.py b/tests/test_client.py index c95b911f..bdc4df8f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,11 +34,6 @@ def test_parse_path_without_token(empty_client: Client): empty_client.parse(product.ReceiptV4, input_doc) -def test_fetch_without_id(empty_client: Client): - with pytest.raises(RuntimeError): - empty_client.get_document(product.ReceiptV4, "") - - def test_feedback_without_id(empty_client: Client): with pytest.raises(RuntimeError): empty_client.send_feedback(product.ReceiptV4, "", {}) From f5281040e1fc19160e9af96a9496ac8f0174b2d1 Mon Sep 17 00:00:00 2001 From: sebastianMindee Date: Wed, 18 Oct 2023 19:02:30 +0200 Subject: [PATCH 13/14] fix lint --- tests/api/test_feedback_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_feedback_response.py b/tests/api/test_feedback_response.py index fc93e7bc..cbcef53b 100644 --- a/tests/api/test_feedback_response.py +++ b/tests/api/test_feedback_response.py @@ -1,4 +1,5 @@ import json + from mindee.parsing.common.feedback_response import FeedbackResponse @@ -9,4 +10,3 @@ def test_empty_feedback_response(): feedback_response = FeedbackResponse(response) assert feedback_response is not None assert feedback_response.feedback["customer_address"] is None - From 6efab79474d68b6ea6a97a1570fa8e79a5f6b793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ianar=C3=A9=20S=C3=A9vi?= Date: Thu, 19 Oct 2023 13:16:26 +0200 Subject: [PATCH 14/14] now feedback works --- mindee/cli.py | 36 ++++------------------ mindee/mindee_http/endpoint.py | 5 +-- mindee/mindee_http/mindee_api.py | 53 ++++++++++++++++---------------- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/mindee/cli.py b/mindee/cli.py index fa589c6a..6ba82d91 100644 --- a/mindee/cli.py +++ b/mindee/cli.py @@ -193,10 +193,10 @@ def call_feedback(self) -> None: response: FeedbackResponse = self.client.send_feedback( self.document_info.doc_class, self.parsed_args.document_id, - self.feedback, + {"feedback": self.feedback}, custom_endpoint, ) - print(response.raw_http) + print(json.dumps(response.feedback, indent=2)) def call_parse(self) -> None: """Calls an endpoint with the appropriate method, and displays the results.""" @@ -316,21 +316,7 @@ def _set_args(self) -> Namespace: ) self._add_main_options(feedback_subp) - self._add_doc_id_option(feedback_subp) self._add_feedback_options(feedback_subp) - feedback_subp.add_argument( - "-i", - "--input-type", - dest="input_type", - choices=["path", "file", "base64", "bytes", "local"], - default="local", - help="Specify how to handle the input.\n" - "- path: open a path (default).\n" - "- file: open as a file handle.\n" - "- base64: open a base64 encoded text file.\n" - "- bytes: open the contents as raw bytes.\n" - "- local: provide the feedback as a dict-like string.", - ) parsed_args = self.parser.parse_args() return parsed_args @@ -404,31 +390,21 @@ def _add_sending_options(self, parser: ArgumentParser) -> None: ) parser.add_argument(dest="path", help="Full path to the file") - def _add_doc_id_option(self, parser: ArgumentParser): + def _add_feedback_options(self, parser: ArgumentParser): """ - Adds an option to provide the queue ID for an async document. + Adds the option to give feedback manually. :param parser: current parser. """ parser.add_argument( dest="document_id", - help="Async queue ID for a document (required)", + help="Mindee UUID of the document.", type=str, ) - - def _add_feedback_options(self, parser: ArgumentParser): - """ - Adds the option to give feedback manually. - - :param parser: current parser. - """ parser.add_argument( - "-f", - "--feedback", dest="feedback", - required=False, type=json.loads, - help="Feedback as a string", + help='Feedback JSON string to send, ex \'{"key": "value"}\'.', ) def _add_custom_options(self, parser: ArgumentParser): diff --git a/mindee/mindee_http/endpoint.py b/mindee/mindee_http/endpoint.py index 5c0a3a5e..ef1a7ed5 100644 --- a/mindee/mindee_http/endpoint.py +++ b/mindee/mindee_http/endpoint.py @@ -1,3 +1,4 @@ +import json from typing import Union import requests @@ -140,9 +141,9 @@ def document_feedback_req_put( :param feedback: Feedback object to send. """ return requests.put( - f"{self.settings.url_root}/documents/{document_id}/feedback", + f"{self.settings.base_url}/documents/{document_id}/feedback", headers=self.settings.base_headers, - data=feedback, + data=json.dumps(feedback, indent=0), timeout=self.settings.request_timeout, ) diff --git a/mindee/mindee_http/mindee_api.py b/mindee/mindee_http/mindee_api.py index db69180c..7f6fbf4d 100644 --- a/mindee/mindee_http/mindee_api.py +++ b/mindee/mindee_http/mindee_api.py @@ -24,6 +24,33 @@ class MindeeApi: api_key: Optional[str] """API Key for the client.""" + base_url: str + request_timeout: int + + def __init__( + self, + api_key: Optional[str], + endpoint_name: str, + account_name: str, + version: str, + ): + self._set_api_key(api_key) + if not self.api_key or len(self.api_key) == 0: + raise RuntimeError( + ( + f"Missing API key for '{endpoint_name} v{version}' (belonging to {account_name})," + " check your Client configuration.\n" + "You can set this using the " + f"'{API_KEY_ENV_NAME}' environment variable." + ) + ) + self.endpoint_name = endpoint_name + self.account_name = account_name + self.version = version + self.request_timeout = TIMEOUT_DEFAULT + self.set_base_url(BASE_URL_DEFAULT) + self.set_from_env() + self.url_root = f"{self.base_url}/products/{self.account_name}/{self.endpoint_name}/v{self.version}" @property def base_headers(self) -> Dict[str, str]: @@ -61,29 +88,3 @@ def set_timeout(self, value: Union[str, int]) -> None: def set_base_url(self, value: str) -> None: """Set the base URL for all requests.""" self.base_url = value - - def __init__( - self, - api_key: Optional[str], - endpoint_name: str, - account_name: str, - version: str, - ): - self._set_api_key(api_key) - if not self.api_key or len(self.api_key) == 0: - raise RuntimeError( - ( - f"Missing API key for '{endpoint_name} v{version}' (belonging to {account_name})," - " check your Client configuration.\n" - "You can set this using the " - f"'{API_KEY_ENV_NAME}' environment variable." - ) - ) - self.endpoint_name = endpoint_name - self.account_name = account_name - self.version = version - self.request_timeout = TIMEOUT_DEFAULT - self.set_base_url(BASE_URL_DEFAULT) - self.set_from_env() - - self.url_root = f"{self.base_url}/products/{self.account_name}/{self.endpoint_name}/v{self.version}"