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,78 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .utilities import get
4
+
5
+
6
+ @dataclass(frozen=True, order=True)
7
+ class Document:
8
+ id: int
9
+ name: str
10
+ etl_output_uri: str
11
+ failed: bool
12
+ error: str
13
+ traceback: str
14
+
15
+ # Auto review changes must reproduce all model sections that were present in the
16
+ # original result file. This may not be possible from the predictions alone--if a
17
+ # model had an empty section because it didn't produce predictions or if all of
18
+ # the predictions were removed to reject them. As such, the models seen when
19
+ # parsing result files are tracked per-document so that the empty sections can be
20
+ # reproduced later.
21
+ _model_sections: "frozenset[str]"
22
+
23
+ @staticmethod
24
+ def from_v1_dict(result: object) -> "Document":
25
+ """
26
+ Create a `Document` from a v1 result file dictionary.
27
+ """
28
+ document_results = get(result, dict, "results", "document", "results")
29
+ model_names = frozenset(document_results.keys())
30
+ etl_output_uri = get(result, str, "etl_output")
31
+
32
+ return Document(
33
+ # v1 result files don't include document IDs or filenames.
34
+ id=None, # type: ignore[arg-type]
35
+ name=None, # type: ignore[arg-type]
36
+ etl_output_uri=etl_output_uri,
37
+ failed=False,
38
+ error="",
39
+ traceback="",
40
+ _model_sections=model_names,
41
+ )
42
+
43
+ @staticmethod
44
+ def from_v3_dict(document: object) -> "Document":
45
+ """
46
+ Create a `Document` from a v3 document dictionary.
47
+ """
48
+ model_results = get(document, dict, "model_results", "ORIGINAL")
49
+ model_ids = frozenset(model_results.keys())
50
+ etl_output_uri = get(document, str, "etl_output")
51
+
52
+ return Document(
53
+ id=get(document, int, "submissionfile_id"),
54
+ name=get(document, str, "input_filename"),
55
+ etl_output_uri=etl_output_uri,
56
+ failed=False,
57
+ error="",
58
+ traceback="",
59
+ _model_sections=model_ids,
60
+ )
61
+
62
+ @staticmethod
63
+ def from_v3_errored_file(errored_file: object) -> "Document":
64
+ """
65
+ Create a `Document` from a v3 errored file dictionary.
66
+ """
67
+ traceback = get(errored_file, str, "error")
68
+ error = traceback.split("\n")[-1].strip()
69
+
70
+ return Document(
71
+ id=get(errored_file, int, "submissionfile_id"),
72
+ name=get(errored_file, str, "input_filename"),
73
+ etl_output_uri="",
74
+ failed=True,
75
+ error=error,
76
+ traceback=traceback,
77
+ _model_sections=frozenset(),
78
+ )
@@ -0,0 +1,4 @@
1
+ class ResultError(Exception):
2
+ """
3
+ Raised when an error occurs while loading or dumping a result file.
4
+ """
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from .utilities import get, has
5
+
6
+
7
+ class ModelGroupType(Enum):
8
+ CLASSIFICATION = "classification"
9
+ DOCUMENT_EXTRACTION = "annotation"
10
+ FORM_EXTRACTION = "form_extraction"
11
+ GENAI_CLASSIFICATION = "genai_classification"
12
+ GENAI_EXTRACTION = "genai_annotation"
13
+ GENAI_SUMMARIZATION = "summarization"
14
+ UNBUNDLING = "classification_unbundling"
15
+
16
+
17
+ @dataclass(frozen=True, order=True)
18
+ class ModelGroup:
19
+ id: int
20
+ name: str
21
+ type: ModelGroupType
22
+
23
+ @staticmethod
24
+ def from_v1_section(section: "tuple[str, object]") -> "ModelGroup":
25
+ """
26
+ Create a `ModelGroup` from a v1 prediction section.
27
+ Use a heuristic on the first prediction of the model to determine its type.
28
+ """
29
+ name, predictions = section
30
+
31
+ if has(predictions, dict, "pre_review", 0):
32
+ prediction = get(predictions, dict, "pre_review", 0)
33
+
34
+ if has(prediction, str, "type"):
35
+ type = ModelGroupType.FORM_EXTRACTION
36
+ elif has(prediction, str, "text"):
37
+ type = ModelGroupType.DOCUMENT_EXTRACTION
38
+ else:
39
+ type = ModelGroupType.CLASSIFICATION
40
+ else:
41
+ # Likely an extraction model that produced no predictions.
42
+ type = ModelGroupType.DOCUMENT_EXTRACTION
43
+
44
+ return ModelGroup(
45
+ # v1 result files don't include model IDs.
46
+ id=None, # type: ignore[arg-type]
47
+ name=name,
48
+ type=type,
49
+ )
50
+
51
+ @staticmethod
52
+ def from_v3_dict(model_group: object) -> "ModelGroup":
53
+ """
54
+ Create a `ModelGroup` from a v3 model group dictionary.
55
+ """
56
+ return ModelGroup(
57
+ id=get(model_group, int, "id"),
58
+ name=get(model_group, str, "name"),
59
+ type=ModelGroupType(get(model_group, str, "task_type")),
60
+ )
@@ -0,0 +1,159 @@
1
+ import re
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .utilities import get, has
5
+
6
+ if TYPE_CHECKING:
7
+ from collections.abc import Iterator
8
+ from typing import Any
9
+
10
+
11
+ def normalize_v1_result(result: "Any") -> None:
12
+ """
13
+ Fix inconsistencies observed in v1 result files.
14
+ """
15
+ submission_results = get(result, dict, "results", "document", "results")
16
+
17
+ for model_name, model_results in tuple(submission_results.items()):
18
+ # Incomplete and unreviewed submissions don't have review sections.
19
+ if not has(model_results, list, "post_reviews"):
20
+ submission_results[model_name] = model_results = {
21
+ "pre_review": model_results,
22
+ "post_reviews": [],
23
+ }
24
+
25
+ # Classifications aren't wrapped in lists like other prediction types.
26
+ if has(model_results, dict, "pre_review"):
27
+ model_results["pre_review"] = [model_results["pre_review"]]
28
+ model_results["post_reviews"] = [
29
+ [prediction] for prediction in model_results["post_reviews"]
30
+ ]
31
+
32
+ predictions: "Any" = (
33
+ prediction
34
+ for review in (
35
+ [get(model_results, list, "pre_review")]
36
+ + get(model_results, list, "post_reviews")
37
+ )
38
+ if review is not None
39
+ for prediction in review
40
+ if prediction is not None
41
+ )
42
+
43
+ for prediction in predictions:
44
+ # Predictions added in review lack a `confidence` section.
45
+ if "confidence" not in prediction:
46
+ prediction["confidence"] = {prediction["label"]: 0}
47
+
48
+ # Form Extractions added in review may lack bounding boxes.
49
+ # Set values that will equal `NULL_BOX`.
50
+ if "type" in prediction and "top" not in prediction:
51
+ prediction["page_num"] = 0
52
+ prediction["top"] = 0
53
+ prediction["left"] = 0
54
+ prediction["right"] = 0
55
+ prediction["bottom"] = 0
56
+
57
+ # Prior to 6.11, some Extractions lack a `normalized` section after review.
58
+ if "text" in prediction and "normalized" not in prediction:
59
+ prediction["normalized"] = {"formatted": prediction["text"]}
60
+
61
+ # Document Extractions that didn't go through a linked labels transformer
62
+ # lack a `groupings` section.
63
+ if (
64
+ "text" in prediction
65
+ and "type" not in prediction
66
+ and "groupings" not in prediction
67
+ ):
68
+ prediction["groupings"] = []
69
+
70
+ # Incomplete and unreviewed submissions don't include a `reviews_meta` section.
71
+ if not has(result, list, "reviews_meta"):
72
+ result["reviews_meta"] = []
73
+
74
+ # Incomplete and unreviewed submissions retrieved with `SubmissionResult()` have a
75
+ # single `{"review_id": None}` review.
76
+ if not has(result, int, "reviews_meta", 0, "review_id"):
77
+ result["reviews_meta"] = []
78
+
79
+ # Review notes are `None` unless the reviewer enters a reason for rejection.
80
+ for review_dict in get(result, list, "reviews_meta"):
81
+ if not has(review_dict, str, "review_notes"):
82
+ review_dict["review_notes"] = ""
83
+
84
+
85
+ def normalize_v3_result(result: "Any") -> None:
86
+ """
87
+ Fix inconsistencies observed in v3 result files.
88
+ """
89
+ task_type_by_model_group_id = {
90
+ model_group_id: model_group["task_type"]
91
+ for model_group_id, model_group in result["modelgroup_metadata"].items()
92
+ }
93
+ predictions_with_task_type: "Iterator[tuple[Any, str]]" = (
94
+ (prediction, task_type_by_model_group_id[model_group_id])
95
+ for submission_result in get(result, list, "submission_results")
96
+ for review_result in get(submission_result, dict, "model_results").values()
97
+ for model_group_id, model_results in review_result.items()
98
+ for prediction in model_results
99
+ )
100
+
101
+ for prediction, task_type in predictions_with_task_type:
102
+ # Predictions added in review may lack a `confidence` section.
103
+ if "confidence" not in prediction:
104
+ prediction["confidence"] = {prediction["label"]: 0}
105
+
106
+ # Document Extractions added in review may lack spans.
107
+ if (
108
+ task_type in ("annotation", "genai_annotation")
109
+ and "spans" not in prediction
110
+ ):
111
+ prediction["spans"] = []
112
+
113
+ # Form Extractions added in review may lack bounding boxes.
114
+ # Set values that will equal `NULL_BOX`.
115
+ if task_type == "form_extraction" and "top" not in prediction:
116
+ prediction["page_num"] = 0
117
+ prediction["top"] = 0
118
+ prediction["left"] = 0
119
+ prediction["right"] = 0
120
+ prediction["bottom"] = 0
121
+
122
+ # Prior to 6.11, some Extractions lack a `normalized` section after
123
+ # review.
124
+ if (
125
+ task_type in ("annotation", "form_extraction", "genai_annotation")
126
+ and "normalized" not in prediction
127
+ ):
128
+ prediction["normalized"] = {"formatted": prediction["text"]}
129
+
130
+ # Document Extractions that didn't go through a linked labels
131
+ # transformer lack a `groupings` section.
132
+ if (
133
+ task_type in ("annotation", "genai_annotation")
134
+ and "groupings" not in prediction
135
+ ):
136
+ prediction["groupings"] = []
137
+
138
+ # Summarizations may lack citations after review.
139
+ if task_type == "summarization" and "citations" not in prediction:
140
+ prediction["citations"] = []
141
+
142
+ # Prior to 6.8, v3 result files don't include a `reviews` section.
143
+ if not has(result, dict, "reviews"):
144
+ result["reviews"] = {}
145
+
146
+ # Review notes are `None` unless the reviewer enters a reason for rejection.
147
+ for review_dict in get(result, dict, "reviews").values():
148
+ if not has(review_dict, str, "review_notes"):
149
+ review_dict["review_notes"] = ""
150
+
151
+ # Prior to 7.0, v3 result files don't include an `errored_files` section.
152
+ if not has(result, dict, "errored_files"):
153
+ result["errored_files"] = {}
154
+
155
+ # Prior to 7.X, errored files may lack filenames.
156
+ for file in get(result, dict, "errored_files").values():
157
+ if not has(file, str, "input_filename") and has(file, str, "reason"):
158
+ match = re.search(r"file '([^']*)' with id", get(file, str, "reason"))
159
+ file["input_filename"] = match.group(1) if match else ""
@@ -0,0 +1,395 @@
1
+ from collections import defaultdict
2
+ from operator import attrgetter
3
+ from typing import TYPE_CHECKING, List, TypeVar, overload
4
+
5
+ from .model import ModelGroupType
6
+ from .predictions import (
7
+ Classification,
8
+ DocumentExtraction,
9
+ Extraction,
10
+ FormExtraction,
11
+ Prediction,
12
+ Summarization,
13
+ Unbundling,
14
+ )
15
+ from .review import Review, ReviewType
16
+ from .utilities import nfilter
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable, Collection, Container, Iterable
20
+ from typing import Any, Final, SupportsIndex
21
+
22
+ from typing_extensions import Self
23
+
24
+ from .document import Document
25
+ from .model import ModelGroup
26
+ from .result import Result
27
+
28
+ PredictionType = TypeVar("PredictionType", bound=Prediction)
29
+ OfType = TypeVar("OfType", bound=Prediction)
30
+ KeyType = TypeVar("KeyType")
31
+
32
+ # Non-None sentinel value to support `PredictionList.where(review=None)`.
33
+ REVIEW_UNSPECIFIED: "Final" = Review(
34
+ id=None, reviewer_id=None, notes=None, rejected=None, type=None # type: ignore[arg-type]
35
+ )
36
+
37
+
38
+ class PredictionList(List[PredictionType]):
39
+ @property
40
+ def classifications(self) -> "PredictionList[Classification]":
41
+ return self.oftype(Classification)
42
+
43
+ @property
44
+ def document_extractions(self) -> "PredictionList[DocumentExtraction]":
45
+ return self.oftype(DocumentExtraction)
46
+
47
+ @property
48
+ def extractions(self) -> "PredictionList[Extraction]":
49
+ return self.oftype(Extraction)
50
+
51
+ @property
52
+ def form_extractions(self) -> "PredictionList[FormExtraction]":
53
+ return self.oftype(FormExtraction)
54
+
55
+ @property
56
+ def summarizations(self) -> "PredictionList[Summarization]":
57
+ return self.oftype(Summarization)
58
+
59
+ @property
60
+ def unbundlings(self) -> "PredictionList[Unbundling]":
61
+ return self.oftype(Unbundling)
62
+
63
+ @overload
64
+ def __getitem__(self, index: "SupportsIndex", /) -> PredictionType: ...
65
+
66
+ @overload
67
+ def __getitem__(self, index: slice, /) -> "PredictionList[PredictionType]": ...
68
+
69
+ def __getitem__(
70
+ self, index: "SupportsIndex | slice"
71
+ ) -> "PredictionType | PredictionList[PredictionType]":
72
+ if isinstance(index, slice):
73
+ return type(self)(super().__getitem__(index))
74
+ else:
75
+ return super().__getitem__(index)
76
+
77
+ def apply(self, function: "Callable[[PredictionType], None]") -> "Self":
78
+ """
79
+ Apply `function` to all predictions.
80
+ """
81
+ for prediction in self:
82
+ function(prediction)
83
+
84
+ return self
85
+
86
+ def groupby(
87
+ self, key: "Callable[[PredictionType], KeyType]"
88
+ ) -> "dict[KeyType, Self]":
89
+ """
90
+ Group predictions into a dictionary using `key` to derive each prediction's key.
91
+ E.g. `key=attrgetter("label")` or `key=attrgetter("model")`.
92
+
93
+ If a derived key is an unhashable mutable collection (like set),
94
+ it's automatically converted to its hashable immutable variant (like frozenset).
95
+ This makes it easy to group by linked labels or unbundling pages.
96
+ """
97
+ grouped = defaultdict(type(self)) # type: ignore[var-annotated]
98
+
99
+ for prediction in self:
100
+ derived_key = key(prediction)
101
+
102
+ if isinstance(derived_key, list):
103
+ derived_key = tuple(derived_key) # type: ignore[assignment]
104
+ elif isinstance(derived_key, set):
105
+ derived_key = frozenset(derived_key) # type: ignore[assignment]
106
+
107
+ grouped[derived_key].append(prediction)
108
+
109
+ return grouped
110
+
111
+ def groupbyiter(
112
+ self, keys: "Callable[[PredictionType], Iterable[KeyType]]"
113
+ ) -> "dict[KeyType, Self]":
114
+ """
115
+ Group predictions into a dictionary using `key` to derive an iterable of keys.
116
+ E.g. `key=attrgetter("groups")` or `key=attrgetter("pages")`.
117
+
118
+ Each prediction is associated with every key in the iterable individually.
119
+ If the iterable is empty, the prediction is not included in any group.
120
+ """
121
+ grouped = defaultdict(type(self)) # type: ignore[var-annotated]
122
+
123
+ for prediction in self:
124
+ for derived_key in keys(prediction):
125
+ grouped[derived_key].append(prediction)
126
+
127
+ return grouped
128
+
129
+ def oftype(self, type: "type[OfType]") -> "PredictionList[OfType]":
130
+ """
131
+ Return a new prediction list containing predictions of type `type`.
132
+ """
133
+ return self.where(lambda prediction: isinstance(prediction, type)) # type: ignore[return-value]
134
+
135
+ def orderby(
136
+ self,
137
+ key: "Callable[[PredictionType], Any]",
138
+ *,
139
+ reverse: bool = False,
140
+ ) -> "Self":
141
+ """
142
+ Return a new prediction list with predictions sorted by `key`.
143
+ """
144
+ return type(self)(sorted(self, key=key, reverse=reverse))
145
+
146
+ def where(
147
+ self,
148
+ predicate: "Callable[[PredictionType], bool] | None" = None,
149
+ *,
150
+ document: "Document | None" = None,
151
+ document_in: "Container[Document] | None" = None,
152
+ model: "ModelGroup | ModelGroupType | str | None" = None,
153
+ model_in: "Container[ModelGroup | ModelGroupType | str] | None" = None,
154
+ review: "Review | ReviewType | None" = REVIEW_UNSPECIFIED,
155
+ review_in: "Container[Review | ReviewType | None]" = {REVIEW_UNSPECIFIED},
156
+ label: "str | None" = None,
157
+ label_in: "Container[str] | None" = None,
158
+ page: "int | None" = None,
159
+ page_in: "Collection[int] | None" = None,
160
+ min_confidence: "float | None" = None,
161
+ max_confidence: "float | None" = None,
162
+ accepted: "bool | None" = None,
163
+ rejected: "bool | None" = None,
164
+ checked: "bool | None" = None,
165
+ signed: "bool | None" = None,
166
+ ) -> "Self":
167
+ """
168
+ Return a new prediction list containing predictions that match
169
+ all of the specified filters.
170
+
171
+ predicate: predictions for which this function returns True,
172
+ document: predictions from this document,
173
+ document_in: predictions from these documents,
174
+ model: predictions from this model, task type, or name,
175
+ model_in: predictions from these models, task types, or names,
176
+ review: predictions from this review or review type,
177
+ review_in: predictions from these reviews or review types,
178
+ label: predictions with this label,
179
+ label_in: predictions with these labels,
180
+ page: extractions/unbundlings on this page,
181
+ page_in: extractions/unbundlings on these pages,
182
+ min_confidence: predictions with confidence >= this threshold,
183
+ max_confidence: predictions with confidence <= this threshold,
184
+ accepted: extractions that have or haven't been accepted,
185
+ rejected: extractions that have or haven't been rejected,
186
+ checked: form extractions that are or aren't checked,
187
+ signed: form extractions that are or aren't signed.
188
+ """
189
+ predicates = []
190
+
191
+ if predicate is not None:
192
+ predicates.append(predicate)
193
+
194
+ if document is not None:
195
+ predicates.append(lambda prediction: prediction.document == document)
196
+
197
+ if document_in is not None:
198
+ predicates.append(lambda prediction: prediction.document in document_in)
199
+
200
+ if model is not None:
201
+ predicates.append(
202
+ lambda prediction: (
203
+ prediction.model == model
204
+ or prediction.model.type == model
205
+ or prediction.model.name == model
206
+ )
207
+ )
208
+
209
+ if model_in is not None:
210
+ predicates.append(
211
+ lambda prediction: (
212
+ prediction.model in model_in
213
+ or prediction.model.type in model_in
214
+ or prediction.model.name in model_in
215
+ )
216
+ )
217
+
218
+ if review is not REVIEW_UNSPECIFIED:
219
+ predicates.append(
220
+ lambda prediction: (
221
+ prediction.review == review
222
+ or (
223
+ prediction.review is not None
224
+ and prediction.review.type == review
225
+ )
226
+ )
227
+ )
228
+
229
+ if review_in != {REVIEW_UNSPECIFIED}:
230
+ predicates.append(
231
+ lambda prediction: (
232
+ prediction.review in review_in
233
+ or (
234
+ prediction.review is not None
235
+ and prediction.review.type in review_in
236
+ )
237
+ )
238
+ )
239
+
240
+ if label is not None:
241
+ predicates.append(lambda prediction: prediction.label == label)
242
+
243
+ if label_in is not None:
244
+ predicates.append(lambda prediction: prediction.label in label_in)
245
+
246
+ if min_confidence is not None:
247
+ predicates.append(
248
+ lambda prediction: prediction.confidence >= min_confidence
249
+ )
250
+
251
+ if max_confidence is not None:
252
+ predicates.append(
253
+ lambda prediction: prediction.confidence <= max_confidence
254
+ )
255
+
256
+ if page is not None:
257
+ predicates.append(
258
+ lambda prediction: (
259
+ (isinstance(prediction, Extraction) and prediction.page == page)
260
+ or (isinstance(prediction, Unbundling) and page in prediction.pages)
261
+ )
262
+ )
263
+
264
+ if page_in is not None:
265
+ page_in = set(page_in)
266
+ predicates.append(
267
+ lambda prediction: (
268
+ (isinstance(prediction, Extraction) and prediction.page in page_in)
269
+ or (
270
+ isinstance(prediction, Unbundling)
271
+ and bool(page_in & set(prediction.pages))
272
+ )
273
+ )
274
+ )
275
+
276
+ if accepted is not None:
277
+ predicates.append(
278
+ lambda prediction: isinstance(prediction, Extraction)
279
+ and prediction.accepted == accepted
280
+ )
281
+
282
+ if rejected is not None:
283
+ predicates.append(
284
+ lambda prediction: isinstance(prediction, Extraction)
285
+ and prediction.rejected == rejected
286
+ )
287
+
288
+ if checked is not None:
289
+ predicates.append(
290
+ lambda prediction: isinstance(prediction, FormExtraction)
291
+ and prediction.checked == checked
292
+ )
293
+
294
+ if signed is not None:
295
+ predicates.append(
296
+ lambda prediction: isinstance(prediction, FormExtraction)
297
+ and prediction.signed == signed
298
+ )
299
+
300
+ return type(self)(nfilter(predicates, self))
301
+
302
+ def accept(self) -> "Self":
303
+ """
304
+ Mark extractions as accepted for auto review.
305
+ """
306
+ self.oftype(Extraction).apply(Extraction.accept)
307
+ return self
308
+
309
+ def unaccept(self) -> "Self":
310
+ """
311
+ Mark extractions as not accepted for auto review.
312
+ """
313
+ self.oftype(Extraction).apply(Extraction.unaccept)
314
+ return self
315
+
316
+ def reject(self) -> "Self":
317
+ """
318
+ Mark extractions as rejected for auto review.
319
+ """
320
+ self.oftype(Extraction).apply(Extraction.reject)
321
+ return self
322
+
323
+ def unreject(self) -> "Self":
324
+ """
325
+ Mark extractions as not rejected for auto review.
326
+ """
327
+ self.oftype(Extraction).apply(Extraction.unreject)
328
+ return self
329
+
330
+ def to_changes(self, result: "Result") -> "Any":
331
+ """
332
+ Create a dict or list for the `changes` argument of `SubmitReview` based on the
333
+ predictions in this prediction list and the documents and version of `result`.
334
+ """
335
+ if result.version == 1:
336
+ return self.to_v1_changes(result.documents[0])
337
+ elif result.version == 3:
338
+ return self.to_v3_changes(result.documents)
339
+ else:
340
+ raise ValueError(f"unsupported file version `{result.version!r}`")
341
+
342
+ def to_v1_changes(self, document: "Document") -> "dict[str, Any]":
343
+ """
344
+ Create a v1 dict for the `changes` argument of `SubmitReview`.
345
+ """
346
+ changes: "dict[str, Any]" = {}
347
+
348
+ for model, predictions in self.groupby(attrgetter("model")).items():
349
+ if model.type == ModelGroupType.CLASSIFICATION:
350
+ changes[model.name] = predictions[0].to_v1_dict()
351
+ else:
352
+ changes[model.name] = [
353
+ prediction.to_v1_dict() for prediction in predictions
354
+ ]
355
+
356
+ for model_name in document._model_sections:
357
+ if model_name not in changes:
358
+ changes[model_name] = []
359
+
360
+ return changes
361
+
362
+ def to_v3_changes(self, documents: "Iterable[Document]") -> "list[dict[str, Any]]":
363
+ """
364
+ Create a v3 list for the `changes` argument of `SubmitReview`.
365
+ """
366
+ changes: "list[dict[str, Any]]" = []
367
+
368
+ for document in documents:
369
+ if document.failed:
370
+ continue
371
+
372
+ model_results: "dict[str, Any]" = {}
373
+ changes.append(
374
+ {
375
+ "submissionfile_id": document.id,
376
+ "model_results": model_results,
377
+ "component_results": {},
378
+ }
379
+ )
380
+ predictions_by_model = self.where(
381
+ document=document,
382
+ ).groupby(
383
+ attrgetter("model"),
384
+ )
385
+
386
+ for model, predictions in predictions_by_model.items():
387
+ model_results[str(model.id)] = [
388
+ prediction.to_v3_dict() for prediction in predictions
389
+ ]
390
+
391
+ for model_id in document._model_sections:
392
+ if model_id not in model_results:
393
+ model_results[model_id] = []
394
+
395
+ return changes