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,193 @@
1
+ import os
2
+ from io import StringIO
3
+
4
+ from indico import IndicoClient, IndicoRequestError
5
+ from indico.queries import (
6
+ CreateExport,
7
+ DownloadExport,
8
+ GraphQLRequest,
9
+ RetrieveStorageObject,
10
+ )
11
+ from indico.types.export import Export
12
+
13
+ from ..errors import ToolkitInputError
14
+ from ..retry import retry
15
+
16
+ try:
17
+ import pandas as pd
18
+
19
+ _PANDAS_INSTALLED = True
20
+ except ImportError as error:
21
+ _PANDAS_INSTALLED = False
22
+ _IMPORT_ERROR = error
23
+
24
+
25
+ class Download:
26
+ """
27
+ Class to support downloading resources from an Indico Cluster
28
+ """
29
+
30
+ def __init__(self, client: IndicoClient):
31
+ self.client = client
32
+
33
+ def get_dataset_pdfs(
34
+ self,
35
+ dataset_id: int,
36
+ labelset_id: int,
37
+ output_dir: str,
38
+ max_files_to_download: int = None,
39
+ ) -> int:
40
+ """Download PDFs from an uploaded dataset to a local directory
41
+
42
+ Args:
43
+ dataset_id (int): Dataset ID to download from
44
+ labelset_id (int): ID of your labelset (from teach task)
45
+ output_dir (str): Path to directory to write PDFs
46
+ max_files_to_download (int): = Max number of files to download
47
+ (default: None = download all)
48
+
49
+ Raises:
50
+ ToolkitInputError: Exception if invalid directory path
51
+
52
+ Returns:
53
+ int: Number of files downloaded
54
+ """
55
+ if not os.path.isdir(output_dir):
56
+ raise ToolkitInputError(f"Path is not a directory: {output_dir}")
57
+ export = self._create_export(dataset_id, labelset_id)
58
+ df = self._download_export(export.id)
59
+ num_files_downloaded = self._download_pdfs_from_export(
60
+ df,
61
+ output_dir,
62
+ f"file_name_{dataset_id}",
63
+ f"file_url_{dataset_id}",
64
+ max_files_to_download,
65
+ )
66
+ return num_files_downloaded
67
+
68
+ def get_uploaded_csv_dataframe(self, dataset_id: int) -> "pd.DataFrame":
69
+ """
70
+ Get a dataframe from a CSV that has been uploaded to the platform
71
+ Args:
72
+ dataset_id (int): id of the dataset
73
+ Returns:
74
+ pd.DataFrame: a dataframe representation of the CSV you uploaded
75
+ """
76
+ if not _PANDAS_INSTALLED:
77
+ raise RuntimeError(
78
+ "getting an uploaded CSV dataframe requires additional dependencies: "
79
+ "`pip install indico-toolkit[downloads]`"
80
+ ) from _IMPORT_ERROR
81
+
82
+ url = self._get_csv_download_url(dataset_id)
83
+ string_df = self._retrieve_storage_object(url)
84
+ return pd.read_csv(StringIO(string_df))
85
+
86
+ def get_snapshot_dataframe(
87
+ self, dataset_id: int, labelset_id: int, file_info: bool = True, **kwargs
88
+ ) -> "pd.DataFrame":
89
+ """Download a snapshot. For additional arguments,
90
+ see documentation for CreateExport in the Python SDK.
91
+
92
+ Args:
93
+ dataset_id (int): dataset ID you're interested in
94
+ labelset_id (int): ID of your labelset (from teach task)
95
+ file_info (bool, optional): Include additional file level metadata.
96
+ Defaults to True.
97
+
98
+ Returns:
99
+ pd.DataFrame: DataFrame with full document text and additional metadata
100
+ """
101
+ export = self._create_export(
102
+ dataset_id, labelset_id, file_info=file_info, **kwargs
103
+ )
104
+ return self._download_export(export.id)
105
+
106
+ def _download_pdfs_from_export(
107
+ self,
108
+ export_df: "pd.DataFrame",
109
+ output_dir: str,
110
+ file_name_col: str,
111
+ file_url_col: str,
112
+ max_files_to_download: int = None,
113
+ ) -> int:
114
+ try:
115
+ import tqdm
116
+ except ImportError as error:
117
+ raise RuntimeError(
118
+ "downloading pdfs requires additional dependencies: "
119
+ "`pip install indico-toolkit[downloads]`"
120
+ ) from error
121
+
122
+ for i, row in tqdm.tqdm(export_df.iterrows()):
123
+ basename = os.path.basename(row[file_name_col])
124
+ pdf_bytes = self._retrieve_storage_object(row[file_url_col])
125
+ with open(os.path.join(output_dir, basename), "wb") as fd:
126
+ fd.write(pdf_bytes)
127
+ if max_files_to_download and i + 1 == max_files_to_download:
128
+ return max_files_to_download
129
+ return export_df.shape[0]
130
+
131
+ @retry(IndicoRequestError, ConnectionError)
132
+ def _download_export(self, export_id: int) -> "pd.DataFrame":
133
+ """
134
+ Download a dataframe representation of your dataset export
135
+ """
136
+ return self.client.call(DownloadExport(export_id=export_id))
137
+
138
+ @retry(IndicoRequestError, ConnectionError)
139
+ def _create_export(
140
+ self,
141
+ dataset_id: int,
142
+ labelset_id: int,
143
+ file_info: bool = True,
144
+ wait: bool = True,
145
+ **kwargs,
146
+ ) -> Export:
147
+ """
148
+ Create an export object that can be used to download datasets. For additional
149
+ kwargs, see CreateExport docstring in the Python SDK.
150
+
151
+ Args:
152
+ dataset_id (int): ID of your dataset
153
+ labelset_id (int): ID of your labelset (from teach task)
154
+ file_info (bool, optional): whether to include additional file metadata.
155
+ Defaults to True.
156
+ wait (bool, optional): wait for export to be created. Defaults to True.
157
+
158
+ Returns:
159
+ Export: Description of dataset assets. See Python SDK for full object
160
+ description
161
+ """
162
+ return self.client.call(
163
+ CreateExport(
164
+ dataset_id=dataset_id,
165
+ file_info=file_info,
166
+ labelset_id=labelset_id,
167
+ wait=wait,
168
+ **kwargs,
169
+ )
170
+ )
171
+
172
+ @retry(IndicoRequestError, ConnectionError)
173
+ def _retrieve_storage_object(self, url: str):
174
+ return self.client.call(RetrieveStorageObject(url))
175
+
176
+ def _get_csv_download_url(self, dataset_id: int) -> str:
177
+ query = """
178
+ query getDataURL($id: Int!) {
179
+ dataset(id: $id) {
180
+ files {
181
+ fileType
182
+ id
183
+ rainbowUrl
184
+ }
185
+ }
186
+ }
187
+ """
188
+ result = self.client.call(GraphQLRequest(query, {"id": dataset_id}))
189
+ # loop through in case there are other file types uploaded to dataset
190
+ for file in result["dataset"]["files"]:
191
+ if file["fileType"] == "CSV":
192
+ return file["rainbowUrl"]
193
+ raise ToolkitInputError(f"There are no CSVs uploaded to {dataset_id}")
@@ -0,0 +1,119 @@
1
+ from typing import List, Union
2
+
3
+ from indico import IndicoClient
4
+ from indico.errors import IndicoRequestError
5
+ from indico.queries import (
6
+ AddModelGroupComponent,
7
+ CreateStorageURLs,
8
+ GraphQLRequest,
9
+ JobStatus,
10
+ ModelGroupPredict,
11
+ RetrieveStorageObject,
12
+ )
13
+ from indico.types import Dataset, ModelGroup, Workflow
14
+
15
+ from ..errors import ToolkitStatusError
16
+ from ..retry import retry
17
+ from ..types import Predictions
18
+
19
+
20
+ class IndicoWrapper:
21
+ """
22
+ Class for shared API functionality
23
+ """
24
+
25
+ def __init__(self, client: IndicoClient):
26
+ """
27
+ Create indico client with user provided arguments
28
+
29
+ args:
30
+ client (IndicoClient): instantiated Indico Client object
31
+ """
32
+ self.client = client
33
+
34
+ def train_model(
35
+ self,
36
+ dataset: Dataset,
37
+ workflow: Workflow,
38
+ model_name: str,
39
+ source_col: str,
40
+ target_col: str,
41
+ after_component_id: int = None,
42
+ ) -> ModelGroup:
43
+ """
44
+ Train an Indico model
45
+ Args:
46
+ dataset (Dataset): A dataset object (should represent an uploaded CSV
47
+ dataset)
48
+ workflow (Workflow): A workflow object to the corresponding dataset
49
+ model_name (str): the name for your model
50
+ source_col (str): the csv column that contained the text
51
+ target_col (str): the csv column that contained the labels
52
+ after_component_id (int, optional): The workflow component that precedes
53
+ this model group. If None, will be set programmatically to the id of the
54
+ Input OCR Extraction component. Defaults to None.
55
+ wait (bool, optional): Wait for the model to finish training.
56
+ Defaults to False.
57
+
58
+ Returns:
59
+ ModelGroup: Model group object
60
+ """
61
+ if not after_component_id:
62
+ after_component_id = workflow.component_by_type("INPUT_OCR_EXTRACTION").id
63
+ return self.client.call(
64
+ AddModelGroupComponent(
65
+ name=model_name,
66
+ dataset_id=dataset.id,
67
+ source_column_id=dataset.datacolumn_by_name(source_col).id,
68
+ labelset_column_id=dataset.labelset_by_name(target_col).id,
69
+ workflow_id=workflow.id,
70
+ after_component_id=after_component_id,
71
+ )
72
+ )
73
+
74
+ @retry(IndicoRequestError, ConnectionError)
75
+ def get_storage_object(self, storage_url: str):
76
+ return self.client.call(RetrieveStorageObject(storage_url))
77
+
78
+ def create_storage_urls(self, file_paths: List[str]) -> List[str]:
79
+ return self.client.call(CreateStorageURLs(files=file_paths))
80
+
81
+ def get_job_status(self, job_id: int, wait: bool = True, timeout: float = None):
82
+ return self.client.call(JobStatus(id=job_id, wait=wait, timeout=timeout))
83
+
84
+ @retry(IndicoRequestError, ConnectionError)
85
+ def graphQL_request(self, graphql_query: str, variables: dict = None):
86
+ return self.client.call(
87
+ GraphQLRequest(query=graphql_query, variables=variables)
88
+ )
89
+
90
+ def get_predictions_with_model_id(
91
+ self,
92
+ model_id: int,
93
+ samples: List[str],
94
+ load: bool = False,
95
+ options: dict = None,
96
+ wait: bool = True,
97
+ ) -> Union[int, List[Predictions]]:
98
+ """
99
+ Submit samples directly to a model. Note: documents must already be in raw text.
100
+ Args:
101
+ model_id (int): The model ID to submit to
102
+ samples (List[str]): A list containing the text samples you want to submit
103
+ load (bool, optional): [deprecated] Always set to False. Defaults to False.
104
+ options (dict, optional): Model Prediction options. Defaults to None.
105
+ wait (bool, optional): Wait for predictions to finish. Defaults to True.
106
+
107
+ Returns: if wait is False, returns the job ID, else returns a list of
108
+ Predictions where each Predictions is either type Classifications or
109
+ Extractions depending on your model.
110
+ """
111
+ job = self.client.call(ModelGroupPredict(model_id, samples, load, options))
112
+ if not wait:
113
+ return job.id
114
+ status = self.get_job_status(job.id, wait=True)
115
+ if status.status != "SUCCESS":
116
+ raise ToolkitStatusError(
117
+ f"Predictions Failed, {status.status}: {status.result}"
118
+ )
119
+ return [Predictions.get_obj(i) for i in status.result]
@@ -0,0 +1,200 @@
1
+ import json
2
+ from typing import Iterator
3
+
4
+ from indico import IndicoClient
5
+ from indico.queries import GraphQLRequest
6
+
7
+ # valid model option parameters
8
+ TEXT_EXTRACTION_PARAMS = {
9
+ "max_empty_chunk_ratio": lambda value: 0 <= value <= 1.0e5,
10
+ "auto_negative_scaling": [True, False],
11
+ "optimize_for": [
12
+ "predict_speed",
13
+ "accuracy",
14
+ "speed",
15
+ "accuracy_fp16",
16
+ "predict_speed_fp16",
17
+ ],
18
+ "subtoken_predictions": [True, False],
19
+ "base_model": [
20
+ "roberta",
21
+ "small",
22
+ "multilingual",
23
+ "fast",
24
+ "textcnn",
25
+ "fasttextcnn",
26
+ ],
27
+ "class_weight": ["linear", "sqrt", "log", None],
28
+ }
29
+
30
+ TEXT_CLASSIFICATION_PARAMS = {
31
+ "model_type": ["tfidf_lr", "tfidf_gbt", "standard", "finetune"]
32
+ }
33
+
34
+
35
+ class ModelOp:
36
+ """
37
+ Class to assit in Model Group Operation calls.
38
+ """
39
+
40
+ def __init__(self, client: IndicoClient):
41
+ self.client = client
42
+
43
+ def get_model_options(
44
+ self, model_group_id: int, model_id: int | None = None
45
+ ) -> dict[str, object]:
46
+ """
47
+ Return model options for `model_id` of `model_group_id`
48
+ or the most recent model if `model_id` is not specified.
49
+ Args:
50
+ model_group_id (int): id of model group
51
+ model_id (int, optional): argument to return a specific model within a model
52
+ group
53
+ """
54
+ all_model_options = self.get_all_model_options(model_group_id)
55
+
56
+ if model_id is None:
57
+ return next(all_model_options)
58
+
59
+ for model_options in all_model_options:
60
+ if model_options["id"] == model_id:
61
+ return model_options
62
+ else:
63
+ raise RuntimeError(
64
+ f"Model group {model_group_id} does not have a model with ID {model_id}"
65
+ )
66
+
67
+ def get_all_model_options(self, model_group_id: int) -> Iterator[dict[str, object]]:
68
+ """
69
+ Return model options for all models of `model_group_id`.
70
+ Args:
71
+ model_group_id (int): id of model group
72
+ Returns:
73
+ dict: dictionary of all model options, including:
74
+ "id", "domain, ""high_quality", "interlabeler_resolution",
75
+ "sampling_strategy", "seed", "test_split", "weight_by_class_frequency",
76
+ "word_predictor_strength", and "model_training_options"
77
+ """
78
+
79
+ response = self.client.call(
80
+ GraphQLRequest(
81
+ """
82
+ query getMoonbowModelGroupModels($modelGroupId: Int!) {
83
+ modelGroup(modelGroupId: $modelGroupId) {
84
+ id
85
+ models {
86
+ id
87
+ modelOptions
88
+ }
89
+ }
90
+ }
91
+ """,
92
+ {"modelGroupId": model_group_id},
93
+ )
94
+ )
95
+
96
+ for model in response["modelGroup"]["models"]:
97
+ model_options = json.loads(model["modelOptions"])
98
+ model_options["id"] = model["id"]
99
+ yield model_options
100
+
101
+ def retrain_model(self, model_group_id: int) -> dict:
102
+ """
103
+ Forces model retrain.
104
+ Args:
105
+ model_group_id (int): id of model group needing retraining
106
+ Returns:
107
+ dict: returns model group id and status of retraining model
108
+ """
109
+ return self.client.call(
110
+ GraphQLRequest(
111
+ """
112
+ mutation retrainMoonbowModelGroup($modelGroupId: Int!) {
113
+ retrainModelGroup(modelGroupId: $modelGroupId, forceRetrain: true) {
114
+ id
115
+ status
116
+ }
117
+ }
118
+ """,
119
+ {"modelGroupId": model_group_id},
120
+ )
121
+ )
122
+
123
+ def update_model_settings(
124
+ self, model_group_id: int, model_type: str, **model_training_options
125
+ ) -> dict[str, object]:
126
+ """
127
+ Update group model settings based on specified model training options.
128
+
129
+ Args:
130
+ model_group_id (int): id of model group
131
+ model_type (str): type of model ("text_extraction", "text_classification")
132
+ **kwargs: Advanced Model Training Options
133
+
134
+ Default values of Advanced Model Training Options and avaiable parameters:
135
+ For Text Extraction Model:
136
+ max_empty_chunk_ratio : 1.0 (min of 0, no max value: a large
137
+ number effectivly turns this option off)
138
+ auto_negative_scaling : True
139
+ optimize_for : "predict_speed" ( "predict_speed", "accuracy",
140
+ "speed", "accuracy_fp16" and "predict_speed_fp16")
141
+ subtoken_predictions : True
142
+ base_model : "roberta" ("roberta", "small" (distilled version of
143
+ RoBERTa), "multilingual", "fast", "textcnn", "fasttextcnn")
144
+ class_weight : "sqrt" ("linear", "sqrt", "log", None)
145
+
146
+ For Text Classification Model:
147
+ model_type : "standard" ("tfidf_lr", "tfidf_gbt", "standard",
148
+ "finetune")
149
+ Returns:
150
+ dict: Dictionary of advanced model training options
151
+ """
152
+ model = self.client.call(
153
+ GraphQLRequest(
154
+ """
155
+ mutation updateModelGroup(
156
+ $modelGroupId: Int!,
157
+ $modelTrainingOptions: JSONString
158
+ ) {
159
+ updateModelGroupSettings(
160
+ modelGroupId: $modelGroupId
161
+ modelTrainingOptions: $modelTrainingOptions
162
+ ) {
163
+ modelOptions {
164
+ id
165
+ modelTrainingOptions
166
+ }
167
+ }
168
+ }
169
+ """,
170
+ {
171
+ "modelGroupId": model_group_id,
172
+ "modelTrainingOptions": json.dumps(
173
+ self._parameter_check(model_type, **model_training_options)
174
+ ),
175
+ },
176
+ )
177
+ )
178
+ options = json.loads(
179
+ model["updateModelGroupSettings"]["modelOptions"]["modelTrainingOptions"]
180
+ )
181
+ options["id"] = model["updateModelGroupSettings"]["modelOptions"]["id"]
182
+ return options
183
+
184
+ def _parameter_check(self, model_type, **model_training_options):
185
+ if model_type == "text_extraction":
186
+ valid_params = TEXT_EXTRACTION_PARAMS
187
+ elif model_type == "text_classification":
188
+ valid_params = TEXT_CLASSIFICATION_PARAMS
189
+ else:
190
+ raise ValueError(f"Invalid model type: {model_type}")
191
+
192
+ for param, value in model_training_options.items():
193
+ if param not in valid_params:
194
+ raise ValueError(f"Invalid parameter for {model_type}: {param}")
195
+ if callable(valid_params[param]) and not valid_params[param](value):
196
+ raise ValueError(f"Invalid value for parameter '{param}': {value}")
197
+ if value not in valid_params[param]:
198
+ raise ValueError(f"Invalid value for parameter '{param}': {value}")
199
+
200
+ return model_training_options
@@ -0,0 +1,111 @@
1
+ import json
2
+
3
+ from indico import IndicoClient
4
+
5
+ from .indico_wrapper import Workflow
6
+
7
+
8
+ class Reviewer(Workflow):
9
+ """
10
+ Class to simulate human reviewer
11
+ """
12
+
13
+ def __init__(
14
+ self,
15
+ client: IndicoClient,
16
+ workflow_id: int,
17
+ ):
18
+ self.client = client
19
+ self.workflow_id = workflow_id
20
+
21
+ def accept_review(self, submission_id: int, changes: dict) -> None:
22
+ """
23
+ Accept a submission in the review queue
24
+ Args:
25
+ submission_id (int): submission ID
26
+ changes (dict): accepted predictions with format like,
27
+ e.g. {"model_name": [{"label"...}]}
28
+ """
29
+ self.graphQL_request(
30
+ SUBMIT_REVIEW,
31
+ {
32
+ "rejected": False,
33
+ "submissionId": submission_id,
34
+ "changes": json.dumps(changes),
35
+ },
36
+ )
37
+
38
+ def get_random_review_id(self):
39
+ response = self.graphQL_request(
40
+ GET_RANDOM_REVIEW_ID, {"workflowId": self.workflow_id}
41
+ )
42
+ try:
43
+ return response["randomSubmission"]["id"]
44
+ except Exception:
45
+ raise RuntimeError("The review queue is empty")
46
+
47
+ def get_random_exception_id(self):
48
+ response = self.graphQL_request(
49
+ GET_RANDOM_EXCEPTION_ID, {"workflowId": self.workflow_id}
50
+ )
51
+ try:
52
+ return response["randomSubmission"]["id"]
53
+ except Exception:
54
+ raise RuntimeError("The exception queue is empty")
55
+
56
+ def reject_submission(self, submission_id):
57
+ return self.graphQL_request(
58
+ SUBMIT_REVIEW, {"rejected": True, "submissionId": submission_id}
59
+ )
60
+
61
+
62
+ SUBMIT_REVIEW = """
63
+ mutation submitStandardQueue(
64
+ $changes: JSONString,
65
+ $rejected: Boolean,
66
+ $submissionId: Int!,
67
+ $notes: String
68
+ ) {
69
+ submitReview(
70
+ changes: $changes,
71
+ rejected: $rejected,
72
+ submissionId: $submissionId,
73
+ notes: $notes
74
+ ) {
75
+ id
76
+ __typename
77
+ }
78
+ }
79
+ """
80
+
81
+ GET_RANDOM_EXCEPTION_ID = """
82
+ query getExceptionsSubmission($workflowId: Int!) {
83
+ randomSubmission(adminReview: true, workflowId: $workflowId) {
84
+ id
85
+ resultFile
86
+ inputFilename
87
+ autoReview {
88
+ id
89
+ changes
90
+ __typename
91
+ }
92
+ __typename
93
+ }
94
+ }
95
+ """
96
+
97
+ GET_RANDOM_REVIEW_ID = """
98
+ query getSubmission($workflowId: Int!) {
99
+ randomSubmission(adminReview: false, workflowId: $workflowId) {
100
+ id
101
+ resultFile
102
+ inputFilename
103
+ autoReview {
104
+ id
105
+ changes
106
+ __typename
107
+ }
108
+ __typename
109
+ }
110
+ }
111
+ """