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/bus.py CHANGED
@@ -67,32 +67,25 @@ import os
67
67
  import queue
68
68
  import tempfile
69
69
  import threading
70
+ from collections.abc import Iterator
70
71
  from dataclasses import dataclass
71
- from typing import (IO,
72
- Any,
73
- Callable,
74
- Dict,
75
- Iterator,
76
- List,
77
- NamedTuple,
78
- Optional,
79
- Type,
80
- TypeVar,
81
- cast)
72
+ from typing import IO, Any, Callable, NamedTuple, Optional, TypeVar, cast
82
73
 
83
74
  from pubsub.core import Publisher
84
75
  from pubsub.core.listener import Listener
85
76
  from pubsub.core.topicobj import Topic
86
77
  from pubsub.core.topicutils import ALL_TOPICS
87
78
 
88
- logger = logging.getLogger( __name__ )
79
+ logger = logging.getLogger(__name__)
89
80
 
90
81
  # We define some ways to talk about jobs.
91
82
 
83
+
92
84
  class Names(NamedTuple):
93
85
  """
94
86
  Stores all the kinds of name a job can have.
95
87
  """
88
+
96
89
  # Name of the kind of job this is
97
90
  job_name: str
98
91
  # Name of this particular work unit
@@ -104,6 +97,7 @@ class Names(NamedTuple):
104
97
  # Job store ID of the job for the work unit
105
98
  job_store_id: str
106
99
 
100
+
107
101
  def get_job_kind(names: Names) -> str:
108
102
  """
109
103
  Return an identifying string for the job.
@@ -127,10 +121,12 @@ def get_job_kind(names: Names) -> str:
127
121
  # We define a bunch of named tuple message types.
128
122
  # These all need to be plain data: only hold ints, strings, etc.
129
123
 
124
+
130
125
  class JobIssuedMessage(NamedTuple):
131
126
  """
132
127
  Produced when a job is issued to run on the batch system.
133
128
  """
129
+
134
130
  # The kind of job issued, for statistics aggregation
135
131
  job_type: str
136
132
  # The job store ID of the job
@@ -138,20 +134,24 @@ class JobIssuedMessage(NamedTuple):
138
134
  # The toil batch ID of the job
139
135
  toil_batch_id: int
140
136
 
137
+
141
138
  class JobUpdatedMessage(NamedTuple):
142
139
  """
143
140
  Produced when a job is "updated" and ready to have something happen to it.
144
141
  """
142
+
145
143
  # The job store ID of the job
146
144
  job_id: str
147
145
  # The error code/return code for the job, which is nonzero if something has
148
146
  # gone wrong, and 0 otherwise.
149
147
  result_status: int
150
148
 
149
+
151
150
  class JobCompletedMessage(NamedTuple):
152
151
  """
153
152
  Produced when a job is completed, whether successful or not.
154
153
  """
154
+
155
155
  # The kind of job issued, for statistics aggregation
156
156
  job_type: str
157
157
  # The job store ID of the job
@@ -159,27 +159,33 @@ class JobCompletedMessage(NamedTuple):
159
159
  # Exit code for job_id
160
160
  exit_code: int
161
161
 
162
+
162
163
  class JobFailedMessage(NamedTuple):
163
164
  """
164
165
  Produced when a job is completely failed, and will not be retried again.
165
166
  """
167
+
166
168
  # The kind of job issued, for statistics aggregation
167
169
  job_type: str
168
170
  # The job store ID of the job
169
171
  job_id: str
170
172
 
173
+
171
174
  class JobMissingMessage(NamedTuple):
172
175
  """
173
176
  Produced when a job goes missing and should be in the batch system but isn't.
174
177
  """
178
+
175
179
  # The job store ID of the job
176
180
  job_id: str
177
181
 
182
+
178
183
  class JobAnnotationMessage(NamedTuple):
179
184
  """
180
185
  Produced when extra information (such as an AWS Batch job ID from the
181
186
  AWSBatchBatchSystem) is available that goes with a job.
182
187
  """
188
+
183
189
  # The job store ID of the job
184
190
  job_id: str
185
191
  # The name of the annotation
@@ -187,50 +193,60 @@ class JobAnnotationMessage(NamedTuple):
187
193
  # The annotation data
188
194
  annotation_value: str
189
195
 
196
+
190
197
  class ExternalBatchIdMessage(NamedTuple):
191
198
  """
192
199
  Produced when using a batch system, links toil assigned batch ID to
193
200
  Batch system ID (Whatever's returned by local implementation, PID, batch ID, etc)
194
201
  """
195
- #Assigned toil batch job id
202
+
203
+ # Assigned toil batch job id
196
204
  toil_batch_id: int
197
- #Batch system scheduler identity
205
+ # Batch system scheduler identity
198
206
  external_batch_id: str
199
- #Batch system name
207
+ # Batch system name
200
208
  batch_system: str
201
209
 
210
+
202
211
  class QueueSizeMessage(NamedTuple):
203
212
  """
204
213
  Produced to describe the size of the queue of jobs issued but not yet
205
214
  completed. Theoretically recoverable from other messages.
206
215
  """
216
+
207
217
  # The size of the queue
208
218
  queue_size: int
209
219
 
220
+
210
221
  class ClusterSizeMessage(NamedTuple):
211
222
  """
212
223
  Produced by the Toil-integrated autoscaler describe the number of
213
224
  instances of a certain type in a cluster.
214
225
  """
226
+
215
227
  # The instance type name, like t4g.medium
216
228
  instance_type: str
217
229
  # The number of instances of that type that the Toil autoscaler thinks
218
230
  # there are
219
231
  current_size: int
220
232
 
233
+
221
234
  class ClusterDesiredSizeMessage(NamedTuple):
222
235
  """
223
236
  Produced by the Toil-integrated autoscaler to describe the number of
224
237
  instances of a certain type that it thinks will be needed.
225
238
  """
239
+
226
240
  # The instance type name, like t4g.medium
227
241
  instance_type: str
228
242
  # The number of instances of that type that the Toil autoscaler wants there
229
243
  # to be
230
244
  desired_size: int
231
245
 
246
+
232
247
  # Then we define a serialization format.
233
248
 
249
+
234
250
  def message_to_bytes(message: NamedTuple) -> bytes:
235
251
  """
236
252
  Convert a plain-old-data named tuple into a byte string.
@@ -240,32 +256,39 @@ def message_to_bytes(message: NamedTuple) -> bytes:
240
256
  if isinstance(item, (int, float, bool)) or item is None:
241
257
  # This also handles e.g. values from an IntEnum, where the type extends int.
242
258
  # They might replace __str__() but we hope they use a compatible __format__()
243
- parts.append(f"{item}".encode('utf-8'))
259
+ parts.append(f"{item}".encode())
244
260
  elif isinstance(item, str):
245
- parts.append(item.encode('unicode_escape'))
261
+ parts.append(item.encode("unicode_escape"))
246
262
  else:
247
263
  # We haven't implemented this type yet.
248
- raise RuntimeError(f"Cannot store message argument of type {type(item)}: {item}")
249
- return b'\t'.join(parts)
264
+ raise RuntimeError(
265
+ f"Cannot store message argument of type {type(item)}: {item}"
266
+ )
267
+ return b"\t".join(parts)
250
268
 
251
269
 
252
270
  # TODO: Messages have to be named tuple types.
253
- MessageType = TypeVar('MessageType')
254
- def bytes_to_message(message_type: Type[MessageType], data: bytes) -> MessageType:
271
+ MessageType = TypeVar("MessageType")
272
+
273
+
274
+ def bytes_to_message(message_type: type[MessageType], data: bytes) -> MessageType:
255
275
  """
256
276
  Convert bytes from message_to_bytes back to a message of the given type.
257
277
  """
258
- parts = data.split(b'\t')
278
+ parts = data.split(b"\t")
259
279
 
260
280
  # Get a mapping from field name to type in the named tuple.
261
281
  # We need to check a couple different fields because this moved in a recent
262
282
  # Python 3 release.
263
- field_to_type: Optional[Dict[str, type]] = cast(Optional[Dict[str, type]],
264
- getattr(message_type, '__annotations__',
265
- getattr(message_type, '_field_types', None)))
283
+ field_to_type: Optional[dict[str, type]] = cast(
284
+ Optional[dict[str, type]],
285
+ getattr(
286
+ message_type, "__annotations__", getattr(message_type, "_field_types", None)
287
+ ),
288
+ )
266
289
  if field_to_type is None:
267
290
  raise RuntimeError(f"Cannot get field types from {message_type}")
268
- field_names: List[str] = getattr(message_type, '_fields')
291
+ field_names: list[str] = getattr(message_type, "_fields")
269
292
 
270
293
  if len(field_names) != len(parts):
271
294
  raise RuntimeError(f"Cannot parse {field_names} from {parts}")
@@ -276,10 +299,10 @@ def bytes_to_message(message_type: Type[MessageType], data: bytes) -> MessageTyp
276
299
  for name, part in zip(field_names, parts):
277
300
  field_type = field_to_type[name]
278
301
  if field_type in [int, float, bool]:
279
- typed_parts.append(field_type(part.decode('utf-8')))
302
+ typed_parts.append(field_type(part.decode("utf-8")))
280
303
  elif field_type == str:
281
304
  # Decode, accounting for escape sequences
282
- typed_parts.append(part.decode('unicode_escape'))
305
+ typed_parts.append(part.decode("unicode_escape"))
283
306
  else:
284
307
  raise RuntimeError(f"Cannot read message argument of type {field_type}")
285
308
 
@@ -287,8 +310,6 @@ def bytes_to_message(message_type: Type[MessageType], data: bytes) -> MessageTyp
287
310
  return message_type(*typed_parts)
288
311
 
289
312
 
290
-
291
-
292
313
  class MessageBus:
293
314
  """
294
315
  Holds messages that should cause jobs to change their scheduling states.
@@ -317,7 +338,7 @@ class MessageBus:
317
338
  characters, hierarchically dotted).
318
339
  """
319
340
 
320
- return '.'.join([message_type.__module__, message_type.__name__])
341
+ return ".".join([message_type.__module__, message_type.__name__])
321
342
 
322
343
  # All our messages are NamedTuples, but NamedTuples don't actually inherit
323
344
  # from NamedTupe, so MyPy complains if we require that here.
@@ -360,13 +381,16 @@ class MessageBus:
360
381
  Runs only in the owning thread. Delivers a message to its listeners.
361
382
  """
362
383
  topic = self._type_to_name(type(message))
363
- logger.debug('Notifying %s with message: %s', topic, message)
384
+ logger.debug("Notifying %s with message: %s", topic, message)
364
385
  self._pubsub.sendMessage(topic, message=message)
365
386
 
366
387
  # This next function takes callables that take things of the type that was passed in as a
367
388
  # runtime argument, which we can explain to MyPy using a TypeVar and Type[]
368
- MessageType = TypeVar('MessageType', bound='NamedTuple')
369
- def subscribe(self, message_type: Type[MessageType], handler: Callable[[MessageType], Any]) -> Listener:
389
+ MessageType = TypeVar("MessageType", bound="NamedTuple")
390
+
391
+ def subscribe(
392
+ self, message_type: type[MessageType], handler: Callable[[MessageType], Any]
393
+ ) -> Listener:
370
394
  """
371
395
  Register the given callable to be called when messages of the given type are sent.
372
396
  It will be called with messages sent after the subscription is created.
@@ -374,7 +398,7 @@ class MessageBus:
374
398
  """
375
399
 
376
400
  topic = self._type_to_name(message_type)
377
- logger.debug('Listening for message topic: %s', topic)
401
+ logger.debug("Listening for message topic: %s", topic)
378
402
 
379
403
  # Make sure to wrap the handler so we get the right argument name and
380
404
  # we can control lifetime.
@@ -387,10 +411,10 @@ class MessageBus:
387
411
  # Hide the handler function in the pubsub listener to keep it alive.
388
412
  # If it goes out of scope the subscription expires, and the pubsub
389
413
  # system only uses weak references.
390
- setattr(listener, 'handler_wrapper', handler_wraper)
414
+ setattr(listener, "handler_wrapper", handler_wraper)
391
415
  return listener
392
416
 
393
- def connect(self, wanted_types: List[type]) -> 'MessageBusConnection':
417
+ def connect(self, wanted_types: list[type]) -> "MessageBusConnection":
394
418
  """
395
419
  Get a connection object that serves as an inbox for messages of the
396
420
  given types.
@@ -402,7 +426,7 @@ class MessageBus:
402
426
  connection._set_bus_and_message_types(self, wanted_types)
403
427
  return connection
404
428
 
405
- def outbox(self) -> 'MessageOutbox':
429
+ def outbox(self) -> "MessageOutbox":
406
430
  """
407
431
  Get a connection object that only allows sending messages.
408
432
  """
@@ -420,24 +444,27 @@ class MessageBus:
420
444
  somewhere or delete it.
421
445
  """
422
446
 
423
-
424
- stream = open(file_path, 'wb')
447
+ stream = open(file_path, "wb")
425
448
 
426
449
  # Type of the ** is the value type of the dictionary; key type is always string.
427
- def handler(topic_object: Topic = Listener.AUTO_TOPIC, **message_data: NamedTuple) -> None:
450
+ def handler(
451
+ topic_object: Topic = Listener.AUTO_TOPIC, **message_data: NamedTuple
452
+ ) -> None:
428
453
  """
429
454
  Log the message in the given message data, associated with the
430
455
  given topic.
431
456
  """
432
457
  # There should always be a "message"
433
- if len(message_data) != 1 or 'message' not in message_data:
434
- raise RuntimeError("Cannot log the bus message. The message is either empty/malformed or there are too many messages provided.")
435
- message = message_data['message']
458
+ if len(message_data) != 1 or "message" not in message_data:
459
+ raise RuntimeError(
460
+ "Cannot log the bus message. The message is either empty/malformed or there are too many messages provided."
461
+ )
462
+ message = message_data["message"]
436
463
  topic = topic_object.getName()
437
- stream.write(topic.encode('utf-8'))
438
- stream.write(b'\t')
464
+ stream.write(topic.encode("utf-8"))
465
+ stream.write(b"\t")
439
466
  stream.write(message_to_bytes(message))
440
- stream.write(b'\n')
467
+ stream.write(b"\n")
441
468
  stream.flush()
442
469
 
443
470
  listener, _ = self._pubsub.subscribe(handler, ALL_TOPICS)
@@ -446,7 +473,6 @@ class MessageBus:
446
473
  # want the pypubsub Listener.
447
474
  return (handler, listener)
448
475
 
449
-
450
476
  # TODO: If we annotate this as returning an Iterator[NamedTuple], MyPy
451
477
  # complains when we loop over it that the loop variable is a <nothing>,
452
478
  # ifen in code protected by isinstance(). Using a typevar makes it complain
@@ -456,7 +482,9 @@ class MessageBus:
456
482
  # union of the types passed in message_types, in a way that MyPy can
457
483
  # understand.
458
484
  @classmethod
459
- def scan_bus_messages(cls, stream: IO[bytes], message_types: List[Type[NamedTuple]]) -> Iterator[Any]:
485
+ def scan_bus_messages(
486
+ cls, stream: IO[bytes], message_types: list[type[NamedTuple]]
487
+ ) -> Iterator[Any]:
460
488
  """
461
489
  Get an iterator over all messages in the given log stream of the given
462
490
  types, in order. Discard any trailing partial messages.
@@ -466,15 +494,15 @@ class MessageBus:
466
494
  name_to_type = {cls._type_to_name(t): t for t in message_types}
467
495
 
468
496
  for line in stream:
469
- logger.debug('Got message: %s', line)
470
- if not line.endswith(b'\n'):
497
+ logger.debug("Got message: %s", line)
498
+ if not line.endswith(b"\n"):
471
499
  # Skip unterminated line
472
500
  continue
473
501
  # Drop the newline and split on first tab
474
- parts = line[:-1].split(b'\t', 1)
502
+ parts = line[:-1].split(b"\t", 1)
475
503
 
476
504
  # Get the type of the message
477
- message_type = name_to_type.get(parts[0].decode('utf-8'))
505
+ message_type = name_to_type.get(parts[0].decode("utf-8"))
478
506
  if message_type is None:
479
507
  # We aren't interested in this kind of message.
480
508
  continue
@@ -485,6 +513,7 @@ class MessageBus:
485
513
  # And produce it
486
514
  yield message
487
515
 
516
+
488
517
  class MessageBusClient:
489
518
  """
490
519
  Base class for clients (inboxes and outboxes) of a message bus. Handles
@@ -507,6 +536,7 @@ class MessageBusClient:
507
536
  """
508
537
  self._bus = bus
509
538
 
539
+
510
540
  class MessageInbox(MessageBusClient):
511
541
  """
512
542
  A buffered connection to a message bus that lets us receive messages.
@@ -522,16 +552,19 @@ class MessageInbox(MessageBusClient):
522
552
  super().__init__()
523
553
 
524
554
  # This holds all the messages on the bus, organized by type.
525
- self._messages_by_type: Dict[type, List[Any]] = {}
555
+ self._messages_by_type: dict[type, list[Any]] = {}
526
556
  # This holds listeners for all the types, when we connect to a bus
527
- self._listeners_by_type: Dict[type, Listener] = {}
557
+ self._listeners_by_type: dict[type, Listener] = {}
528
558
 
529
559
  # We define a handler for messages
530
560
  def on_message(message: Any) -> None:
531
561
  self._messages_by_type[type(message)].append(message)
562
+
532
563
  self._handler = on_message
533
564
 
534
- def _set_bus_and_message_types(self, bus: MessageBus, wanted_types: List[type]) -> None:
565
+ def _set_bus_and_message_types(
566
+ self, bus: MessageBus, wanted_types: list[type]
567
+ ) -> None:
535
568
  """
536
569
  Connect to the given bus and collect the given message types.
537
570
 
@@ -576,8 +609,9 @@ class MessageInbox(MessageBusClient):
576
609
 
577
610
  # This next function returns things of the type that was passed in as a
578
611
  # runtime argument, which we can explain to MyPy using a TypeVar and Type[]
579
- MessageType = TypeVar('MessageType')
580
- def for_each(self, message_type: Type[MessageType]) -> Iterator[MessageType]:
612
+ MessageType = TypeVar("MessageType")
613
+
614
+ def for_each(self, message_type: type[MessageType]) -> Iterator[MessageType]:
581
615
  """
582
616
  Loop over all messages currently pending of the given type. Each that
583
617
  is handled without raising an exception will be removed.
@@ -607,7 +641,9 @@ class MessageInbox(MessageBusClient):
607
641
  try:
608
642
  # Emit the message
609
643
  if not isinstance(message, message_type):
610
- raise RuntimeError(f"Unacceptable message type {type(message)} in list for type {message_type}")
644
+ raise RuntimeError(
645
+ f"Unacceptable message type {type(message)} in list for type {message_type}"
646
+ )
611
647
  yield message
612
648
  # If we get here it was handled without error.
613
649
  handled = True
@@ -622,7 +658,10 @@ class MessageInbox(MessageBusClient):
622
658
  # Dump anything remaining in our buffer back into the main buffer,
623
659
  # in the right order, and before the later messages.
624
660
  message_list.reverse()
625
- self._messages_by_type[message_type] = message_list + self._messages_by_type[message_type]
661
+ self._messages_by_type[message_type] = (
662
+ message_list + self._messages_by_type[message_type]
663
+ )
664
+
626
665
 
627
666
  class MessageOutbox(MessageBusClient):
628
667
  """
@@ -645,6 +684,7 @@ class MessageOutbox(MessageBusClient):
645
684
  raise RuntimeError("Cannot send message when not connected to a bus")
646
685
  self._bus.publish(message)
647
686
 
687
+
648
688
  class MessageBusConnection(MessageInbox, MessageOutbox):
649
689
  """
650
690
  A two-way connection to a message bus. Buffers incoming messages until you
@@ -657,7 +697,9 @@ class MessageBusConnection(MessageInbox, MessageOutbox):
657
697
  """
658
698
  super().__init__()
659
699
 
660
- def _set_bus_and_message_types(self, bus: MessageBus, wanted_types: List[type]) -> None:
700
+ def _set_bus_and_message_types(
701
+ self, bus: MessageBus, wanted_types: list[type]
702
+ ) -> None:
661
703
  """
662
704
  Connect to the given bus and collect the given message types.
663
705
 
@@ -673,20 +715,28 @@ class MessageBusConnection(MessageInbox, MessageOutbox):
673
715
  class JobStatus:
674
716
  """
675
717
  Records the status of a job.
718
+
719
+ When exit_code is -1, this means the job is either not observed or currently running.
676
720
  """
677
721
 
678
722
  job_store_id: str
679
723
  name: str
680
724
  exit_code: int
681
- annotations: Dict[str, str]
725
+ annotations: dict[str, str]
682
726
  toil_batch_id: int
683
727
  external_batch_id: str
684
728
  batch_system: str
685
729
 
686
730
  def __repr__(self) -> str:
687
- return json.dumps(self, default= lambda o: o.__dict__, indent=4)
731
+ return json.dumps(self, default=lambda o: o.__dict__, indent=4)
732
+
733
+ def is_running(self) -> bool:
734
+ return (
735
+ self.exit_code < 0 and self.job_store_id != ""
736
+ ) # if the exit code is -1 and the job id is specified, we assume the job is running
688
737
 
689
- def replay_message_bus(path: str) -> Dict[str, JobStatus]:
738
+
739
+ def replay_message_bus(path: str) -> dict[str, JobStatus]:
690
740
  """
691
741
  Replay all the messages and work out what they mean for jobs.
692
742
 
@@ -702,15 +752,26 @@ def replay_message_bus(path: str) -> Dict[str, JobStatus]:
702
752
  is running.
703
753
  """
704
754
 
705
- job_statuses: Dict[str, JobStatus] = collections.defaultdict(lambda: JobStatus('', '', -1, {}, -1, '', ''))
755
+ job_statuses: dict[str, JobStatus] = collections.defaultdict(
756
+ lambda: JobStatus("", "", -1, {}, -1, "", "")
757
+ )
706
758
  batch_to_job_id = {}
707
759
  try:
708
- with open(path, 'rb') as log_stream:
760
+ with open(path, "rb") as log_stream:
709
761
  # Read all the full, properly-terminated messages about job updates
710
- for event in MessageBus.scan_bus_messages(log_stream, [JobUpdatedMessage, JobIssuedMessage, JobCompletedMessage,
711
- JobFailedMessage, JobAnnotationMessage, ExternalBatchIdMessage]):
762
+ for event in MessageBus.scan_bus_messages(
763
+ log_stream,
764
+ [
765
+ JobUpdatedMessage,
766
+ JobIssuedMessage,
767
+ JobCompletedMessage,
768
+ JobFailedMessage,
769
+ JobAnnotationMessage,
770
+ ExternalBatchIdMessage,
771
+ ],
772
+ ):
712
773
  # And for each of them
713
- logger.info('Got message from workflow: %s', event)
774
+ logger.debug("Got message from workflow: %s", event)
714
775
 
715
776
  if isinstance(event, JobUpdatedMessage):
716
777
  # Apply the latest return code from the job with this ID.
@@ -731,16 +792,23 @@ def replay_message_bus(path: str) -> Dict[str, JobStatus]:
731
792
  job_statuses[event.job_id].exit_code = 1
732
793
  elif isinstance(event, JobAnnotationMessage):
733
794
  # Remember the last value of any annotation that is set
734
- job_statuses[event.job_id].annotations[event.annotation_name] = event.annotation_value
795
+ job_statuses[event.job_id].annotations[
796
+ event.annotation_name
797
+ ] = event.annotation_value
735
798
  elif isinstance(event, ExternalBatchIdMessage):
736
799
  if event.toil_batch_id in batch_to_job_id:
737
- job_statuses[batch_to_job_id[event.toil_batch_id]].external_batch_id = event.external_batch_id
738
- job_statuses[batch_to_job_id[event.toil_batch_id]].batch_system = event.batch_system
800
+ job_statuses[
801
+ batch_to_job_id[event.toil_batch_id]
802
+ ].external_batch_id = event.external_batch_id
803
+ job_statuses[
804
+ batch_to_job_id[event.toil_batch_id]
805
+ ].batch_system = event.batch_system
739
806
  except FileNotFoundError:
740
807
  logger.warning("We were unable to access the file")
741
808
 
742
809
  return job_statuses
743
810
 
811
+
744
812
  def gen_message_bus_path(tmpdir: Optional[str] = None) -> str:
745
813
  """
746
814
  Return a file path in tmp to store the message bus at.
@@ -753,4 +821,4 @@ def gen_message_bus_path(tmpdir: Optional[str] = None) -> str:
753
821
  fd, path = tempfile.mkstemp(dir=tmpdir)
754
822
  os.close(fd)
755
823
  return path
756
- #TODO Might want to clean up the tmpfile at some point after running the workflow
824
+ # TODO Might want to clean up the tmpfile at some point after running the workflow