Skip to content

Commit 26c827c

Browse files
✨ add support for feedbacks (#177)
1 parent 5968338 commit 26c827c

File tree

15 files changed

+316
-146
lines changed

15 files changed

+316
-146
lines changed

docs/parsing/common.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ Job
5656
:members:
5757

5858

59+
Miscellaneous Parsing
60+
=====================
61+
62+
FeedbackResponse
63+
----------------
64+
.. autoclass:: mindee.parsing.common.feedback_response.FeedbackResponse
65+
:members:
66+
67+
5968
OCR Extraction
6069
==============
6170

mindee/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
from mindee.client import Client, PageOptions
33
from mindee.parsing.common.api_response import ApiResponse
44
from mindee.parsing.common.async_predict_response import AsyncPredictResponse, Job
5+
from mindee.parsing.common.feedback_response import FeedbackResponse
56
from mindee.parsing.common.predict_response import PredictResponse

mindee/cli.py

Lines changed: 119 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from mindee.input.sources import LocalInputSource, UrlInputSource
1010
from mindee.parsing.common.async_predict_response import AsyncPredictResponse
1111
from mindee.parsing.common.document import Document, serialize_for_json
12+
from mindee.parsing.common.feedback_response import FeedbackResponse
1213
from mindee.parsing.common.inference import Inference, TypeInference
1314
from mindee.parsing.common.predict_response import PredictResponse
15+
from mindee.parsing.common.string_dict import StringDict
1416

1517

1618
@dataclass
@@ -142,43 +144,59 @@ class MindeeParser:
142144
input_doc: Union[LocalInputSource, UrlInputSource]
143145
"""Document to be parsed."""
144146
product_class: Type[Inference]
145-
"""Product to parse"""
147+
"""Product to parse."""
148+
feedback: Optional[StringDict]
149+
"""Dict representation of a feedback."""
146150

147151
def __init__(
148152
self,
149153
parser: Optional[ArgumentParser] = None,
150154
parsed_args: Optional[Namespace] = None,
151155
client: Optional[Client] = None,
152-
input_doc: Optional[Union[LocalInputSource, UrlInputSource]] = None,
153156
document_info: Optional[CommandConfig] = None,
154157
) -> None:
155158
self.parser = parser if parser else ArgumentParser(description="Mindee_API")
156159
self.parsed_args = parsed_args if parsed_args else self._set_args()
157-
self.client = client if client else Client(api_key=self.parsed_args.api_key)
158-
# if self.parsed_args.parse_type == "parse":
159-
self.input_doc = input_doc if input_doc else self._get_input_doc()
160+
self.client = (
161+
client
162+
if client
163+
else Client(
164+
api_key=self.parsed_args.api_key
165+
if "api_key" in self.parsed_args
166+
else None
167+
)
168+
)
169+
self._set_input()
160170
self.document_info = (
161171
document_info if document_info else DOCUMENTS[self.parsed_args.product_name]
162172
)
163173

164174
def call_endpoint(self) -> None:
165175
"""Calls the proper type of endpoint according to given command."""
166-
# if self.parsed_args.parse_type == "parse":
167-
self.call_parse()
168-
169-
# else:
170-
# self.call_fetch()
171-
172-
# def call_fetch(self) -> None:
173-
# """Fetches an API's for a previously enqueued document."""
174-
# response: AsyncPredictResponse = self._parse_queued()
175-
# if self.parsed_args.output_type == "raw":
176-
# print(response.raw_http)
177-
# else:
178-
# if not hasattr(response, "document") or response.document is None:
179-
# print(response.job)
180-
# else:
181-
# print(response.document)
176+
if self.parsed_args.parse_type == "parse":
177+
self.call_parse()
178+
else:
179+
self.call_feedback()
180+
181+
def call_feedback(self) -> None:
182+
"""Sends feedback to an API."""
183+
custom_endpoint: Optional[Endpoint] = None
184+
if self.parsed_args.product_name == "custom":
185+
custom_endpoint = self.client.create_endpoint(
186+
self.parsed_args.endpoint_name,
187+
self.parsed_args.account_name,
188+
self.parsed_args.api_version,
189+
)
190+
if self.feedback is None:
191+
raise RuntimeError("Invalid feedback provided.")
192+
193+
response: FeedbackResponse = self.client.send_feedback(
194+
self.document_info.doc_class,
195+
self.parsed_args.document_id,
196+
{"feedback": self.feedback},
197+
custom_endpoint,
198+
)
199+
print(json.dumps(response.feedback, indent=2))
182200

183201
def call_parse(self) -> None:
184202
"""Calls an endpoint with the appropriate method, and displays the results."""
@@ -251,51 +269,27 @@ def _parse_async(self) -> AsyncPredictResponse:
251269
endpoint=custom_endpoint,
252270
)
253271

254-
# def _parse_queued(self) -> AsyncPredictResponse:
255-
# """Fetches a queue's result from a document's id."""
256-
# custom_endpoint: Optional[Endpoint] = None
257-
# if self.parsed_args.product_name == "custom":
258-
# self.client.create_endpoint(
259-
# self.parsed_args.endpoint_name,
260-
# self.parsed_args.account_name,
261-
# self.parsed_args.api_version,
262-
# )
263-
# return self.client.parse_queued(
264-
# self.document_info.doc_class,
265-
# self.parsed_args.queue_id,
266-
# custom_endpoint,
267-
# )
268-
269272
def _doc_str(self, output_type: str, doc_response: Document) -> str:
270273
if output_type == "parsed":
271274
return json.dumps(doc_response, indent=2, default=serialize_for_json)
272275
return str(doc_response)
273276

274277
def _set_args(self) -> Namespace:
275278
"""Parse command line arguments."""
276-
# call_parser = self.parser.add_subparsers(
277-
# dest="parse_type",
278-
# required=True,
279-
# )
280-
# parse_subparser = call_parser.add_parser("parse")
281-
# fetch_subparser = call_parser.add_parser("fetch")
282-
283-
# parse_product_subparsers = parse_subparser.add_subparsers(
284-
# dest="product_name",
285-
# required=True,
286-
# )
287279
parse_product_subparsers = self.parser.add_subparsers(
288280
dest="product_name",
289281
required=True,
290282
)
291283

292-
# fetch_product_subparsers = fetch_subparser.add_subparsers(
293-
# dest="product_name",
294-
# required=True,
295-
# )
296-
297284
for name, info in DOCUMENTS.items():
298-
parse_subp = parse_product_subparsers.add_parser(name, help=info.help)
285+
parse_subparser = parse_product_subparsers.add_parser(name, help=info.help)
286+
287+
call_parser = parse_subparser.add_subparsers(
288+
dest="parse_type", required=True
289+
)
290+
parse_subp = call_parser.add_parser("parse")
291+
feedback_subp = call_parser.add_parser("feedback")
292+
299293
self._add_main_options(parse_subp)
300294
self._add_sending_options(parse_subp)
301295
self._add_display_options(parse_subp)
@@ -321,16 +315,18 @@ def _set_args(self) -> Namespace:
321315
default=False,
322316
)
323317

324-
# if info.is_async:
325-
# fetch_subp = fetch_product_subparsers.add_parser(name, help=info.help)
326-
# self._add_main_options(fetch_subp)
327-
# self._add_display_options(fetch_subp)
328-
# self._add_fetch_options(fetch_subp)
318+
self._add_main_options(feedback_subp)
319+
self._add_feedback_options(feedback_subp)
329320

330321
parsed_args = self.parser.parse_args()
331322
return parsed_args
332323

333324
def _add_main_options(self, parser: ArgumentParser) -> None:
325+
"""
326+
Adds main options for most parsings.
327+
328+
:param parser: current parser.
329+
"""
334330
parser.add_argument(
335331
"-k",
336332
"--key",
@@ -341,7 +337,11 @@ def _add_main_options(self, parser: ArgumentParser) -> None:
341337
)
342338

343339
def _add_display_options(self, parser: ArgumentParser) -> None:
344-
"""Adds options related to output/display of a document (parse, parse-queued)."""
340+
"""
341+
Adds options related to output/display of a document (parse, parse-queued).
342+
343+
:param parser: current parser.
344+
"""
345345
parser.add_argument(
346346
"-o",
347347
"--output-type",
@@ -355,7 +355,11 @@ def _add_display_options(self, parser: ArgumentParser) -> None:
355355
)
356356

357357
def _add_sending_options(self, parser: ArgumentParser) -> None:
358-
"""Adds options for sending requests (parse, enqueue)."""
358+
"""
359+
Adds options for sending requests (parse, enqueue).
360+
361+
:param parser: current parser.
362+
"""
359363
parser.add_argument(
360364
"-i",
361365
"--input-type",
@@ -386,15 +390,29 @@ def _add_sending_options(self, parser: ArgumentParser) -> None:
386390
)
387391
parser.add_argument(dest="path", help="Full path to the file")
388392

389-
# def _add_fetch_options(self, parser: ArgumentParser):
390-
# """Adds an option to provide the queue ID for an async document."""
391-
# parser.add_argument(
392-
# dest="queue_id",
393-
# help="Async queue ID for a document (required)",
394-
# )
393+
def _add_feedback_options(self, parser: ArgumentParser):
394+
"""
395+
Adds the option to give feedback manually.
396+
397+
:param parser: current parser.
398+
"""
399+
parser.add_argument(
400+
dest="document_id",
401+
help="Mindee UUID of the document.",
402+
type=str,
403+
)
404+
parser.add_argument(
405+
dest="feedback",
406+
type=json.loads,
407+
help='Feedback JSON string to send, ex \'{"key": "value"}\'.',
408+
)
395409

396410
def _add_custom_options(self, parser: ArgumentParser):
397-
"""Adds options to custom-type documents."""
411+
"""
412+
Adds options to custom-type documents.
413+
414+
:param parser: current parser.
415+
"""
398416
parser.add_argument(
399417
"-a",
400418
"--account",
@@ -418,6 +436,7 @@ def _add_custom_options(self, parser: ArgumentParser):
418436
)
419437

420438
def _get_input_doc(self) -> Union[LocalInputSource, UrlInputSource]:
439+
"""Loads an input document."""
421440
if self.parsed_args.input_type == "file":
422441
with open(self.parsed_args.path, "rb", buffering=30) as file_handle:
423442
return self.client.source_from_file(file_handle)
@@ -435,6 +454,39 @@ def _get_input_doc(self) -> Union[LocalInputSource, UrlInputSource]:
435454
return self.client.source_from_url(self.parsed_args.path)
436455
return self.client.source_from_path(self.parsed_args.path)
437456

457+
def _get_feedback_doc(self) -> StringDict:
458+
"""Loads a feedback."""
459+
json_doc: StringDict = {}
460+
if self.parsed_args.input_type == "file":
461+
with open(self.parsed_args.path, "rb", buffering=30) as f_f:
462+
json_doc = json.loads(f_f.read())
463+
elif self.parsed_args.input_type == "base64":
464+
with open(self.parsed_args.path, "rt", encoding="ascii") as f_b64:
465+
json_doc = json.loads(f_b64.read())
466+
elif self.parsed_args.input_type == "bytes":
467+
with open(self.parsed_args.path, "rb") as f_b:
468+
json_doc = json.loads(f_b.read())
469+
else:
470+
if (
471+
not self.parsed_args.feedback
472+
or not "feedback" in self.parsed_args.feedback
473+
):
474+
raise RuntimeError("Invalid feedback.")
475+
if not json_doc or "feedback" not in json_doc:
476+
raise RuntimeError("Invalid feedback.")
477+
return json_doc
478+
479+
def _set_input(self) -> None:
480+
"""Loads an input document, or a feedback document."""
481+
self.feedback = None
482+
if self.parsed_args.parse_type == "feedback":
483+
if not self.parsed_args.feedback:
484+
self.feedback = self._get_feedback_doc()
485+
else:
486+
self.feedback = self.parsed_args.feedback
487+
else:
488+
self.input_doc = self._get_input_doc()
489+
438490

439491
def main() -> None:
440492
"""Run the Command Line Interface."""

mindee/client.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
from mindee.mindee_http.error import handle_error
1717
from mindee.mindee_http.mindee_api import MindeeApi
1818
from mindee.parsing.common.async_predict_response import AsyncPredictResponse
19+
from mindee.parsing.common.feedback_response import FeedbackResponse
1920
from mindee.parsing.common.inference import Inference, TypeInference
2021
from mindee.parsing.common.predict_response import PredictResponse
22+
from mindee.parsing.common.string_dict import StringDict
2123

2224
OTS_OWNER = "mindee"
2325

@@ -175,7 +177,7 @@ def parse_queued(
175177
176178
:param product_class: The document class to use.
177179
The response object will be instantiated based on this parameter.
178-
:param queue_id: queue_id received from the API
180+
:param queue_id: queue_id received from the API.
179181
:param endpoint: For custom endpoints, an endpoint has to be given.
180182
"""
181183
if not endpoint:
@@ -275,6 +277,38 @@ def enqueue_and_parse(
275277

276278
return poll_results
277279

280+
def send_feedback(
281+
self,
282+
product_class: Type[Inference],
283+
document_id: str,
284+
feedback: StringDict,
285+
endpoint: Optional[Endpoint] = None,
286+
) -> FeedbackResponse:
287+
"""
288+
Send a feedback for a document.
289+
290+
:param product_class: The document class to use.
291+
The response object will be instantiated based on this parameter.
292+
293+
:param document_id: The id of the document to send feedback to.
294+
:param feedback: Feedback to send.
295+
:param endpoint: For custom endpoints, an endpoint has to be given.
296+
"""
297+
if not document_id or len(document_id) == 0:
298+
raise RuntimeError("Invalid document_id.")
299+
if not endpoint:
300+
endpoint = self._initialize_ots_endpoint(product_class)
301+
302+
feedback_response = endpoint.document_feedback_req_put(document_id, feedback)
303+
if not feedback_response.ok:
304+
raise handle_error(
305+
str(product_class.endpoint_name),
306+
feedback_response.json(),
307+
feedback_response.status_code,
308+
)
309+
310+
return FeedbackResponse(feedback_response.json())
311+
278312
def _make_request(
279313
self,
280314
product_class: Type[Inference],
@@ -360,7 +394,7 @@ def _get_queued_document(
360394

361395
return AsyncPredictResponse(product_class, queue_response.json())
362396

363-
def _initialize_ots_endpoint(self, product_class) -> Endpoint:
397+
def _initialize_ots_endpoint(self, product_class: Type[Inference]) -> Endpoint:
364398
if product_class.__name__ == "CustomV1":
365399
raise TypeError("Missing endpoint specifications for custom build.")
366400
endpoint_info: Dict[str, str] = product_class.get_endpoint_info(product_class)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from abc import ABC
2+
3+
from mindee.mindee_http.mindee_api import MindeeApi
4+
5+
6+
class BaseEndpoint(ABC):
7+
"""Base endpoint for the Mindee API."""
8+
9+
def __init__(self, settings: MindeeApi) -> None:
10+
"""
11+
Base API endpoint class for all endpoints.
12+
13+
:param settings: Settings relating to all endpoints
14+
"""
15+
self.settings = settings

0 commit comments

Comments
 (0)