indico_toolkit 6.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. indico_toolkit/__init__.py +24 -0
  2. indico_toolkit/association/__init__.py +15 -0
  3. indico_toolkit/association/association.py +65 -0
  4. indico_toolkit/association/extracted_tokens.py +106 -0
  5. indico_toolkit/association/line_items.py +189 -0
  6. indico_toolkit/association/positioning.py +301 -0
  7. indico_toolkit/association/splitting.py +41 -0
  8. indico_toolkit/auto_populate/__init__.py +3 -0
  9. indico_toolkit/auto_populate/populator.py +362 -0
  10. indico_toolkit/auto_populate/types.py +66 -0
  11. indico_toolkit/auto_review/__init__.py +6 -0
  12. indico_toolkit/auto_review/auto_review_functions.py +123 -0
  13. indico_toolkit/auto_review/auto_reviewer.py +62 -0
  14. indico_toolkit/client.py +35 -0
  15. indico_toolkit/errors.py +32 -0
  16. indico_toolkit/etloutput/__init__.py +210 -0
  17. indico_toolkit/etloutput/cell.py +47 -0
  18. indico_toolkit/etloutput/errors.py +16 -0
  19. indico_toolkit/etloutput/etloutput.py +110 -0
  20. indico_toolkit/etloutput/range.py +30 -0
  21. indico_toolkit/etloutput/table.py +44 -0
  22. indico_toolkit/etloutput/token.py +25 -0
  23. indico_toolkit/indico_wrapper/__init__.py +15 -0
  24. indico_toolkit/indico_wrapper/dataset.py +114 -0
  25. indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
  26. indico_toolkit/indico_wrapper/download.py +193 -0
  27. indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
  28. indico_toolkit/indico_wrapper/modelop.py +200 -0
  29. indico_toolkit/indico_wrapper/reviewer.py +111 -0
  30. indico_toolkit/indico_wrapper/workflow.py +231 -0
  31. indico_toolkit/metrics/__init__.py +11 -0
  32. indico_toolkit/metrics/compare_ground_truth.py +123 -0
  33. indico_toolkit/metrics/compare_models.py +180 -0
  34. indico_toolkit/metrics/metrics.py +373 -0
  35. indico_toolkit/metrics/plotting.py +102 -0
  36. indico_toolkit/ocr/__init__.py +9 -0
  37. indico_toolkit/ocr/customocr_object.py +42 -0
  38. indico_toolkit/ocr/ondoc_object.py +93 -0
  39. indico_toolkit/ocr/standard_object.py +54 -0
  40. indico_toolkit/pipelines/__init__.py +3 -0
  41. indico_toolkit/pipelines/file_processing.py +157 -0
  42. indico_toolkit/polling/__init__.py +8 -0
  43. indico_toolkit/polling/autoreview.py +204 -0
  44. indico_toolkit/polling/downstream.py +145 -0
  45. indico_toolkit/polling/queries.py +68 -0
  46. indico_toolkit/py.typed +0 -0
  47. indico_toolkit/results/__init__.py +104 -0
  48. indico_toolkit/results/document.py +78 -0
  49. indico_toolkit/results/errors.py +4 -0
  50. indico_toolkit/results/model.py +60 -0
  51. indico_toolkit/results/normalization.py +159 -0
  52. indico_toolkit/results/predictionlist.py +395 -0
  53. indico_toolkit/results/predictions/__init__.py +88 -0
  54. indico_toolkit/results/predictions/box.py +58 -0
  55. indico_toolkit/results/predictions/citation.py +47 -0
  56. indico_toolkit/results/predictions/classification.py +50 -0
  57. indico_toolkit/results/predictions/documentextraction.py +160 -0
  58. indico_toolkit/results/predictions/extraction.py +35 -0
  59. indico_toolkit/results/predictions/formextraction.py +121 -0
  60. indico_toolkit/results/predictions/group.py +29 -0
  61. indico_toolkit/results/predictions/prediction.py +41 -0
  62. indico_toolkit/results/predictions/span.py +43 -0
  63. indico_toolkit/results/predictions/summarization.py +103 -0
  64. indico_toolkit/results/predictions/unbundling.py +51 -0
  65. indico_toolkit/results/result.py +152 -0
  66. indico_toolkit/results/review.py +32 -0
  67. indico_toolkit/results/utilities.py +73 -0
  68. indico_toolkit/retry.py +94 -0
  69. indico_toolkit/snapshots/__init__.py +3 -0
  70. indico_toolkit/snapshots/snapshot.py +340 -0
  71. indico_toolkit/structure/__init__.py +3 -0
  72. indico_toolkit/structure/create_structure.py +232 -0
  73. indico_toolkit/structure/queries.py +119 -0
  74. indico_toolkit/structure/utils.py +16 -0
  75. indico_toolkit/types/__init__.py +12 -0
  76. indico_toolkit/types/classification.py +92 -0
  77. indico_toolkit/types/extractions.py +227 -0
  78. indico_toolkit/types/predictions.py +28 -0
  79. indico_toolkit/types/workflow_object.py +133 -0
  80. indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
  81. indico_toolkit-6.14.0.dist-info/METADATA +144 -0
  82. indico_toolkit-6.14.0.dist-info/RECORD +83 -0
  83. indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,24 @@
1
+ """Classes, functions, and abstractions for Indico IPA"""
2
+
3
+ from .client import create_client
4
+ from .errors import (
5
+ ToolkitAuthError,
6
+ ToolkitError,
7
+ ToolkitInputError,
8
+ ToolkitInstantiationError,
9
+ ToolkitPopulationError,
10
+ ToolkitStaggeredLoopError,
11
+ ToolkitStatusError,
12
+ )
13
+
14
+ __all__ = (
15
+ "create_client",
16
+ "ToolkitAuthError",
17
+ "ToolkitError",
18
+ "ToolkitInputError",
19
+ "ToolkitInstantiationError",
20
+ "ToolkitPopulationError",
21
+ "ToolkitStaggeredLoopError",
22
+ "ToolkitStatusError",
23
+ )
24
+ __version__ = "6.14.0"
@@ -0,0 +1,15 @@
1
+ from .association import Association, sequences_exact, sequences_overlap
2
+ from .extracted_tokens import ExtractedTokens
3
+ from .line_items import LineItems
4
+ from .positioning import Positioning
5
+ from .splitting import split_prediction
6
+
7
+ __all__ = (
8
+ "Association",
9
+ "ExtractedTokens",
10
+ "LineItems",
11
+ "Positioning",
12
+ "sequences_exact",
13
+ "sequences_overlap",
14
+ "split_prediction",
15
+ )
@@ -0,0 +1,65 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections import defaultdict
3
+ from typing import Dict, List, Union
4
+
5
+ from ..types import Extractions
6
+
7
+
8
+ class Association(ABC):
9
+ """
10
+ Base class for matching tokens to extraction predictions.
11
+ """
12
+
13
+ def __init__(self, predictions: Union[List[dict], Extractions]):
14
+ self._predictions = self.validate_prediction_formatting(predictions)
15
+ self._mapped_positions = []
16
+ self._manually_added_preds = []
17
+ self._errored_predictions = []
18
+
19
+ @abstractmethod
20
+ def match_pred_to_token(self):
21
+ pass
22
+
23
+ @staticmethod
24
+ def sort_predictions_by_start_index(predictions: List[dict]) -> List[dict]:
25
+ return sorted(predictions, key=lambda x: x["start"])
26
+
27
+ @property
28
+ def mapped_positions_by_page(self) -> Dict[int, List[dict]]:
29
+ """
30
+ Return mapped positions by page on which they first appear
31
+ """
32
+ page_map = defaultdict(list)
33
+ for position in self._mapped_positions:
34
+ page_map[position["page_num"]].append(position)
35
+ return page_map
36
+
37
+ def validate_prediction_formatting(
38
+ self, predictions: Union[List[dict], Extractions]
39
+ ) -> List[dict]:
40
+ if isinstance(predictions, Extractions):
41
+ predictions = predictions.to_list()
42
+ return predictions
43
+
44
+ def _is_manually_added_pred(self, prediction: dict) -> bool:
45
+ return Extractions.is_manually_added_prediction(prediction)
46
+
47
+
48
+ def sequences_overlap(x: dict, y: dict) -> bool:
49
+ """
50
+ Boolean return value indicates whether or not seqs overlap
51
+ """
52
+ return x["start"] < y["end"] and y["start"] < x["end"]
53
+
54
+
55
+ def sequences_exact(x: dict, y: dict) -> bool:
56
+ """
57
+ Boolean return value indicates whether or not seqs are exact
58
+ """
59
+ return x["start"] == y["start"] and x["end"] == y["end"]
60
+
61
+
62
+ def _check_if_token_match_found(pred: dict, no_match_indicator: bool):
63
+ if no_match_indicator:
64
+ pred["error"] = "No matching token found for extraction"
65
+ raise ValueError(f"Couldn't match a token to this prediction:\n{pred}")
@@ -0,0 +1,106 @@
1
+ from typing import List, Union
2
+
3
+ from ..types import Extractions
4
+ from .association import Association, _check_if_token_match_found, sequences_overlap
5
+
6
+ # TODO: add test that 'errored_predictions' actually works with ValueErrors
7
+
8
+
9
+ class ExtractedTokens(Association):
10
+ """
11
+ Class to collect all extracted tokens, e.g. to enable highlighting of source
12
+ document
13
+ """
14
+
15
+ def __init__(self, predictions: Union[List[dict], Extractions]):
16
+ super().__init__(predictions)
17
+
18
+ def match_pred_to_token(self, pred: dict, ocr_tokens: List[dict], pred_index: int):
19
+ """
20
+ Append matching token positions to self.mapped_positions, if no matches for
21
+ pred, raise ValueError
22
+
23
+ Args:
24
+ pred (dict): Indico extraction model prediction
25
+ ocr_tokens (List[dict]): List of OCR tokens
26
+ pred_index (int): unique number for each prediction so that tokens can be
27
+ linked to it
28
+
29
+ Raises:
30
+ ValueError: No matching token was found
31
+
32
+ Returns:
33
+ [int]: index in ocr tokens where prediction matched
34
+ """
35
+ no_match = True
36
+ match_token_index = 0
37
+ pred["prediction_index"] = pred_index
38
+ for ind, token in enumerate(ocr_tokens):
39
+ if sequences_overlap(token["doc_offset"], pred):
40
+ self._add_pred_meta_to_token(token, pred, pred_index)
41
+ self._remove_unneeded_token_keys(token)
42
+ self._mapped_positions.append(token)
43
+ match_token_index = ind
44
+ no_match = False
45
+ elif token["doc_offset"]["start"] > pred["end"]:
46
+ break
47
+ _check_if_token_match_found(pred, no_match)
48
+ return match_token_index
49
+
50
+ def collect_tokens(self, ocr_tokens: List[dict], raise_for_no_match: bool = True):
51
+ """
52
+ Collect all extracted tokens and with pred text and label added to dictionaries
53
+ Args:
54
+ ocr_tokens (List[dict]): Tokens from 'ondocument' OCR config output
55
+ raise_for_no_match (bool): raise exception if a matching token isn't found for a
56
+ prediction
57
+ """
58
+ self._separate_manually_added_predictions()
59
+ self._predictions = self.sort_predictions_by_start_index(self._predictions)
60
+ match_index = 0
61
+ for pred_ind, pred in enumerate(self._predictions):
62
+ try:
63
+ match_index = self.match_pred_to_token(
64
+ pred, ocr_tokens[match_index:], pred_ind
65
+ )
66
+ except ValueError as e:
67
+ if raise_for_no_match:
68
+ raise ValueError(e)
69
+ else:
70
+ print(f"Ignoring Error: {e}")
71
+ self._errored_predictions.append(pred)
72
+
73
+ @property
74
+ def extracted_tokens(self) -> List[dict]:
75
+ return self._mapped_positions
76
+
77
+ @property
78
+ def predictions(self) -> Extractions:
79
+ return Extractions(
80
+ self._predictions + self._errored_predictions + self._manually_added_preds
81
+ )
82
+
83
+ @property
84
+ def unmapped_predictions(self) -> Extractions:
85
+ return Extractions(self._errored_predictions + self._manually_added_preds)
86
+
87
+ def _remove_unneeded_token_keys(
88
+ self, token: dict, keys: List[str] = ["style", "block_offset", "page_offset"]
89
+ ):
90
+ for key in keys:
91
+ token.pop(key, None)
92
+
93
+ def _separate_manually_added_predictions(self):
94
+ predictions = []
95
+ for pred in self._predictions:
96
+ if self._is_manually_added_pred(pred):
97
+ pred["error"] = "Can't match tokens for manually added prediction"
98
+ self._manually_added_preds.append(pred)
99
+ else:
100
+ predictions.append(pred)
101
+ self._predictions = predictions
102
+
103
+ def _add_pred_meta_to_token(self, token: dict, pred: dict, pred_index: int):
104
+ token["label"] = pred["label"]
105
+ token["text"] = pred["text"]
106
+ token["prediction_index"] = pred_index
@@ -0,0 +1,189 @@
1
+ """Associate row items"""
2
+
3
+ from collections import defaultdict
4
+ from copy import deepcopy
5
+ from typing import Iterable, List, Union
6
+
7
+ from ..types import Extractions
8
+ from .association import Association, _check_if_token_match_found, sequences_overlap
9
+
10
+
11
+ class LineItems(Association):
12
+ """
13
+ Class for associating line items given extraction predictions and ondocument OCR
14
+ tokens
15
+
16
+ Example Usage:
17
+
18
+ litems = LineItems(
19
+ predictions=[{"label": "line_date", "start": 12, "text": "1/2/2021".....}],
20
+ line_item_fields=["line_value", "line_date"],
21
+ )
22
+ litems.get_bounding_boxes(ocr_tokens=[{"postion"...,},])
23
+ litems.assign_row_number()
24
+
25
+ # Get your updated predictions
26
+ print(litems.updated_predictions)
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ predictions: Union[List[dict], Extractions],
32
+ line_item_fields: Iterable[str],
33
+ ):
34
+ """
35
+ Args:
36
+ predictions (List[dict]): List of extraction predictions
37
+ line_item_fields (Iterable[str]): Fields/labels to include as line item values,
38
+ other values will not be assigned a row_number.
39
+ """
40
+ self.predictions = self.validate_prediction_formatting(predictions)
41
+ self.line_item_fields: Iterable[str] = line_item_fields
42
+ self._mapped_positions: List[dict] = []
43
+ self._unmapped_positions: List[dict] = []
44
+ self._errored_predictions: List[dict] = []
45
+
46
+ @property
47
+ def updated_predictions(self) -> Extractions:
48
+ return Extractions(
49
+ self._mapped_positions
50
+ + self._unmapped_positions
51
+ + self._errored_predictions
52
+ )
53
+
54
+ @staticmethod
55
+ def match_pred_to_token(pred: dict, ocr_tokens: List[dict]):
56
+ """
57
+ Match and add bounding box metadata to prediction.
58
+
59
+ Args:
60
+ pred (dict): Indico extraction model prediction
61
+ ocr_tokens (List[dict]): List of OCR tokens
62
+
63
+ Raises:
64
+ ValueError: No matching token was found
65
+
66
+ Returns:
67
+ [int]: index in ocr tokens where prediction matched
68
+ """
69
+ no_match = True
70
+ match_token_index = 0
71
+ for ind, token in enumerate(ocr_tokens):
72
+ if no_match and sequences_overlap(token["doc_offset"], pred):
73
+ _add_bounding_metadata_to_pred(pred, token)
74
+ no_match = False
75
+ match_token_index = ind
76
+ elif sequences_overlap(token["doc_offset"], pred):
77
+ _update_bounding_metadata_for_pred(pred, token)
78
+ elif token["doc_offset"]["start"] > pred["end"]:
79
+ break
80
+ _check_if_token_match_found(pred, no_match)
81
+ return match_token_index
82
+
83
+ def get_bounding_boxes(
84
+ self, ocr_tokens: List[dict], raise_for_no_match: bool = True
85
+ ):
86
+ """
87
+ Adds keys for bounding box top/bottom/left/right and page number to line item
88
+ predictions
89
+
90
+ Args:
91
+ ocr_tokens (List[dict]): Tokens from 'ondocument' OCR config output
92
+ raise_for_no_match (bool): raise exception if a matching token isn't found for a
93
+ prediction
94
+ """
95
+ predictions = deepcopy(self.predictions)
96
+ predictions = self._remove_unneeded_predictions(predictions)
97
+ predictions = self.sort_predictions_by_start_index(predictions)
98
+ match_index = 0
99
+ for pred in predictions:
100
+ try:
101
+ match_index = self.match_pred_to_token(pred, ocr_tokens[match_index:])
102
+ self._mapped_positions.append(pred)
103
+ except ValueError as e:
104
+ if raise_for_no_match:
105
+ raise e
106
+ else:
107
+ print(f"Ignoring Error: {e}")
108
+ self._errored_predictions.append(pred)
109
+
110
+ def assign_row_number(self):
111
+ """
112
+ Adds a row_number:int key/val pair based on bounding box position and page
113
+
114
+ Updates:
115
+ self._mapped_positions (list of dicts): predictions with row_number added
116
+ """
117
+ self._mapped_positions = sorted(
118
+ self._mapped_positions,
119
+ key=lambda x: (x["page_num"], x["bbTop"], x["bbLeft"]),
120
+ )
121
+ starting_pred = self._get_first_valid_line_item_pred()
122
+ max_bot = starting_pred["bbBot"]
123
+ page_number = starting_pred["page_num"]
124
+ row_number = 1
125
+ for pred in self._mapped_positions:
126
+ # if the top of one box equals the bottom of another, we still want a new
127
+ # line
128
+ if pred["bbTop"] >= max_bot or pred["page_num"] != page_number:
129
+ row_number += 1
130
+ page_number = pred["page_num"]
131
+ max_bot = pred["bbBot"]
132
+ else:
133
+ max_bot = max(pred["bbBot"], max_bot)
134
+ pred["row_number"] = row_number
135
+
136
+ @property
137
+ def grouped_line_items(self) -> List[List[dict]]:
138
+ """
139
+ After row number has been assigned to predictions, returns line item predictions
140
+ as a list of lists where each list is a row.
141
+ """
142
+ rows = defaultdict(list)
143
+ for pred in self._mapped_positions:
144
+ rows[pred["row_number"]].append(pred)
145
+ return list(rows.values())
146
+
147
+ def _remove_unneeded_predictions(self, predictions: List[dict]) -> List[dict]:
148
+ """
149
+ Remove predictions that are not line item fields or don't have valid start/end
150
+ index data
151
+ """
152
+ valid_line_item_preds = []
153
+ for pred in predictions:
154
+ if not self.is_line_item_pred(pred):
155
+ self._unmapped_positions.append(pred)
156
+ elif self._is_manually_added_pred(pred):
157
+ pred["error"] = "Can't match tokens for manually added prediction"
158
+ self._errored_predictions.append(pred)
159
+ else:
160
+ valid_line_item_preds.append(pred)
161
+ return valid_line_item_preds
162
+
163
+ def is_line_item_pred(self, pred: dict):
164
+ if pred["label"] in self.line_item_fields:
165
+ return True
166
+ return False
167
+
168
+ def _get_first_valid_line_item_pred(self) -> dict:
169
+ if len(self._mapped_positions) == 0:
170
+ raise RuntimeError(
171
+ "Whoops! You have no line_item_fields predictions. "
172
+ "Did you run get_bounding_boxes?"
173
+ )
174
+ return self._mapped_positions[0]
175
+
176
+
177
+ def _add_bounding_metadata_to_pred(pred: dict, token: dict):
178
+ pred["bbTop"] = token["position"]["bbTop"]
179
+ pred["bbBot"] = token["position"]["bbBot"]
180
+ pred["bbLeft"] = token["position"]["bbLeft"]
181
+ pred["bbRight"] = token["position"]["bbRight"]
182
+ pred["page_num"] = token["page_num"]
183
+
184
+
185
+ def _update_bounding_metadata_for_pred(pred: dict, token: dict):
186
+ pred["bbTop"] = min(token["position"]["bbTop"], pred["bbTop"])
187
+ pred["bbBot"] = max(token["position"]["bbBot"], pred["bbBot"])
188
+ pred["bbLeft"] = min(token["position"]["bbLeft"], pred["bbLeft"])
189
+ pred["bbRight"] = max(token["position"]["bbRight"], pred["bbRight"])