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,103 @@
1
+ from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ..review import Review
5
+ from ..utilities import get, has, omit
6
+ from .citation import NULL_CITATION, Citation
7
+ from .extraction import Extraction
8
+
9
+ if TYPE_CHECKING:
10
+ from typing import Any
11
+
12
+ from ..document import Document
13
+ from ..model import ModelGroup
14
+ from .span import Span
15
+
16
+
17
+ @dataclass
18
+ class Summarization(Extraction):
19
+ citations: "list[Citation]"
20
+
21
+ @property
22
+ def citation(self) -> Citation:
23
+ """
24
+ Return the first `Citation` the summarization covers else `NULL_CITATION`.
25
+
26
+ Post-review, summarizations have no citations.
27
+ """
28
+ return self.citations[0] if self.citations else NULL_CITATION
29
+
30
+ @citation.setter
31
+ def citation(self, citation: Citation) -> None:
32
+ """
33
+ Overwrite all `citations` with the one provided.
34
+
35
+ This is implemented under the assumption that if you're setting the single
36
+ citation, you want it to be the only one. And if you're working in a context
37
+ that's multiple-citation sensetive, you'll set `extraction.citations` instead.
38
+ """
39
+ self.citations = [citation]
40
+
41
+ @property
42
+ def span(self) -> "Span":
43
+ return self.citation.span
44
+
45
+ @span.setter
46
+ def span(self, span: "Span") -> None:
47
+ self.citations = [Citation(self.citation.start, self.citation.end, span)]
48
+
49
+ @staticmethod
50
+ def from_v3_dict(
51
+ document: "Document",
52
+ model: "ModelGroup",
53
+ review: "Review | None",
54
+ prediction: object,
55
+ ) -> "Summarization":
56
+ """
57
+ Create a `Summarization` from a v3 prediction dictionary.
58
+ """
59
+ return Summarization(
60
+ document=document,
61
+ model=model,
62
+ review=review,
63
+ label=get(prediction, str, "label"),
64
+ confidences=get(prediction, dict, "confidence"),
65
+ text=get(prediction, str, "text"),
66
+ accepted=(
67
+ has(prediction, bool, "accepted") and get(prediction, bool, "accepted")
68
+ ),
69
+ rejected=(
70
+ has(prediction, bool, "rejected") and get(prediction, bool, "rejected")
71
+ ),
72
+ citations=sorted(
73
+ map(Citation.from_dict, get(prediction, list, "citations"))
74
+ ),
75
+ extras=omit(
76
+ prediction,
77
+ "label",
78
+ "confidence",
79
+ "text",
80
+ "accepted",
81
+ "rejected",
82
+ "citations",
83
+ ),
84
+ )
85
+
86
+ def to_v3_dict(self) -> "dict[str, Any]":
87
+ """
88
+ Create a prediction dictionary for v3 auto review changes.
89
+ """
90
+ prediction = {
91
+ **self.extras,
92
+ "label": self.label,
93
+ "confidence": self.confidences,
94
+ "text": self.text,
95
+ "citations": [citation.to_dict() for citation in self.citations],
96
+ }
97
+
98
+ if self.accepted:
99
+ prediction["accepted"] = True
100
+ elif self.rejected:
101
+ prediction["rejected"] = True
102
+
103
+ return prediction
@@ -0,0 +1,51 @@
1
+ from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ..review import Review
5
+ from ..utilities import get, omit
6
+ from .prediction import Prediction
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Any
10
+
11
+ from ..document import Document
12
+ from ..model import ModelGroup
13
+
14
+
15
+ @dataclass
16
+ class Unbundling(Prediction):
17
+ pages: "list[int]"
18
+
19
+ @staticmethod
20
+ def from_v3_dict(
21
+ document: "Document",
22
+ model: "ModelGroup",
23
+ review: "Review | None",
24
+ prediction: object,
25
+ ) -> "Unbundling":
26
+ """
27
+ Create an `Unbundling` from a v3 prediction dictionary.
28
+ """
29
+ return Unbundling(
30
+ document=document,
31
+ model=model,
32
+ review=review,
33
+ label=get(prediction, str, "label"),
34
+ confidences=get(prediction, dict, "confidence"),
35
+ pages=[
36
+ get(span, int, "page_num")
37
+ for span in get(prediction, list, "spans") # fmt: skip
38
+ ],
39
+ extras=omit(prediction, "confidence", "label", "spans"),
40
+ )
41
+
42
+ def to_v3_dict(self) -> "dict[str, Any]":
43
+ """
44
+ Create a prediction dictionary for v3 auto review changes.
45
+ """
46
+ return {
47
+ **self.extras,
48
+ "label": self.label,
49
+ "confidence": self.confidences,
50
+ "spans": [{"page_num": page} for page in self.pages],
51
+ }
@@ -0,0 +1,152 @@
1
+ from dataclasses import dataclass
2
+ from functools import partial
3
+ from itertools import chain
4
+ from typing import TYPE_CHECKING
5
+
6
+ from . import predictions as prediction
7
+ from .document import Document
8
+ from .model import ModelGroup
9
+ from .normalization import normalize_v1_result, normalize_v3_result
10
+ from .predictionlist import PredictionList
11
+ from .predictions import Prediction
12
+ from .review import Review, ReviewType
13
+ from .utilities import get
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any
17
+
18
+
19
+ @dataclass(frozen=True, order=True)
20
+ class Result:
21
+ version: int
22
+ submission_id: int
23
+ documents: "tuple[Document, ...]"
24
+ models: "tuple[ModelGroup, ...]"
25
+ predictions: "PredictionList[Prediction]"
26
+ reviews: "tuple[Review, ...]"
27
+
28
+ @property
29
+ def rejected(self) -> bool:
30
+ return len(self.reviews) > 0 and self.reviews[-1].rejected
31
+
32
+ @property
33
+ def pre_review(self) -> "PredictionList[Prediction]":
34
+ return self.predictions.where(review=None)
35
+
36
+ @property
37
+ def auto_review(self) -> "PredictionList[Prediction]":
38
+ return self.predictions.where(review=ReviewType.AUTO)
39
+
40
+ @property
41
+ def manual_review(self) -> "PredictionList[Prediction]":
42
+ return self.predictions.where(review=ReviewType.MANUAL)
43
+
44
+ @property
45
+ def admin_review(self) -> "PredictionList[Prediction]":
46
+ return self.predictions.where(review=ReviewType.ADMIN)
47
+
48
+ @property
49
+ def final(self) -> "PredictionList[Prediction]":
50
+ return self.predictions.where(review=self.reviews[-1] if self.reviews else None)
51
+
52
+ @staticmethod
53
+ def from_v1_dict(result: object) -> "Result":
54
+ """
55
+ Create a `Result` from a v1 result file dictionary.
56
+ """
57
+ normalize_v1_result(result)
58
+
59
+ version = get(result, int, "file_version")
60
+ submission_id = get(result, int, "submission_id")
61
+ submission_results = get(result, dict, "results", "document", "results")
62
+ review_metadata = get(result, list, "reviews_meta")
63
+
64
+ document = Document.from_v1_dict(result)
65
+ models = sorted(map(ModelGroup.from_v1_section, submission_results.items()))
66
+ predictions: "PredictionList[Prediction]" = PredictionList()
67
+ # Reviews must be sorted after parsing predictions, as they match positionally
68
+ # with prediction lists in `post_reviews`.
69
+ reviews = list(map(Review.from_dict, review_metadata))
70
+
71
+ for model_name, model_predictions in submission_results.items():
72
+ model = next(filter(lambda model: model.name == model_name, models))
73
+ reviewed_model_predictions: "list[tuple[Review | None, Any]]" = [
74
+ (None, get(model_predictions, list, "pre_review")),
75
+ *filter(
76
+ lambda review_predictions: not review_predictions[0].rejected,
77
+ zip(reviews, get(model_predictions, list, "post_reviews")),
78
+ ),
79
+ ]
80
+
81
+ for review, model_predictions in reviewed_model_predictions:
82
+ predictions.extend(
83
+ map(
84
+ partial(prediction.from_v1_dict, document, model, review),
85
+ model_predictions,
86
+ )
87
+ )
88
+
89
+ return Result(
90
+ version=version,
91
+ submission_id=submission_id,
92
+ documents=(document,),
93
+ models=tuple(models),
94
+ predictions=predictions,
95
+ reviews=tuple(sorted(reviews)),
96
+ )
97
+
98
+ @staticmethod
99
+ def from_v3_dict(result: object) -> "Result":
100
+ """
101
+ Create a `Result` from a v3 result file dictionary.
102
+ """
103
+ normalize_v3_result(result)
104
+
105
+ version = get(result, int, "file_version")
106
+ submission_id = get(result, int, "submission_id")
107
+ submission_results = get(result, list, "submission_results")
108
+ modelgroup_metadata = get(result, dict, "modelgroup_metadata")
109
+ review_metadata = get(result, dict, "reviews")
110
+
111
+ processed_documents = map(Document.from_v3_dict, submission_results)
112
+ errored_files = get(result, dict, "errored_files").values()
113
+ failed_documents = map(Document.from_v3_errored_file, errored_files)
114
+ documents = sorted(chain(processed_documents, failed_documents))
115
+ models = sorted(map(ModelGroup.from_v3_dict, modelgroup_metadata.values()))
116
+ predictions: "PredictionList[Prediction]" = PredictionList()
117
+ reviews = sorted(map(Review.from_dict, review_metadata.values()))
118
+
119
+ for document_dict in submission_results:
120
+ document_id = get(document_dict, int, "submissionfile_id")
121
+ document = next(
122
+ filter(lambda document: document.id == document_id, documents)
123
+ )
124
+ reviewed_model_predictions: "list[tuple[Review | None, Any]]" = [
125
+ (None, get(document_dict, dict, "model_results", "ORIGINAL"))
126
+ ]
127
+
128
+ if reviews:
129
+ reviewed_model_predictions.append(
130
+ (reviews[-1], get(document_dict, dict, "model_results", "FINAL"))
131
+ )
132
+
133
+ for review, model_section in reviewed_model_predictions:
134
+ for model_id, model_predictions in model_section.items():
135
+ model = next(
136
+ filter(lambda model: model.id == int(model_id), models)
137
+ )
138
+ predictions.extend(
139
+ map(
140
+ partial(prediction.from_v3_dict, document, model, review),
141
+ model_predictions,
142
+ )
143
+ )
144
+
145
+ return Result(
146
+ version=version,
147
+ submission_id=submission_id,
148
+ documents=tuple(documents),
149
+ models=tuple(models),
150
+ predictions=predictions,
151
+ reviews=tuple(reviews),
152
+ )
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from .utilities import get
5
+
6
+
7
+ class ReviewType(Enum):
8
+ ADMIN = "admin"
9
+ AUTO = "auto"
10
+ MANUAL = "manual"
11
+
12
+
13
+ @dataclass(frozen=True, order=True)
14
+ class Review:
15
+ id: int
16
+ reviewer_id: int
17
+ notes: str
18
+ rejected: bool
19
+ type: ReviewType
20
+
21
+ @staticmethod
22
+ def from_dict(review: object) -> "Review":
23
+ """
24
+ Create a `Review` from a review dictionary.
25
+ """
26
+ return Review(
27
+ id=get(review, int, "review_id"),
28
+ reviewer_id=get(review, int, "reviewer_id"),
29
+ notes=get(review, str, "review_notes"),
30
+ rejected=get(review, bool, "review_rejected"),
31
+ type=ReviewType(get(review, str, "review_type")),
32
+ )
@@ -0,0 +1,73 @@
1
+ from collections.abc import Iterable, Iterator
2
+ from typing import Callable, TypeVar
3
+
4
+ from .errors import ResultError
5
+
6
+ Value = TypeVar("Value")
7
+
8
+
9
+ def get(result: object, value_type: "type[Value]", *keys: "str | int") -> Value:
10
+ """
11
+ Return the value obtained by traversing `result` using `keys` as indices if that
12
+ value has type `value_type`. Raise a `ResultError` otherwise.
13
+ """
14
+ for key in keys:
15
+ if isinstance(key, str) and isinstance(result, dict) and key in result:
16
+ result = result[key]
17
+ elif isinstance(key, int) and isinstance(result, list) and key < len(result):
18
+ result = result[key]
19
+ else:
20
+ raise ResultError(
21
+ f"result object `{type(result)!r}` does not contain key `{key!r}`"
22
+ )
23
+
24
+ if isinstance(result, value_type):
25
+ return result
26
+ else:
27
+ raise ResultError(
28
+ f"result object `{type(result)!r}` does not have a value for "
29
+ f"key `{key!r}` of type `{value_type}`"
30
+ )
31
+
32
+
33
+ def has(result: object, value_type: "type[Value]", *keys: "str | int") -> bool:
34
+ """
35
+ Check if `result` can be traversed using `keys` to a value of type `value_type`.
36
+ """
37
+ for key in keys:
38
+ if isinstance(key, str) and isinstance(result, dict) and key in result:
39
+ result = result[key]
40
+ elif isinstance(key, int) and isinstance(result, list) and key < len(result):
41
+ result = result[key]
42
+ else:
43
+ return False
44
+
45
+ return isinstance(result, value_type)
46
+
47
+
48
+ def nfilter(
49
+ predicates: "Iterable[Callable[[Value], bool]]", values: "Iterable[Value]"
50
+ ) -> "Iterator[Value]":
51
+ """
52
+ Apply multiple filter predicates to an iterable of values.
53
+
54
+ `nfilter([first, second, third], values)` is equivalent to
55
+ `filter(third, filter(second, filter(first, values)))`.
56
+ """
57
+ for predicate in predicates:
58
+ values = filter(predicate, values)
59
+
60
+ yield from values
61
+
62
+
63
+ def omit(dictionary: object, *keys: str) -> "dict[str, Value]":
64
+ """
65
+ Return a shallow copy of `dictionary` with `keys` omitted.
66
+ """
67
+ if not isinstance(dictionary, dict):
68
+ return {}
69
+ return {
70
+ key: value
71
+ for key, value in dictionary.items()
72
+ if key not in keys
73
+ } # fmt: skip
@@ -0,0 +1,94 @@
1
+ import asyncio
2
+ import time
3
+ from functools import wraps
4
+ from inspect import iscoroutinefunction
5
+ from random import random
6
+ from typing import TYPE_CHECKING, overload
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import ParamSpec, TypeVar
11
+
12
+ ArgumentsType = ParamSpec("ArgumentsType")
13
+ OuterReturnType = TypeVar("OuterReturnType")
14
+ InnerReturnType = TypeVar("InnerReturnType")
15
+
16
+
17
+ def retry(
18
+ *errors: "type[Exception]",
19
+ count: int = 4,
20
+ wait: float = 1,
21
+ backoff: float = 4,
22
+ jitter: float = 0.5,
23
+ ) -> "Callable[[Callable[ArgumentsType, OuterReturnType]], Callable[ArgumentsType, OuterReturnType]]": # noqa: E501
24
+ """
25
+ Decorate a function or coroutine to retry when it raises specified errors,
26
+ apply exponential backoff and jitter to the wait time,
27
+ and raise the last error if it retries too many times.
28
+
29
+ By default, the decorated function or coroutine will be retried up to 4 times over
30
+ the course of ~2 minutes (waiting 1, 4, 16, and 64 seconds; plus up to 50% jitter).
31
+
32
+ Arguments:
33
+ errors: Retry the function when it raises one of these errors.
34
+ count: Retry the function this many times.
35
+ wait: Wait this many seconds after the first error before retrying.
36
+ backoff: Multiply the wait time by this amount for each additional error.
37
+ jitter: Add a random amount of time (up to this percent as a decimal)
38
+ to the wait time to prevent simultaneous retries.
39
+ """
40
+
41
+ def wait_time(times_retried: int) -> float:
42
+ """
43
+ Calculate the sleep time based on number of times retried.
44
+ """
45
+ return wait * backoff**times_retried * (1 + jitter * random())
46
+
47
+ @overload
48
+ def retry_decorator(
49
+ decorated: "Callable[ArgumentsType, Awaitable[InnerReturnType]]",
50
+ ) -> "Callable[ArgumentsType, Awaitable[InnerReturnType]]": ...
51
+ @overload
52
+ def retry_decorator(
53
+ decorated: "Callable[ArgumentsType, InnerReturnType]",
54
+ ) -> "Callable[ArgumentsType, InnerReturnType]": ...
55
+ def retry_decorator(
56
+ decorated: "Callable[ArgumentsType, InnerReturnType]",
57
+ ) -> "Callable[ArgumentsType, Awaitable[InnerReturnType]] | Callable[ArgumentsType, InnerReturnType]": # noqa: E501
58
+ """
59
+ Decorate either a function or coroutine as appropriate.
60
+ """
61
+ if iscoroutinefunction(decorated):
62
+
63
+ @wraps(decorated)
64
+ async def retrying_coroutine( # type: ignore[return]
65
+ *args: "ArgumentsType.args", **kwargs: "ArgumentsType.kwargs"
66
+ ) -> "InnerReturnType":
67
+ for times_retried in range(count + 1):
68
+ try:
69
+ return await decorated(*args, **kwargs) # type: ignore[no-any-return]
70
+ except errors:
71
+ if times_retried >= count:
72
+ raise
73
+
74
+ await asyncio.sleep(wait_time(times_retried))
75
+
76
+ return retrying_coroutine
77
+ else:
78
+
79
+ @wraps(decorated)
80
+ def retrying_function( # type: ignore[return]
81
+ *args: "ArgumentsType.args", **kwargs: "ArgumentsType.kwargs"
82
+ ) -> "InnerReturnType":
83
+ for times_retried in range(count + 1):
84
+ try:
85
+ return decorated(*args, **kwargs)
86
+ except errors:
87
+ if times_retried >= count:
88
+ raise
89
+
90
+ time.sleep(wait_time(times_retried))
91
+
92
+ return retrying_function
93
+
94
+ return retry_decorator
@@ -0,0 +1,3 @@
1
+ from .snapshot import Snapshot
2
+
3
+ __all__ = ("Snapshot",)