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.
- indico_toolkit/__init__.py +24 -0
- indico_toolkit/association/__init__.py +15 -0
- indico_toolkit/association/association.py +65 -0
- indico_toolkit/association/extracted_tokens.py +106 -0
- indico_toolkit/association/line_items.py +189 -0
- indico_toolkit/association/positioning.py +301 -0
- indico_toolkit/association/splitting.py +41 -0
- indico_toolkit/auto_populate/__init__.py +3 -0
- indico_toolkit/auto_populate/populator.py +362 -0
- indico_toolkit/auto_populate/types.py +66 -0
- indico_toolkit/auto_review/__init__.py +6 -0
- indico_toolkit/auto_review/auto_review_functions.py +123 -0
- indico_toolkit/auto_review/auto_reviewer.py +62 -0
- indico_toolkit/client.py +35 -0
- indico_toolkit/errors.py +32 -0
- indico_toolkit/etloutput/__init__.py +210 -0
- indico_toolkit/etloutput/cell.py +47 -0
- indico_toolkit/etloutput/errors.py +16 -0
- indico_toolkit/etloutput/etloutput.py +110 -0
- indico_toolkit/etloutput/range.py +30 -0
- indico_toolkit/etloutput/table.py +44 -0
- indico_toolkit/etloutput/token.py +25 -0
- indico_toolkit/indico_wrapper/__init__.py +15 -0
- indico_toolkit/indico_wrapper/dataset.py +114 -0
- indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
- indico_toolkit/indico_wrapper/download.py +193 -0
- indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
- indico_toolkit/indico_wrapper/modelop.py +200 -0
- indico_toolkit/indico_wrapper/reviewer.py +111 -0
- indico_toolkit/indico_wrapper/workflow.py +231 -0
- indico_toolkit/metrics/__init__.py +11 -0
- indico_toolkit/metrics/compare_ground_truth.py +123 -0
- indico_toolkit/metrics/compare_models.py +180 -0
- indico_toolkit/metrics/metrics.py +373 -0
- indico_toolkit/metrics/plotting.py +102 -0
- indico_toolkit/ocr/__init__.py +9 -0
- indico_toolkit/ocr/customocr_object.py +42 -0
- indico_toolkit/ocr/ondoc_object.py +93 -0
- indico_toolkit/ocr/standard_object.py +54 -0
- indico_toolkit/pipelines/__init__.py +3 -0
- indico_toolkit/pipelines/file_processing.py +157 -0
- indico_toolkit/polling/__init__.py +8 -0
- indico_toolkit/polling/autoreview.py +204 -0
- indico_toolkit/polling/downstream.py +145 -0
- indico_toolkit/polling/queries.py +68 -0
- indico_toolkit/py.typed +0 -0
- indico_toolkit/results/__init__.py +104 -0
- indico_toolkit/results/document.py +78 -0
- indico_toolkit/results/errors.py +4 -0
- indico_toolkit/results/model.py +60 -0
- indico_toolkit/results/normalization.py +159 -0
- indico_toolkit/results/predictionlist.py +395 -0
- indico_toolkit/results/predictions/__init__.py +88 -0
- indico_toolkit/results/predictions/box.py +58 -0
- indico_toolkit/results/predictions/citation.py +47 -0
- indico_toolkit/results/predictions/classification.py +50 -0
- indico_toolkit/results/predictions/documentextraction.py +160 -0
- indico_toolkit/results/predictions/extraction.py +35 -0
- indico_toolkit/results/predictions/formextraction.py +121 -0
- indico_toolkit/results/predictions/group.py +29 -0
- indico_toolkit/results/predictions/prediction.py +41 -0
- indico_toolkit/results/predictions/span.py +43 -0
- indico_toolkit/results/predictions/summarization.py +103 -0
- indico_toolkit/results/predictions/unbundling.py +51 -0
- indico_toolkit/results/result.py +152 -0
- indico_toolkit/results/review.py +32 -0
- indico_toolkit/results/utilities.py +73 -0
- indico_toolkit/retry.py +94 -0
- indico_toolkit/snapshots/__init__.py +3 -0
- indico_toolkit/snapshots/snapshot.py +340 -0
- indico_toolkit/structure/__init__.py +3 -0
- indico_toolkit/structure/create_structure.py +232 -0
- indico_toolkit/structure/queries.py +119 -0
- indico_toolkit/structure/utils.py +16 -0
- indico_toolkit/types/__init__.py +12 -0
- indico_toolkit/types/classification.py +92 -0
- indico_toolkit/types/extractions.py +227 -0
- indico_toolkit/types/predictions.py +28 -0
- indico_toolkit/types/workflow_object.py +133 -0
- indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
- indico_toolkit-6.14.0.dist-info/METADATA +144 -0
- indico_toolkit-6.14.0.dist-info/RECORD +83 -0
- 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,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
|