toil 9.1.1__py3-none-any.whl → 9.2.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.
- toil/__init__.py +5 -9
- toil/batchSystems/abstractBatchSystem.py +23 -22
- toil/batchSystems/abstractGridEngineBatchSystem.py +17 -12
- toil/batchSystems/awsBatch.py +8 -8
- toil/batchSystems/cleanup_support.py +4 -4
- toil/batchSystems/contained_executor.py +3 -3
- toil/batchSystems/gridengine.py +3 -4
- toil/batchSystems/htcondor.py +5 -5
- toil/batchSystems/kubernetes.py +65 -63
- toil/batchSystems/local_support.py +2 -3
- toil/batchSystems/lsf.py +6 -7
- toil/batchSystems/mesos/batchSystem.py +11 -7
- toil/batchSystems/mesos/test/__init__.py +1 -2
- toil/batchSystems/options.py +9 -10
- toil/batchSystems/registry.py +3 -7
- toil/batchSystems/singleMachine.py +8 -11
- toil/batchSystems/slurm.py +49 -38
- toil/batchSystems/torque.py +3 -4
- toil/bus.py +36 -34
- toil/common.py +129 -89
- toil/cwl/cwltoil.py +857 -729
- toil/cwl/utils.py +44 -35
- toil/fileStores/__init__.py +3 -1
- toil/fileStores/abstractFileStore.py +28 -30
- toil/fileStores/cachingFileStore.py +8 -8
- toil/fileStores/nonCachingFileStore.py +10 -21
- toil/job.py +159 -158
- toil/jobStores/abstractJobStore.py +68 -69
- toil/jobStores/aws/jobStore.py +249 -213
- toil/jobStores/aws/utils.py +13 -24
- toil/jobStores/fileJobStore.py +28 -22
- toil/jobStores/googleJobStore.py +21 -17
- toil/jobStores/utils.py +3 -7
- toil/leader.py +17 -22
- toil/lib/accelerators.py +6 -4
- toil/lib/aws/__init__.py +9 -10
- toil/lib/aws/ami.py +33 -19
- toil/lib/aws/iam.py +6 -6
- toil/lib/aws/s3.py +259 -157
- toil/lib/aws/session.py +76 -76
- toil/lib/aws/utils.py +51 -43
- toil/lib/checksum.py +19 -15
- toil/lib/compatibility.py +3 -2
- toil/lib/conversions.py +45 -18
- toil/lib/directory.py +29 -26
- toil/lib/docker.py +93 -99
- toil/lib/dockstore.py +77 -50
- toil/lib/ec2.py +39 -38
- toil/lib/ec2nodes.py +11 -4
- toil/lib/exceptions.py +8 -5
- toil/lib/ftp_utils.py +9 -14
- toil/lib/generatedEC2Lists.py +161 -20
- toil/lib/history.py +141 -97
- toil/lib/history_submission.py +163 -72
- toil/lib/io.py +27 -17
- toil/lib/memoize.py +2 -1
- toil/lib/misc.py +15 -11
- toil/lib/pipes.py +40 -25
- toil/lib/plugins.py +12 -8
- toil/lib/resources.py +1 -0
- toil/lib/retry.py +32 -38
- toil/lib/threading.py +12 -12
- toil/lib/throttle.py +1 -2
- toil/lib/trs.py +113 -51
- toil/lib/url.py +14 -23
- toil/lib/web.py +7 -2
- toil/options/common.py +18 -15
- toil/options/cwl.py +2 -2
- toil/options/runner.py +9 -5
- toil/options/wdl.py +1 -3
- toil/provisioners/__init__.py +9 -9
- toil/provisioners/abstractProvisioner.py +22 -20
- toil/provisioners/aws/__init__.py +20 -14
- toil/provisioners/aws/awsProvisioner.py +10 -8
- toil/provisioners/clusterScaler.py +19 -18
- toil/provisioners/gceProvisioner.py +2 -3
- toil/provisioners/node.py +11 -13
- toil/realtimeLogger.py +4 -4
- toil/resource.py +5 -5
- toil/server/app.py +2 -2
- toil/server/cli/wes_cwl_runner.py +11 -11
- toil/server/utils.py +18 -21
- toil/server/wes/abstract_backend.py +9 -8
- toil/server/wes/amazon_wes_utils.py +3 -3
- toil/server/wes/tasks.py +3 -5
- toil/server/wes/toil_backend.py +17 -21
- toil/server/wsgi_app.py +3 -3
- toil/serviceManager.py +3 -4
- toil/statsAndLogging.py +12 -13
- toil/test/__init__.py +33 -24
- toil/test/batchSystems/batchSystemTest.py +12 -11
- toil/test/batchSystems/batch_system_plugin_test.py +3 -5
- toil/test/batchSystems/test_slurm.py +38 -24
- toil/test/cwl/conftest.py +5 -6
- toil/test/cwl/cwlTest.py +194 -78
- toil/test/cwl/download_file_uri.json +6 -0
- toil/test/cwl/download_file_uri_no_hostname.json +6 -0
- toil/test/docs/scripts/tutorial_staging.py +1 -0
- toil/test/jobStores/jobStoreTest.py +9 -7
- toil/test/lib/aws/test_iam.py +1 -3
- toil/test/lib/aws/test_s3.py +1 -1
- toil/test/lib/dockerTest.py +9 -9
- toil/test/lib/test_ec2.py +12 -11
- toil/test/lib/test_history.py +4 -4
- toil/test/lib/test_trs.py +16 -14
- toil/test/lib/test_url.py +7 -6
- toil/test/lib/url_plugin_test.py +12 -18
- toil/test/provisioners/aws/awsProvisionerTest.py +10 -8
- toil/test/provisioners/clusterScalerTest.py +2 -5
- toil/test/provisioners/clusterTest.py +1 -3
- toil/test/server/serverTest.py +13 -4
- toil/test/sort/restart_sort.py +2 -6
- toil/test/sort/sort.py +3 -8
- toil/test/src/deferredFunctionTest.py +7 -7
- toil/test/src/environmentTest.py +1 -2
- toil/test/src/fileStoreTest.py +5 -5
- toil/test/src/importExportFileTest.py +5 -6
- toil/test/src/jobServiceTest.py +22 -14
- toil/test/src/jobTest.py +121 -25
- toil/test/src/miscTests.py +5 -7
- toil/test/src/promisedRequirementTest.py +8 -7
- toil/test/src/regularLogTest.py +2 -3
- toil/test/src/resourceTest.py +5 -8
- toil/test/src/restartDAGTest.py +5 -6
- toil/test/src/resumabilityTest.py +2 -2
- toil/test/src/retainTempDirTest.py +3 -3
- toil/test/src/systemTest.py +3 -3
- toil/test/src/threadingTest.py +1 -1
- toil/test/src/workerTest.py +1 -2
- toil/test/utils/toilDebugTest.py +6 -4
- toil/test/utils/toilKillTest.py +1 -1
- toil/test/utils/utilsTest.py +15 -14
- toil/test/wdl/wdltoil_test.py +247 -124
- toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
- toil/toilState.py +2 -3
- toil/utils/toilDebugFile.py +3 -8
- toil/utils/toilDebugJob.py +1 -2
- toil/utils/toilLaunchCluster.py +1 -2
- toil/utils/toilSshCluster.py +2 -0
- toil/utils/toilStats.py +19 -24
- toil/utils/toilStatus.py +11 -14
- toil/version.py +10 -10
- toil/wdl/wdltoil.py +313 -209
- toil/worker.py +18 -12
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/METADATA +11 -14
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/RECORD +150 -153
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/WHEEL +1 -1
- toil/test/cwl/staging_cat.cwl +0 -27
- toil/test/cwl/staging_make_file.cwl +0 -25
- toil/test/cwl/staging_workflow.cwl +0 -43
- toil/test/cwl/zero_default.cwl +0 -61
- toil/test/utils/ABCWorkflowDebug/ABC.txt +0 -1
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/entry_points.txt +0 -0
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/licenses/LICENSE +0 -0
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/top_level.txt +0 -0
toil/lib/history_submission.py
CHANGED
|
@@ -23,29 +23,24 @@ import io
|
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
25
|
import multiprocessing
|
|
26
|
-
import os
|
|
27
|
-
import pathlib
|
|
28
|
-
import subprocess
|
|
29
26
|
import sys
|
|
30
27
|
import textwrap
|
|
31
|
-
from typing import
|
|
28
|
+
from typing import Literal, Optional, TypeVar, Union
|
|
32
29
|
|
|
33
30
|
from toil.lib.dockstore import (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
pack_workflow_metrics,
|
|
31
|
+
RunExecution,
|
|
32
|
+
TaskExecutions,
|
|
37
33
|
pack_single_task_metrics,
|
|
34
|
+
pack_workflow_metrics,
|
|
38
35
|
pack_workflow_task_set_metrics,
|
|
39
|
-
|
|
40
|
-
TaskExecutions
|
|
36
|
+
send_metrics,
|
|
41
37
|
)
|
|
42
|
-
from toil.lib.history import HistoryManager,
|
|
43
|
-
from toil.lib.misc import unix_seconds_to_local_time
|
|
38
|
+
from toil.lib.history import HistoryManager, JobAttemptSummary, WorkflowAttemptSummary
|
|
44
39
|
from toil.lib.trs import parse_trs_spec
|
|
45
|
-
from toil.lib.io import get_toil_home
|
|
46
40
|
|
|
47
41
|
logger = logging.getLogger(__name__)
|
|
48
42
|
|
|
43
|
+
|
|
49
44
|
def workflow_execution_id(workflow_attempt: WorkflowAttemptSummary) -> str:
|
|
50
45
|
"""
|
|
51
46
|
Get the execution ID for a workflow attempt.
|
|
@@ -55,7 +50,12 @@ def workflow_execution_id(workflow_attempt: WorkflowAttemptSummary) -> str:
|
|
|
55
50
|
Deterministic.
|
|
56
51
|
"""
|
|
57
52
|
|
|
58
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
workflow_attempt.workflow_id.replace("-", "_")
|
|
55
|
+
+ "_attempt_"
|
|
56
|
+
+ str(workflow_attempt.attempt_number)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
59
|
|
|
60
60
|
def workflow_task_set_execution_id(workflow_attempt: WorkflowAttemptSummary) -> str:
|
|
61
61
|
"""
|
|
@@ -68,6 +68,7 @@ def workflow_task_set_execution_id(workflow_attempt: WorkflowAttemptSummary) ->
|
|
|
68
68
|
|
|
69
69
|
return workflow_execution_id(workflow_attempt) + "_tasks"
|
|
70
70
|
|
|
71
|
+
|
|
71
72
|
def job_execution_id(job_attempt: JobAttemptSummary) -> str:
|
|
72
73
|
"""
|
|
73
74
|
Get the execution ID for a job attempt.
|
|
@@ -79,6 +80,7 @@ def job_execution_id(job_attempt: JobAttemptSummary) -> str:
|
|
|
79
80
|
|
|
80
81
|
return job_attempt.id.replace("-", "_")
|
|
81
82
|
|
|
83
|
+
|
|
82
84
|
def get_parsed_trs_spec(workflow_attempt: WorkflowAttemptSummary) -> tuple[str, str]:
|
|
83
85
|
"""
|
|
84
86
|
Get the TRS ID and version of the workflow, or raise an error.
|
|
@@ -92,9 +94,12 @@ def get_parsed_trs_spec(workflow_attempt: WorkflowAttemptSummary) -> tuple[str,
|
|
|
92
94
|
raise ValueError("Workflow cannot be submitted without a TRS spec")
|
|
93
95
|
trs_id, trs_version = parse_trs_spec(workflow_attempt.workflow_trs_spec)
|
|
94
96
|
if trs_version is None:
|
|
95
|
-
raise ValueError(
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Workflow stored in history with TRS ID but without TRS version"
|
|
99
|
+
)
|
|
96
100
|
return trs_id, trs_version
|
|
97
101
|
|
|
102
|
+
|
|
98
103
|
class Submission:
|
|
99
104
|
"""
|
|
100
105
|
Class holding a package of information to submit to Dockstore, and the
|
|
@@ -111,8 +116,13 @@ class Submission:
|
|
|
111
116
|
|
|
112
117
|
# This collects everything by TRS ID and version, and keeps separate lists for workflows and for task sets.
|
|
113
118
|
# Each thing to be submitted comes along with the reference to what in the database to makr submitted when it goes in.
|
|
114
|
-
self.data: dict[
|
|
115
|
-
|
|
119
|
+
self.data: dict[
|
|
120
|
+
tuple[str, str],
|
|
121
|
+
tuple[
|
|
122
|
+
list[tuple[RunExecution, str, int]],
|
|
123
|
+
list[tuple[TaskExecutions, list[str]]],
|
|
124
|
+
],
|
|
125
|
+
] = collections.defaultdict(lambda: ([], []))
|
|
116
126
|
|
|
117
127
|
def add_workflow_attempt(self, workflow_attempt: WorkflowAttemptSummary) -> None:
|
|
118
128
|
"""
|
|
@@ -123,9 +133,10 @@ class Submission:
|
|
|
123
133
|
trs_id, trs_version = get_parsed_trs_spec(workflow_attempt)
|
|
124
134
|
|
|
125
135
|
# Figure out what kind of job store was used.
|
|
126
|
-
job_store_type:
|
|
136
|
+
job_store_type: str | None = None
|
|
127
137
|
try:
|
|
128
138
|
from toil.common import Toil
|
|
139
|
+
|
|
129
140
|
job_store_type = Toil.parseLocator(workflow_attempt.workflow_job_store)[0]
|
|
130
141
|
if job_store_type not in Toil.JOB_STORE_TYPES:
|
|
131
142
|
# Make sure we don't send typo'd job store types in.
|
|
@@ -146,13 +157,23 @@ class Submission:
|
|
|
146
157
|
python_version=workflow_attempt.python_version,
|
|
147
158
|
platform_system=workflow_attempt.platform_system,
|
|
148
159
|
platform_machine=workflow_attempt.platform_machine,
|
|
149
|
-
job_store_type=job_store_type
|
|
160
|
+
job_store_type=job_store_type,
|
|
150
161
|
)
|
|
151
162
|
|
|
152
163
|
# Add it to the package
|
|
153
|
-
self.data[(trs_id, trs_version)][0].append(
|
|
164
|
+
self.data[(trs_id, trs_version)][0].append(
|
|
165
|
+
(
|
|
166
|
+
workflow_metrics,
|
|
167
|
+
workflow_attempt.workflow_id,
|
|
168
|
+
workflow_attempt.attempt_number,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
154
171
|
|
|
155
|
-
def add_job_attempts(
|
|
172
|
+
def add_job_attempts(
|
|
173
|
+
self,
|
|
174
|
+
workflow_attempt: WorkflowAttemptSummary,
|
|
175
|
+
job_attempts: list[JobAttemptSummary],
|
|
176
|
+
) -> None:
|
|
156
177
|
"""
|
|
157
178
|
Add the job attempts for a workflow attempt to the submission.
|
|
158
179
|
"""
|
|
@@ -170,13 +191,20 @@ class Submission:
|
|
|
170
191
|
cores=job_attempt.cores,
|
|
171
192
|
cpu_seconds=job_attempt.cpu_seconds,
|
|
172
193
|
memory_bytes=job_attempt.memory_bytes,
|
|
173
|
-
disk_bytes=job_attempt.disk_bytes
|
|
174
|
-
)
|
|
194
|
+
disk_bytes=job_attempt.disk_bytes,
|
|
195
|
+
)
|
|
196
|
+
for job_attempt in job_attempts
|
|
175
197
|
]
|
|
176
|
-
task_set_metrics = pack_workflow_task_set_metrics(
|
|
198
|
+
task_set_metrics = pack_workflow_task_set_metrics(
|
|
199
|
+
workflow_task_set_execution_id(workflow_attempt),
|
|
200
|
+
workflow_attempt.start_time,
|
|
201
|
+
per_task_metrics,
|
|
202
|
+
)
|
|
177
203
|
|
|
178
204
|
# Add it to the package
|
|
179
|
-
self.data[(trs_id, trs_version)][1].append(
|
|
205
|
+
self.data[(trs_id, trs_version)][1].append(
|
|
206
|
+
(task_set_metrics, [job_attempt.id for job_attempt in job_attempts])
|
|
207
|
+
)
|
|
180
208
|
|
|
181
209
|
def empty(self) -> bool:
|
|
182
210
|
"""
|
|
@@ -200,16 +228,23 @@ class Submission:
|
|
|
200
228
|
|
|
201
229
|
stream = io.StringIO()
|
|
202
230
|
|
|
203
|
-
for (trs_id, trs_version), (
|
|
231
|
+
for (trs_id, trs_version), (
|
|
232
|
+
workflow_metrics,
|
|
233
|
+
task_metrics,
|
|
234
|
+
) in self.data.items():
|
|
204
235
|
stream.write(f"For workflow {trs_id} version {trs_version}:\n")
|
|
205
236
|
if len(workflow_metrics) > 0:
|
|
206
237
|
stream.write(" Workflow executions:\n")
|
|
207
238
|
for workflow_metric, _, _ in workflow_metrics:
|
|
208
|
-
stream.write(
|
|
239
|
+
stream.write(
|
|
240
|
+
textwrap.indent(json.dumps(workflow_metric, indent=2), " ")
|
|
241
|
+
)
|
|
209
242
|
if len(task_metrics) > 0:
|
|
210
243
|
stream.write(" Task execution sets:\n")
|
|
211
244
|
for tasks_metric, _ in task_metrics:
|
|
212
|
-
stream.write(
|
|
245
|
+
stream.write(
|
|
246
|
+
textwrap.indent(json.dumps(tasks_metric, indent=2), " ")
|
|
247
|
+
)
|
|
213
248
|
|
|
214
249
|
return stream.getvalue()
|
|
215
250
|
|
|
@@ -228,10 +263,18 @@ class Submission:
|
|
|
228
263
|
|
|
229
264
|
all_submitted_and_marked = True
|
|
230
265
|
|
|
231
|
-
for (trs_id, trs_version), (
|
|
266
|
+
for (trs_id, trs_version), (
|
|
267
|
+
workflow_metrics,
|
|
268
|
+
task_metrics,
|
|
269
|
+
) in self.data.items():
|
|
232
270
|
submitted = False
|
|
233
271
|
try:
|
|
234
|
-
send_metrics(
|
|
272
|
+
send_metrics(
|
|
273
|
+
trs_id,
|
|
274
|
+
trs_version,
|
|
275
|
+
[item[0] for item in workflow_metrics],
|
|
276
|
+
[item[0] for item in task_metrics],
|
|
277
|
+
)
|
|
235
278
|
submitted = True
|
|
236
279
|
except:
|
|
237
280
|
logger.exception("Could not submit to Dockstore")
|
|
@@ -242,7 +285,9 @@ class Submission:
|
|
|
242
285
|
for _, workflow_id, attempt_number in workflow_metrics:
|
|
243
286
|
# For each workflow attempt of this TRS ID/version that we successfully submitted, mark it
|
|
244
287
|
try:
|
|
245
|
-
HistoryManager.mark_workflow_attempt_submitted(
|
|
288
|
+
HistoryManager.mark_workflow_attempt_submitted(
|
|
289
|
+
workflow_id, attempt_number
|
|
290
|
+
)
|
|
246
291
|
except:
|
|
247
292
|
logger.exception("Could not mark workflow attempt submitted")
|
|
248
293
|
all_submitted_and_marked = False
|
|
@@ -259,7 +304,9 @@ class Submission:
|
|
|
259
304
|
return all_submitted_and_marked
|
|
260
305
|
|
|
261
306
|
|
|
262
|
-
def create_history_submission(
|
|
307
|
+
def create_history_submission(
|
|
308
|
+
batch_size: int | None = None, desired_tasks: int | None = None
|
|
309
|
+
) -> Submission:
|
|
263
310
|
"""
|
|
264
311
|
Make a package of data about recent workflow runs to send in.
|
|
265
312
|
|
|
@@ -284,7 +331,9 @@ def create_history_submission(batch_size: Optional[int] = None, desired_tasks: O
|
|
|
284
331
|
# count how many tasks we've colected so far.
|
|
285
332
|
total_tasks = 0
|
|
286
333
|
|
|
287
|
-
for workflow_attempt in HistoryManager.get_submittable_workflow_attempts(
|
|
334
|
+
for workflow_attempt in HistoryManager.get_submittable_workflow_attempts(
|
|
335
|
+
limit=batch_size
|
|
336
|
+
):
|
|
288
337
|
try:
|
|
289
338
|
submission.add_workflow_attempt(workflow_attempt)
|
|
290
339
|
except:
|
|
@@ -303,8 +352,14 @@ def create_history_submission(batch_size: Optional[int] = None, desired_tasks: O
|
|
|
303
352
|
# <https://ucsc-cgl.atlassian.net/browse/SEAB-6919>), set the default task
|
|
304
353
|
# limit to submit to be nonzero.
|
|
305
354
|
|
|
306
|
-
for
|
|
307
|
-
|
|
355
|
+
for (
|
|
356
|
+
workflow_attempt
|
|
357
|
+
) in HistoryManager.get_workflow_attempts_with_submittable_job_attempts(
|
|
358
|
+
limit=batch_size
|
|
359
|
+
):
|
|
360
|
+
job_attempts = HistoryManager.get_unsubmitted_job_attempts(
|
|
361
|
+
workflow_attempt.workflow_id, workflow_attempt.attempt_number
|
|
362
|
+
)
|
|
308
363
|
|
|
309
364
|
if desired_tasks == 0 or total_tasks + len(job_attempts) > desired_tasks:
|
|
310
365
|
# Don't add any more task sets to the submission
|
|
@@ -321,6 +376,7 @@ def create_history_submission(batch_size: Optional[int] = None, desired_tasks: O
|
|
|
321
376
|
|
|
322
377
|
return submission
|
|
323
378
|
|
|
379
|
+
|
|
324
380
|
def create_current_submission(workflow_id: str, attempt_number: int) -> Submission:
|
|
325
381
|
"""
|
|
326
382
|
Make a package of data about the current workflow attempt to send in.
|
|
@@ -330,16 +386,22 @@ def create_current_submission(workflow_id: str, attempt_number: int) -> Submissi
|
|
|
330
386
|
|
|
331
387
|
submission = Submission()
|
|
332
388
|
try:
|
|
333
|
-
workflow_attempt = HistoryManager.get_workflow_attempt(
|
|
389
|
+
workflow_attempt = HistoryManager.get_workflow_attempt(
|
|
390
|
+
workflow_id, attempt_number
|
|
391
|
+
)
|
|
334
392
|
if workflow_attempt is not None and HistoryManager.enabled():
|
|
335
393
|
if not workflow_attempt.submitted_to_dockstore:
|
|
336
394
|
submission.add_workflow_attempt(workflow_attempt)
|
|
337
395
|
if HistoryManager.enabled_job():
|
|
338
396
|
try:
|
|
339
|
-
job_attempts = HistoryManager.get_unsubmitted_job_attempts(
|
|
397
|
+
job_attempts = HistoryManager.get_unsubmitted_job_attempts(
|
|
398
|
+
workflow_attempt.workflow_id, workflow_attempt.attempt_number
|
|
399
|
+
)
|
|
340
400
|
submission.add_job_attempts(workflow_attempt, job_attempts)
|
|
341
401
|
except:
|
|
342
|
-
logger.exception(
|
|
402
|
+
logger.exception(
|
|
403
|
+
"Could not compose metrics report for workflow task set"
|
|
404
|
+
)
|
|
343
405
|
# Keep going with just the workflow.
|
|
344
406
|
except:
|
|
345
407
|
logger.exception("Could not compose metrics report for workflow execution")
|
|
@@ -347,10 +409,14 @@ def create_current_submission(workflow_id: str, attempt_number: int) -> Submissi
|
|
|
347
409
|
|
|
348
410
|
return submission
|
|
349
411
|
|
|
412
|
+
|
|
350
413
|
# We have dialog functions that MyPy knows can return strings from a possibly restricted set
|
|
351
|
-
KeyType = TypeVar(
|
|
414
|
+
KeyType = TypeVar("KeyType", bound=str)
|
|
352
415
|
|
|
353
|
-
|
|
416
|
+
|
|
417
|
+
def dialog_tkinter(
|
|
418
|
+
title: str, text: str, options: dict[KeyType, str], timeout: float
|
|
419
|
+
) -> KeyType | None:
|
|
354
420
|
"""
|
|
355
421
|
Display a dialog with tkinter.
|
|
356
422
|
|
|
@@ -366,8 +432,13 @@ def dialog_tkinter(title: str, text: str, options: dict[KeyType, str], timeout:
|
|
|
366
432
|
"""
|
|
367
433
|
|
|
368
434
|
# Multiprocessing queues aren't actually generic, but MyPy requires them to be.
|
|
369
|
-
result_queue: "multiprocessing.Queue[Union[Exception, Optional[KeyType]]]" =
|
|
370
|
-
|
|
435
|
+
result_queue: "multiprocessing.Queue[Union[Exception, Optional[KeyType]]]" = (
|
|
436
|
+
multiprocessing.Queue()
|
|
437
|
+
)
|
|
438
|
+
process = multiprocessing.Process(
|
|
439
|
+
target=display_dialog_tkinter,
|
|
440
|
+
args=(title, text, options, timeout, result_queue),
|
|
441
|
+
)
|
|
371
442
|
process.start()
|
|
372
443
|
result = result_queue.get()
|
|
373
444
|
process.join()
|
|
@@ -378,8 +449,13 @@ def dialog_tkinter(title: str, text: str, options: dict[KeyType, str], timeout:
|
|
|
378
449
|
return result
|
|
379
450
|
|
|
380
451
|
|
|
381
|
-
|
|
382
|
-
|
|
452
|
+
def display_dialog_tkinter(
|
|
453
|
+
title: str,
|
|
454
|
+
text: str,
|
|
455
|
+
options: dict[KeyType, str],
|
|
456
|
+
timeout: float,
|
|
457
|
+
result_queue: "multiprocessing.Queue[Union[Exception, Optional[KeyType]]]",
|
|
458
|
+
) -> None:
|
|
383
459
|
"""
|
|
384
460
|
Display a dialog with tkinter in the current process.
|
|
385
461
|
|
|
@@ -415,7 +491,6 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
415
491
|
# So we schedule the root to go away.
|
|
416
492
|
root.after(100, root.destroy)
|
|
417
493
|
|
|
418
|
-
|
|
419
494
|
# Make a frame
|
|
420
495
|
frame = ttk.Frame(root, padding=FRAME_PADDING)
|
|
421
496
|
# Put it on a grid in the parent
|
|
@@ -441,7 +516,10 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
441
516
|
result.append(set_to)
|
|
442
517
|
# Close the window.
|
|
443
518
|
close_root()
|
|
444
|
-
|
|
519
|
+
|
|
520
|
+
ttk.Button(button_frame, text=v, command=setter).grid(
|
|
521
|
+
column=button_column, row=0
|
|
522
|
+
)
|
|
445
523
|
button_column += 1
|
|
446
524
|
|
|
447
525
|
# Buttons do not grow as the window resizes.
|
|
@@ -465,7 +543,10 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
465
543
|
# contents require. We can't use the required width exactly because
|
|
466
544
|
# that would never let us shrink and rewrap the label, so we use the
|
|
467
545
|
# button frame instead and add padding ourselves.
|
|
468
|
-
min_width = max(
|
|
546
|
+
min_width = max(
|
|
547
|
+
int(root.winfo_fpixels("2i")),
|
|
548
|
+
button_frame.winfo_reqwidth() + BUTTON_FRAME_PADDING + FRAME_PADDING,
|
|
549
|
+
)
|
|
469
550
|
min_height = max(int(root.winfo_fpixels("1i")), root.winfo_reqheight())
|
|
470
551
|
root.minsize(min_width, min_height)
|
|
471
552
|
|
|
@@ -473,6 +554,7 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
473
554
|
|
|
474
555
|
# Rewrap the text as the window size changes
|
|
475
556
|
setattr(label, "last_width", 100)
|
|
557
|
+
|
|
476
558
|
# MyPy demands a generic argument here but Python won't be able to handle
|
|
477
559
|
# it until
|
|
478
560
|
# <https://github.com/python/cpython/commit/42a818912bdb367c4ec2b7d58c18db35f55ebe3b>
|
|
@@ -491,7 +573,8 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
491
573
|
label.update_idletasks()
|
|
492
574
|
# Impose minimum height
|
|
493
575
|
force_fit()
|
|
494
|
-
|
|
576
|
+
|
|
577
|
+
label.bind("<Configure>", resize_label)
|
|
495
578
|
|
|
496
579
|
# Do root sizing
|
|
497
580
|
force_fit()
|
|
@@ -503,8 +586,8 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
503
586
|
# To make the dialog pop up over the terminal instead of behind it, we
|
|
504
587
|
# lift it and temporarily make it topmost. We don't keep it topmost
|
|
505
588
|
# because we want to let the user switch away from it.
|
|
506
|
-
root.attributes(
|
|
507
|
-
root.after(10, lambda: root.attributes(
|
|
589
|
+
root.attributes("-topmost", True)
|
|
590
|
+
root.after(10, lambda: root.attributes("-topmost", False))
|
|
508
591
|
root.lift()
|
|
509
592
|
|
|
510
593
|
# Run the window's main loop
|
|
@@ -519,7 +602,10 @@ def display_dialog_tkinter(title: str, text: str, options: dict[KeyType, str], t
|
|
|
519
602
|
except Exception as e:
|
|
520
603
|
result_queue.put(e)
|
|
521
604
|
|
|
522
|
-
|
|
605
|
+
|
|
606
|
+
def dialog_tui(
|
|
607
|
+
title: str, text: str, options: dict[KeyType, str], timeout: float
|
|
608
|
+
) -> KeyType | None:
|
|
523
609
|
"""
|
|
524
610
|
Display a dialog in the terminal.
|
|
525
611
|
|
|
@@ -538,7 +624,6 @@ def dialog_tui(title: str, text: str, options: dict[KeyType, str], timeout: floa
|
|
|
538
624
|
# for how we run concurrently with it.
|
|
539
625
|
|
|
540
626
|
from prompt_toolkit.shortcuts import button_dialog
|
|
541
|
-
from prompt_toolkit.eventloop.inputhook import set_eventloop_with_inputhook
|
|
542
627
|
|
|
543
628
|
# We take button options in the reverse order from how prompt_toolkit does it.
|
|
544
629
|
# TODO: This is not scrollable! What if there's more than 1 screen of text?
|
|
@@ -572,7 +657,9 @@ def dialog_tui(title: str, text: str, options: dict[KeyType, str], timeout: floa
|
|
|
572
657
|
timeout_task = loop.create_task(timeout_coroutine)
|
|
573
658
|
|
|
574
659
|
# This task finishes when the first task in the set finishes.
|
|
575
|
-
race_task = asyncio.wait(
|
|
660
|
+
race_task = asyncio.wait(
|
|
661
|
+
{application_task, timeout_task}, return_when=asyncio.FIRST_COMPLETED
|
|
662
|
+
)
|
|
576
663
|
# Run it until then
|
|
577
664
|
loop.run_until_complete(race_task)
|
|
578
665
|
# Maybe the application needs to finish its exit?
|
|
@@ -585,6 +672,7 @@ def dialog_tui(title: str, text: str, options: dict[KeyType, str], timeout: floa
|
|
|
585
672
|
|
|
586
673
|
return application_task.result()
|
|
587
674
|
|
|
675
|
+
|
|
588
676
|
# Define the dialog form in the abstract
|
|
589
677
|
Decision = Union[Literal["all"], Literal["current"], Literal["no"], Literal["never"]]
|
|
590
678
|
|
|
@@ -614,20 +702,27 @@ DIALOG_OPTIONS: dict[Decision, str] = {
|
|
|
614
702
|
"all": "All",
|
|
615
703
|
"current": "Yes",
|
|
616
704
|
"no": "No",
|
|
617
|
-
"never": "Never"
|
|
705
|
+
"never": "Never",
|
|
618
706
|
}
|
|
619
707
|
|
|
620
708
|
# Make sure the option texts are short enough; prompt_toolkit can only handle 10 characters.
|
|
621
709
|
for k, v in DIALOG_OPTIONS.items():
|
|
622
|
-
assert
|
|
710
|
+
assert (
|
|
711
|
+
len(v) <= 10
|
|
712
|
+
), f'Label for "{k}" dialog option is too long to work on all backends!'
|
|
623
713
|
|
|
624
714
|
# Make sure the options all have unique labels
|
|
625
|
-
assert len(set(DIALOG_OPTIONS.values())) == len(
|
|
715
|
+
assert len(set(DIALOG_OPTIONS.values())) == len(
|
|
716
|
+
DIALOG_OPTIONS
|
|
717
|
+
), "Labels for dialog options are not unique!"
|
|
626
718
|
|
|
627
719
|
# How many seconds should we show the dialog for before assuming the user means "no"?
|
|
628
720
|
DIALOG_TIMEOUT = 120
|
|
629
721
|
|
|
630
|
-
|
|
722
|
+
|
|
723
|
+
def ask_user_about_publishing_metrics() -> (
|
|
724
|
+
Literal["all"] | Literal["current"] | Literal["no"]
|
|
725
|
+
):
|
|
631
726
|
"""
|
|
632
727
|
Ask the user to set standing workflow submission consent.
|
|
633
728
|
|
|
@@ -636,13 +731,13 @@ def ask_user_about_publishing_metrics() -> Union[Literal["all"], Literal["curren
|
|
|
636
731
|
:returns: The user's decision about when to publish metrics.
|
|
637
732
|
"""
|
|
638
733
|
|
|
639
|
-
from toil.common import
|
|
734
|
+
from toil.common import get_default_config_path, update_config
|
|
640
735
|
|
|
641
736
|
# Find the default config path to talk about or update
|
|
642
737
|
default_config_path = get_default_config_path()
|
|
643
738
|
|
|
644
739
|
# Actual chatting with the user will fill this in if possible
|
|
645
|
-
decision:
|
|
740
|
+
decision: Decision | None = None
|
|
646
741
|
|
|
647
742
|
if sys.stdin.isatty() and sys.stderr.isatty():
|
|
648
743
|
# IO is not redirected (except maybe JSON to a file). We might be able to raise the user.
|
|
@@ -656,10 +751,15 @@ def ask_user_about_publishing_metrics() -> Union[Literal["all"], Literal["curren
|
|
|
656
751
|
# dialogs can't hold enough text. We don't really want to bring in
|
|
657
752
|
# e.g. QT for this.
|
|
658
753
|
try:
|
|
659
|
-
strategy_decision = strategy(
|
|
754
|
+
strategy_decision = strategy(
|
|
755
|
+
DIALOG_TITLE,
|
|
756
|
+
DIALOG_TEXT.format(default_config_path),
|
|
757
|
+
DIALOG_OPTIONS,
|
|
758
|
+
DIALOG_TIMEOUT,
|
|
759
|
+
)
|
|
660
760
|
if strategy_decision is None:
|
|
661
761
|
# User declined to choose. Treat that as choosing "no".
|
|
662
|
-
logger.warning(
|
|
762
|
+
logger.warning('User did not make a selection. Assuming "no".')
|
|
663
763
|
strategy_decision = "no"
|
|
664
764
|
decision = strategy_decision
|
|
665
765
|
break
|
|
@@ -669,7 +769,9 @@ def ask_user_about_publishing_metrics() -> Union[Literal["all"], Literal["curren
|
|
|
669
769
|
if decision is None:
|
|
670
770
|
# If we think we should be able to reach the user, but we can't, fail and make them tell us via option
|
|
671
771
|
# TODO: Provide a command (or a general toil config editing command) to manage this setting
|
|
672
|
-
logger.critical(
|
|
772
|
+
logger.critical(
|
|
773
|
+
"Decide whether to publish workflow metrics and pass --publishWorkflowMetrics=[all|current|no]"
|
|
774
|
+
)
|
|
673
775
|
sys.exit(1)
|
|
674
776
|
|
|
675
777
|
if decision is None:
|
|
@@ -682,14 +784,3 @@ def ask_user_about_publishing_metrics() -> Union[Literal["all"], Literal["curren
|
|
|
682
784
|
update_config(default_config_path, "publishWorkflowMetrics", result)
|
|
683
785
|
|
|
684
786
|
return result
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|