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,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
|
indico_toolkit/retry.py
ADDED
|
@@ -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
|