toil 7.0.0__py3-none-any.whl → 8.1.0b1__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 (197) hide show
  1. toil/__init__.py +124 -86
  2. toil/batchSystems/__init__.py +1 -0
  3. toil/batchSystems/abstractBatchSystem.py +137 -77
  4. toil/batchSystems/abstractGridEngineBatchSystem.py +211 -101
  5. toil/batchSystems/awsBatch.py +237 -128
  6. toil/batchSystems/cleanup_support.py +22 -16
  7. toil/batchSystems/contained_executor.py +30 -26
  8. toil/batchSystems/gridengine.py +85 -49
  9. toil/batchSystems/htcondor.py +164 -87
  10. toil/batchSystems/kubernetes.py +622 -386
  11. toil/batchSystems/local_support.py +17 -12
  12. toil/batchSystems/lsf.py +132 -79
  13. toil/batchSystems/lsfHelper.py +13 -11
  14. toil/batchSystems/mesos/__init__.py +41 -29
  15. toil/batchSystems/mesos/batchSystem.py +288 -149
  16. toil/batchSystems/mesos/executor.py +77 -49
  17. toil/batchSystems/mesos/test/__init__.py +31 -23
  18. toil/batchSystems/options.py +39 -29
  19. toil/batchSystems/registry.py +53 -19
  20. toil/batchSystems/singleMachine.py +293 -123
  21. toil/batchSystems/slurm.py +651 -155
  22. toil/batchSystems/torque.py +46 -32
  23. toil/bus.py +141 -73
  24. toil/common.py +784 -397
  25. toil/cwl/__init__.py +1 -1
  26. toil/cwl/cwltoil.py +1137 -534
  27. toil/cwl/utils.py +17 -22
  28. toil/deferred.py +62 -41
  29. toil/exceptions.py +5 -3
  30. toil/fileStores/__init__.py +5 -5
  31. toil/fileStores/abstractFileStore.py +88 -57
  32. toil/fileStores/cachingFileStore.py +711 -247
  33. toil/fileStores/nonCachingFileStore.py +113 -75
  34. toil/job.py +1031 -349
  35. toil/jobStores/abstractJobStore.py +387 -243
  36. toil/jobStores/aws/jobStore.py +772 -412
  37. toil/jobStores/aws/utils.py +161 -109
  38. toil/jobStores/conftest.py +1 -0
  39. toil/jobStores/fileJobStore.py +289 -151
  40. toil/jobStores/googleJobStore.py +137 -70
  41. toil/jobStores/utils.py +36 -15
  42. toil/leader.py +614 -269
  43. toil/lib/accelerators.py +115 -18
  44. toil/lib/aws/__init__.py +55 -28
  45. toil/lib/aws/ami.py +122 -87
  46. toil/lib/aws/iam.py +284 -108
  47. toil/lib/aws/s3.py +31 -0
  48. toil/lib/aws/session.py +204 -58
  49. toil/lib/aws/utils.py +290 -213
  50. toil/lib/bioio.py +13 -5
  51. toil/lib/compatibility.py +11 -6
  52. toil/lib/conversions.py +83 -49
  53. toil/lib/docker.py +131 -103
  54. toil/lib/dockstore.py +379 -0
  55. toil/lib/ec2.py +322 -209
  56. toil/lib/ec2nodes.py +174 -105
  57. toil/lib/encryption/_dummy.py +5 -3
  58. toil/lib/encryption/_nacl.py +10 -6
  59. toil/lib/encryption/conftest.py +1 -0
  60. toil/lib/exceptions.py +26 -7
  61. toil/lib/expando.py +4 -2
  62. toil/lib/ftp_utils.py +217 -0
  63. toil/lib/generatedEC2Lists.py +127 -19
  64. toil/lib/history.py +1271 -0
  65. toil/lib/history_submission.py +681 -0
  66. toil/lib/humanize.py +6 -2
  67. toil/lib/io.py +121 -12
  68. toil/lib/iterables.py +4 -2
  69. toil/lib/memoize.py +12 -8
  70. toil/lib/misc.py +83 -18
  71. toil/lib/objects.py +2 -2
  72. toil/lib/resources.py +19 -7
  73. toil/lib/retry.py +125 -87
  74. toil/lib/threading.py +282 -80
  75. toil/lib/throttle.py +15 -14
  76. toil/lib/trs.py +390 -0
  77. toil/lib/web.py +38 -0
  78. toil/options/common.py +850 -402
  79. toil/options/cwl.py +185 -90
  80. toil/options/runner.py +50 -0
  81. toil/options/wdl.py +70 -19
  82. toil/provisioners/__init__.py +111 -46
  83. toil/provisioners/abstractProvisioner.py +322 -157
  84. toil/provisioners/aws/__init__.py +62 -30
  85. toil/provisioners/aws/awsProvisioner.py +980 -627
  86. toil/provisioners/clusterScaler.py +541 -279
  87. toil/provisioners/gceProvisioner.py +283 -180
  88. toil/provisioners/node.py +147 -79
  89. toil/realtimeLogger.py +34 -22
  90. toil/resource.py +137 -75
  91. toil/server/app.py +127 -61
  92. toil/server/celery_app.py +3 -1
  93. toil/server/cli/wes_cwl_runner.py +84 -55
  94. toil/server/utils.py +56 -31
  95. toil/server/wes/abstract_backend.py +64 -26
  96. toil/server/wes/amazon_wes_utils.py +21 -15
  97. toil/server/wes/tasks.py +121 -63
  98. toil/server/wes/toil_backend.py +142 -107
  99. toil/server/wsgi_app.py +4 -3
  100. toil/serviceManager.py +58 -22
  101. toil/statsAndLogging.py +183 -65
  102. toil/test/__init__.py +263 -179
  103. toil/test/batchSystems/batchSystemTest.py +438 -195
  104. toil/test/batchSystems/batch_system_plugin_test.py +18 -7
  105. toil/test/batchSystems/test_gridengine.py +173 -0
  106. toil/test/batchSystems/test_lsf_helper.py +67 -58
  107. toil/test/batchSystems/test_slurm.py +265 -49
  108. toil/test/cactus/test_cactus_integration.py +20 -22
  109. toil/test/cwl/conftest.py +39 -0
  110. toil/test/cwl/cwlTest.py +375 -72
  111. toil/test/cwl/measure_default_memory.cwl +12 -0
  112. toil/test/cwl/not_run_required_input.cwl +29 -0
  113. toil/test/cwl/optional-file.cwl +18 -0
  114. toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
  115. toil/test/docs/scriptsTest.py +60 -34
  116. toil/test/jobStores/jobStoreTest.py +412 -235
  117. toil/test/lib/aws/test_iam.py +116 -48
  118. toil/test/lib/aws/test_s3.py +16 -9
  119. toil/test/lib/aws/test_utils.py +5 -6
  120. toil/test/lib/dockerTest.py +118 -141
  121. toil/test/lib/test_conversions.py +113 -115
  122. toil/test/lib/test_ec2.py +57 -49
  123. toil/test/lib/test_history.py +212 -0
  124. toil/test/lib/test_misc.py +12 -5
  125. toil/test/lib/test_trs.py +161 -0
  126. toil/test/mesos/MesosDataStructuresTest.py +23 -10
  127. toil/test/mesos/helloWorld.py +7 -6
  128. toil/test/mesos/stress.py +25 -20
  129. toil/test/options/options.py +7 -2
  130. toil/test/provisioners/aws/awsProvisionerTest.py +293 -140
  131. toil/test/provisioners/clusterScalerTest.py +440 -250
  132. toil/test/provisioners/clusterTest.py +81 -42
  133. toil/test/provisioners/gceProvisionerTest.py +174 -100
  134. toil/test/provisioners/provisionerTest.py +25 -13
  135. toil/test/provisioners/restartScript.py +5 -4
  136. toil/test/server/serverTest.py +188 -141
  137. toil/test/sort/restart_sort.py +137 -68
  138. toil/test/sort/sort.py +134 -66
  139. toil/test/sort/sortTest.py +91 -49
  140. toil/test/src/autoDeploymentTest.py +140 -100
  141. toil/test/src/busTest.py +20 -18
  142. toil/test/src/checkpointTest.py +8 -2
  143. toil/test/src/deferredFunctionTest.py +49 -35
  144. toil/test/src/dockerCheckTest.py +33 -26
  145. toil/test/src/environmentTest.py +20 -10
  146. toil/test/src/fileStoreTest.py +538 -271
  147. toil/test/src/helloWorldTest.py +7 -4
  148. toil/test/src/importExportFileTest.py +61 -31
  149. toil/test/src/jobDescriptionTest.py +32 -17
  150. toil/test/src/jobEncapsulationTest.py +2 -0
  151. toil/test/src/jobFileStoreTest.py +74 -50
  152. toil/test/src/jobServiceTest.py +187 -73
  153. toil/test/src/jobTest.py +120 -70
  154. toil/test/src/miscTests.py +19 -18
  155. toil/test/src/promisedRequirementTest.py +82 -36
  156. toil/test/src/promisesTest.py +7 -6
  157. toil/test/src/realtimeLoggerTest.py +6 -6
  158. toil/test/src/regularLogTest.py +71 -37
  159. toil/test/src/resourceTest.py +80 -49
  160. toil/test/src/restartDAGTest.py +36 -22
  161. toil/test/src/resumabilityTest.py +9 -2
  162. toil/test/src/retainTempDirTest.py +45 -14
  163. toil/test/src/systemTest.py +12 -8
  164. toil/test/src/threadingTest.py +44 -25
  165. toil/test/src/toilContextManagerTest.py +10 -7
  166. toil/test/src/userDefinedJobArgTypeTest.py +8 -5
  167. toil/test/src/workerTest.py +33 -16
  168. toil/test/utils/toilDebugTest.py +70 -58
  169. toil/test/utils/toilKillTest.py +4 -5
  170. toil/test/utils/utilsTest.py +239 -102
  171. toil/test/wdl/wdltoil_test.py +789 -148
  172. toil/test/wdl/wdltoil_test_kubernetes.py +37 -23
  173. toil/toilState.py +52 -26
  174. toil/utils/toilConfig.py +13 -4
  175. toil/utils/toilDebugFile.py +44 -27
  176. toil/utils/toilDebugJob.py +85 -25
  177. toil/utils/toilDestroyCluster.py +11 -6
  178. toil/utils/toilKill.py +8 -3
  179. toil/utils/toilLaunchCluster.py +251 -145
  180. toil/utils/toilMain.py +37 -16
  181. toil/utils/toilRsyncCluster.py +27 -14
  182. toil/utils/toilSshCluster.py +45 -22
  183. toil/utils/toilStats.py +75 -36
  184. toil/utils/toilStatus.py +226 -119
  185. toil/utils/toilUpdateEC2Instances.py +3 -1
  186. toil/version.py +6 -6
  187. toil/wdl/utils.py +5 -5
  188. toil/wdl/wdltoil.py +3528 -1053
  189. toil/worker.py +370 -149
  190. toil-8.1.0b1.dist-info/METADATA +178 -0
  191. toil-8.1.0b1.dist-info/RECORD +259 -0
  192. {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/WHEEL +1 -1
  193. toil-7.0.0.dist-info/METADATA +0 -158
  194. toil-7.0.0.dist-info/RECORD +0 -244
  195. {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/LICENSE +0 -0
  196. {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/entry_points.txt +0 -0
  197. {toil-7.0.0.dist-info → toil-8.1.0b1.dist-info}/top_level.txt +0 -0
toil/lib/dockstore.py ADDED
@@ -0,0 +1,379 @@
1
+ # Copyright (C) 2024 Regents of the University of California
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Contains functions for integrating Toil with UCSC Dockstore, for reporting metrics.
17
+
18
+ For basic TRS functionality for fetching workflows, see trs.py.
19
+ """
20
+
21
+ import datetime
22
+ import logging
23
+ import math
24
+ import os
25
+ import re
26
+ import sys
27
+ import uuid
28
+ from typing import Any, Literal, Optional, Union, TypedDict, cast
29
+
30
+ from urllib.parse import urlparse, unquote, quote
31
+ import requests
32
+
33
+ from toil.lib.misc import unix_seconds_to_timestamp, seconds_to_duration
34
+ from toil.lib.trs import TRS_ROOT
35
+ from toil.lib.retry import retry
36
+ from toil.lib.web import web_session
37
+ from toil.version import baseVersion
38
+
39
+ if sys.version_info < (3, 11):
40
+ from typing_extensions import NotRequired
41
+ else:
42
+ from typing import NotRequired
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ # We assume TRS_ROOT is actually a Dockstore instance.
47
+
48
+ # How should we authenticate our Dockstore requests?
49
+ DOCKSTORE_TOKEN = os.environ.get("TOIL_DOCKSTORE_TOKEN")
50
+
51
+
52
+ # This is a https://schema.org/CompletedActionStatus
53
+ # The values here are from expanding the type info in the Docksotre docs at
54
+ # <https://dockstore.org/api/static/swagger-ui/index.html#/extendedGA4GH/executionMetricsPost>
55
+ ExecutionStatus = Union[Literal["ALL"], Literal["SUCCESSFUL"], Literal["FAILED"], Literal["FAILED_SEMANTIC_INVALID"], Literal["FAILED_RUNTIME_INVALID"], Literal["ABORTED"]]
56
+
57
+ class Cost(TypedDict):
58
+ """
59
+ Representation of the cost of running something.
60
+ """
61
+
62
+ value: float
63
+ """
64
+ Cost in US Dollars.
65
+ """
66
+
67
+ class RunExecution(TypedDict):
68
+ """
69
+ Dockstore metrics data for a workflow or task run.
70
+ """
71
+
72
+ executionId: str
73
+ """
74
+ Executor-generated unique execution ID for this workflow or task.
75
+ """
76
+
77
+ dateExecuted: str
78
+ """
79
+ ISO 8601 UTC timestamp when the execution happend.
80
+ """
81
+
82
+ executionStatus: ExecutionStatus
83
+ """
84
+ Did the execution work?
85
+ """
86
+
87
+ executionTime: NotRequired[str]
88
+ """
89
+ Total time of the run in ISO 8601 duration format.
90
+ """
91
+
92
+ # TODO: Is this meant to be actual usage or amount provided?
93
+ memoryRequirementsGB: NotRequired[float]
94
+ """
95
+ Memory required for the execution in gigabytes (not GiB).
96
+ """
97
+
98
+ cpuRequirements: NotRequired[int]
99
+ """
100
+ Number of CPUs required.
101
+ """
102
+
103
+ cost: NotRequired[Cost]
104
+ """
105
+ How much the execution cost to run.
106
+ """
107
+
108
+ # TODO: What if two cloud providers have the same region naming scheme?
109
+ region: NotRequired[str]
110
+ """
111
+ The (cloud) region the workflow was executed in.
112
+ """
113
+
114
+ additionalProperties: NotRequired[dict[str, Any]]
115
+ """
116
+ Any additional properties to send.
117
+
118
+ Dockstore can take any JSON-able structured data.
119
+ """
120
+
121
+ class TaskExecutions(TypedDict):
122
+ """
123
+ Dockstore metrics data for all the tasks in a workflow.
124
+ """
125
+
126
+ # TODO: Right now we use different IDs for the workflow RunExecution and
127
+ # for its corresponding collection of TaskExecutions, so there's no nice
128
+ # way to find the one from the other.
129
+ executionId: str
130
+ """
131
+ Executor-generated unique execution ID.
132
+ """
133
+
134
+ dateExecuted: str
135
+ """
136
+ ISO 8601 UTC timestamp when the execution happend.
137
+ """
138
+
139
+ taskExecutions: list[RunExecution]
140
+ """
141
+ Individual executions of each task in the workflow.
142
+ """
143
+
144
+ additionalProperties: NotRequired[dict[str, Any]]
145
+ """
146
+ Any additional properties to send.
147
+
148
+ Dockstore can take any JSON-able structured data.
149
+ """
150
+
151
+ def ensure_valid_id(execution_id: str) -> None:
152
+ """
153
+ Make sure the given execution ID is in Dockstore format and will be accepted by Dockstore.
154
+
155
+ Must be alphanumeric (with internal underscores allowed) and <100
156
+ characters long.
157
+
158
+ :raises ValueError: if the ID is not in the right format
159
+ """
160
+ if len(execution_id) >= 100:
161
+ raise ValueError("Execution ID too long")
162
+ if len(execution_id) == 0:
163
+ raise ValueError("Execution ID must not be empty")
164
+ if execution_id[0] == "_" or execution_id[-1] == "_":
165
+ raise ValueError("Execution ID must not start or end with an underscore")
166
+ if not re.fullmatch("[a-zA-Z0-9_]+", execution_id):
167
+ raise ValueError("Execution ID must be alphanumeric with internal underscores")
168
+
169
+ def pack_workflow_metrics(
170
+ execution_id: str,
171
+ start_time: float,
172
+ runtime: float,
173
+ succeeded: bool,
174
+ job_store_type: Optional[str] = None,
175
+ batch_system: Optional[str] = None,
176
+ caching: Optional[bool] = None,
177
+ toil_version: Optional[str] = None,
178
+ python_version: Optional[str] = None,
179
+ platform_system: Optional[str] = None,
180
+ platform_machine: Optional[str] = None
181
+ ) -> RunExecution:
182
+ """
183
+ Pack up per-workflow metrics into a format that can be submitted to Dockstore.
184
+
185
+ :param execution_id: Unique ID for the workflow execution. Must be in
186
+ Dockstore format.
187
+ :param start_time: Execution start time in seconds since the Unix epoch.
188
+ :param rutime: Execution duration in seconds.
189
+ :param jobstore_type: Kind of job store used, like "file" or "aws".
190
+ :param batch_system: Python class name implementing the batch system used.
191
+ :param caching: Whether Toil filestore-level cahcing was used.
192
+ :param toil_version: Version of Toil used (without any Git hash).
193
+ :param python_version: Version of Python used.
194
+ :param platform_system: Operating system type (like "Darwin" or "Linux").
195
+ :param platform_machine: Machine architecture of the leader (like "AMD64").
196
+ """
197
+
198
+ # Enforce Dockstore's constraints
199
+ ensure_valid_id(execution_id)
200
+
201
+ # Pack up into a RunExecution
202
+ result = RunExecution(
203
+ executionId=execution_id,
204
+ dateExecuted=unix_seconds_to_timestamp(start_time),
205
+ executionTime=seconds_to_duration(runtime),
206
+ executionStatus="SUCCESSFUL" if succeeded else "FAILED"
207
+ )
208
+
209
+ # TODO: Just use kwargs here?
210
+ additional_properties: dict[str, Any] = {}
211
+
212
+ if job_store_type is not None:
213
+ additional_properties["jobStoreType"] = job_store_type
214
+
215
+ if batch_system is not None:
216
+ additional_properties["batchSystem"] = batch_system
217
+
218
+ if caching is not None:
219
+ additional_properties["caching"] = caching
220
+
221
+ if toil_version is not None:
222
+ additional_properties["toilVersion"] = toil_version
223
+
224
+ if python_version is not None:
225
+ additional_properties["pythonVersion"] = python_version
226
+
227
+ if platform_system is not None:
228
+ additional_properties["platformSystem"] = platform_system
229
+
230
+ if platform_machine is not None:
231
+ additional_properties["platformMachine"] = platform_machine
232
+
233
+ if len(additional_properties) > 0:
234
+ result["additionalProperties"] = additional_properties
235
+
236
+ return result
237
+
238
+ def pack_single_task_metrics(
239
+ execution_id: str,
240
+ start_time: float,
241
+ runtime: float,
242
+ succeeded: bool,
243
+ job_name: Optional[str] = None,
244
+ cores: Optional[float] = None,
245
+ cpu_seconds: Optional[float] = None,
246
+ memory_bytes: Optional[int] = None,
247
+ disk_bytes: Optional[int] = None,
248
+ ) -> RunExecution:
249
+ """
250
+ Pack up metrics for a single task execution in a format that can be used in a Dockstore submission.
251
+
252
+ :param execution_id: Unique ID for the workflow execution. Must be in
253
+ Dockstore format.
254
+ :param start_time: Execution start time in seconds since the Unix epoch.
255
+ :param rutime: Execution duration in seconds.
256
+ :param succeeded: Whether the execution succeeded.
257
+ :param job_name: Name of the job within the workflow.
258
+ :param cores: CPU cores allocated to the job.
259
+ :param cpu_seconds: CPU seconds consumed by the job.
260
+ :param memory_bytes: Memory consumed by the job in bytes.
261
+ :param disk_bytes: Disk space consumed by the job in bytes.
262
+ """
263
+
264
+ # TODO: Deduplicate with workflow code since the output type is the same.
265
+
266
+ # Enforce Dockstore's constraints
267
+ ensure_valid_id(execution_id)
268
+
269
+ # Pack up into a RunExecution
270
+ result = RunExecution(
271
+ executionId=execution_id,
272
+ dateExecuted=unix_seconds_to_timestamp(start_time),
273
+ executionTime=seconds_to_duration(runtime),
274
+ executionStatus="SUCCESSFUL" if succeeded else "FAILED"
275
+ )
276
+
277
+ if memory_bytes is not None:
278
+ # Convert bytes to fractional gigabytes
279
+ result["memoryRequirementsGB"] = memory_bytes / 1_000_000_000
280
+
281
+ if cores is not None:
282
+ # Convert possibly fractional cores to an integer for Dockstore
283
+ result["cpuRequirements"] = int(math.ceil(cores))
284
+
285
+ # TODO: Just use kwargs here?
286
+ additional_properties: dict[str, Any] = {}
287
+
288
+ if job_name is not None:
289
+ # Convert to Doskstore-style camelCase property keys
290
+ additional_properties["jobName"] = job_name
291
+
292
+ if disk_bytes is not None:
293
+ # Convert to a Dockstore-style fractional disk gigabytes
294
+ additional_properties["diskRequirementsGB"] = disk_bytes / 1_000_000_000
295
+
296
+ if cpu_seconds is not None:
297
+ # Use a Dockstore-ier name here too
298
+ additional_properties["cpuRequirementsCoreSeconds"] = cpu_seconds
299
+
300
+ if len(additional_properties) > 0:
301
+ result["additionalProperties"] = additional_properties
302
+
303
+ return result
304
+
305
+
306
+ def pack_workflow_task_set_metrics(execution_id: str, start_time: float, tasks: list[RunExecution]) -> TaskExecutions:
307
+ """
308
+ Pack up metrics for all the tasks in a workflow execution into a format that can be submitted to Dockstore.
309
+
310
+ :param execution_id: Unique ID for the workflow execution. Must be in
311
+ Dockstore format.
312
+ :param start_time: Execution start time for the overall workflow execution
313
+ in seconds since the Unix epoch.
314
+ :param tasks: Packed tasks from pack_single_task_metrics()
315
+ """
316
+
317
+ # Enforce Dockstore's constraints
318
+ ensure_valid_id(execution_id)
319
+
320
+ return TaskExecutions(
321
+ executionId=execution_id,
322
+ dateExecuted=unix_seconds_to_timestamp(start_time),
323
+ taskExecutions=tasks
324
+ )
325
+
326
+ def send_metrics(trs_workflow_id: str, trs_version: str, workflow_runs: list[RunExecution], workflow_task_sets: list[TaskExecutions]) -> None:
327
+ """
328
+ Send packed workflow and/or task metrics to Dockstore.
329
+
330
+ :param workflow_runs: list of packed metrics objects for each workflow.
331
+
332
+ :param workflow_task_sets: list of packed metrics objects for the tasks in
333
+ each workflow. Each workflow should have one entry containing all its
334
+ tasks. Does not have to be the same order/set of workflows as
335
+ workflow_runs.
336
+
337
+ :raises requests.HTTPError: if Dockstore does not accept the metrics.
338
+ """
339
+
340
+ # Aggregate into a submission
341
+ to_post = {
342
+ "runExecutions": workflow_runs,
343
+ "taskExecutions": workflow_task_sets,
344
+ "validationExecutions": []
345
+ }
346
+
347
+ # Set the submission query string metadata
348
+ submission_params = {
349
+ "platform": "OTHER",
350
+ "description": "Workflow status from Toil"
351
+ }
352
+
353
+ # Set the headers. Even though user agent isn't in here, it still gets
354
+ # sent.
355
+ headers = {}
356
+ if DOCKSTORE_TOKEN is not None:
357
+ headers["Authorization"] = f"Bearer {DOCKSTORE_TOKEN}"
358
+
359
+ # Note that Dockstore's metrics apparently need two levels of /api for some reason.
360
+ endpoint_url = f"{TRS_ROOT}/api/api/ga4gh/v2/extended/{quote(trs_workflow_id, safe='')}/versions/{quote(trs_version, safe='')}/executions"
361
+
362
+ logger.info("Sending workflow metrics to %s", endpoint_url)
363
+ logger.debug("With data: %s", to_post)
364
+ logger.debug("With headers: %s", headers)
365
+
366
+ try:
367
+ result = web_session.post(endpoint_url, params=submission_params, json=to_post, headers=headers)
368
+ result.raise_for_status()
369
+ except requests.HTTPError as e:
370
+ logger.warning("Workflow metrics were not accepted by Dockstore. Dockstore complained: %s", e.response.text)
371
+ raise
372
+
373
+ def get_metrics_url(trs_workflow_id: str, trs_version: str, execution_id: str) -> str:
374
+ """
375
+ Get the URL where a workflow metrics object (for a workflow, or for a set of tasks) can be fetched back from.
376
+ """
377
+
378
+ return f"{TRS_ROOT}/api/api/ga4gh/v2/extended/{quote(trs_workflow_id, safe='')}/versions/{quote(trs_version, safe='')}/execution?platform=OTHER&executionId={quote(execution_id, safe='')}"
379
+