toil 6.1.0a1__py3-none-any.whl → 8.0.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 (193) hide show
  1. toil/__init__.py +122 -315
  2. toil/batchSystems/__init__.py +1 -0
  3. toil/batchSystems/abstractBatchSystem.py +173 -89
  4. toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
  5. toil/batchSystems/awsBatch.py +244 -135
  6. toil/batchSystems/cleanup_support.py +26 -16
  7. toil/batchSystems/contained_executor.py +31 -28
  8. toil/batchSystems/gridengine.py +86 -50
  9. toil/batchSystems/htcondor.py +166 -89
  10. toil/batchSystems/kubernetes.py +632 -382
  11. toil/batchSystems/local_support.py +20 -15
  12. toil/batchSystems/lsf.py +134 -81
  13. toil/batchSystems/lsfHelper.py +13 -11
  14. toil/batchSystems/mesos/__init__.py +41 -29
  15. toil/batchSystems/mesos/batchSystem.py +290 -151
  16. toil/batchSystems/mesos/executor.py +79 -50
  17. toil/batchSystems/mesos/test/__init__.py +31 -23
  18. toil/batchSystems/options.py +46 -28
  19. toil/batchSystems/registry.py +53 -19
  20. toil/batchSystems/singleMachine.py +296 -125
  21. toil/batchSystems/slurm.py +603 -138
  22. toil/batchSystems/torque.py +47 -33
  23. toil/bus.py +186 -76
  24. toil/common.py +664 -368
  25. toil/cwl/__init__.py +1 -1
  26. toil/cwl/cwltoil.py +1136 -483
  27. toil/cwl/utils.py +17 -22
  28. toil/deferred.py +63 -42
  29. toil/exceptions.py +5 -3
  30. toil/fileStores/__init__.py +5 -5
  31. toil/fileStores/abstractFileStore.py +140 -60
  32. toil/fileStores/cachingFileStore.py +717 -269
  33. toil/fileStores/nonCachingFileStore.py +116 -87
  34. toil/job.py +1225 -368
  35. toil/jobStores/abstractJobStore.py +416 -266
  36. toil/jobStores/aws/jobStore.py +863 -477
  37. toil/jobStores/aws/utils.py +201 -120
  38. toil/jobStores/conftest.py +3 -2
  39. toil/jobStores/fileJobStore.py +292 -154
  40. toil/jobStores/googleJobStore.py +140 -74
  41. toil/jobStores/utils.py +36 -15
  42. toil/leader.py +668 -272
  43. toil/lib/accelerators.py +115 -18
  44. toil/lib/aws/__init__.py +74 -31
  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 +214 -39
  49. toil/lib/aws/utils.py +287 -231
  50. toil/lib/bioio.py +13 -5
  51. toil/lib/compatibility.py +11 -6
  52. toil/lib/conversions.py +104 -47
  53. toil/lib/docker.py +131 -103
  54. toil/lib/ec2.py +361 -199
  55. toil/lib/ec2nodes.py +174 -106
  56. toil/lib/encryption/_dummy.py +5 -3
  57. toil/lib/encryption/_nacl.py +10 -6
  58. toil/lib/encryption/conftest.py +1 -0
  59. toil/lib/exceptions.py +26 -7
  60. toil/lib/expando.py +5 -3
  61. toil/lib/ftp_utils.py +217 -0
  62. toil/lib/generatedEC2Lists.py +127 -19
  63. toil/lib/humanize.py +6 -2
  64. toil/lib/integration.py +341 -0
  65. toil/lib/io.py +141 -15
  66. toil/lib/iterables.py +4 -2
  67. toil/lib/memoize.py +12 -8
  68. toil/lib/misc.py +66 -21
  69. toil/lib/objects.py +2 -2
  70. toil/lib/resources.py +68 -15
  71. toil/lib/retry.py +126 -81
  72. toil/lib/threading.py +299 -82
  73. toil/lib/throttle.py +16 -15
  74. toil/options/common.py +843 -409
  75. toil/options/cwl.py +175 -90
  76. toil/options/runner.py +50 -0
  77. toil/options/wdl.py +73 -17
  78. toil/provisioners/__init__.py +117 -46
  79. toil/provisioners/abstractProvisioner.py +332 -157
  80. toil/provisioners/aws/__init__.py +70 -33
  81. toil/provisioners/aws/awsProvisioner.py +1145 -715
  82. toil/provisioners/clusterScaler.py +541 -279
  83. toil/provisioners/gceProvisioner.py +282 -179
  84. toil/provisioners/node.py +155 -79
  85. toil/realtimeLogger.py +34 -22
  86. toil/resource.py +137 -75
  87. toil/server/app.py +128 -62
  88. toil/server/celery_app.py +3 -1
  89. toil/server/cli/wes_cwl_runner.py +82 -53
  90. toil/server/utils.py +54 -28
  91. toil/server/wes/abstract_backend.py +64 -26
  92. toil/server/wes/amazon_wes_utils.py +21 -15
  93. toil/server/wes/tasks.py +121 -63
  94. toil/server/wes/toil_backend.py +142 -107
  95. toil/server/wsgi_app.py +4 -3
  96. toil/serviceManager.py +58 -22
  97. toil/statsAndLogging.py +224 -70
  98. toil/test/__init__.py +282 -183
  99. toil/test/batchSystems/batchSystemTest.py +460 -210
  100. toil/test/batchSystems/batch_system_plugin_test.py +90 -0
  101. toil/test/batchSystems/test_gridengine.py +173 -0
  102. toil/test/batchSystems/test_lsf_helper.py +67 -58
  103. toil/test/batchSystems/test_slurm.py +110 -49
  104. toil/test/cactus/__init__.py +0 -0
  105. toil/test/cactus/test_cactus_integration.py +56 -0
  106. toil/test/cwl/cwlTest.py +496 -287
  107. toil/test/cwl/measure_default_memory.cwl +12 -0
  108. toil/test/cwl/not_run_required_input.cwl +29 -0
  109. toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
  110. toil/test/cwl/seqtk_seq.cwl +1 -1
  111. toil/test/docs/scriptsTest.py +69 -46
  112. toil/test/jobStores/jobStoreTest.py +427 -264
  113. toil/test/lib/aws/test_iam.py +118 -50
  114. toil/test/lib/aws/test_s3.py +16 -9
  115. toil/test/lib/aws/test_utils.py +5 -6
  116. toil/test/lib/dockerTest.py +118 -141
  117. toil/test/lib/test_conversions.py +113 -115
  118. toil/test/lib/test_ec2.py +58 -50
  119. toil/test/lib/test_integration.py +104 -0
  120. toil/test/lib/test_misc.py +12 -5
  121. toil/test/mesos/MesosDataStructuresTest.py +23 -10
  122. toil/test/mesos/helloWorld.py +7 -6
  123. toil/test/mesos/stress.py +25 -20
  124. toil/test/options/__init__.py +13 -0
  125. toil/test/options/options.py +42 -0
  126. toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
  127. toil/test/provisioners/clusterScalerTest.py +440 -250
  128. toil/test/provisioners/clusterTest.py +166 -44
  129. toil/test/provisioners/gceProvisionerTest.py +174 -100
  130. toil/test/provisioners/provisionerTest.py +25 -13
  131. toil/test/provisioners/restartScript.py +5 -4
  132. toil/test/server/serverTest.py +188 -141
  133. toil/test/sort/restart_sort.py +137 -68
  134. toil/test/sort/sort.py +134 -66
  135. toil/test/sort/sortTest.py +91 -49
  136. toil/test/src/autoDeploymentTest.py +141 -101
  137. toil/test/src/busTest.py +20 -18
  138. toil/test/src/checkpointTest.py +8 -2
  139. toil/test/src/deferredFunctionTest.py +49 -35
  140. toil/test/src/dockerCheckTest.py +32 -24
  141. toil/test/src/environmentTest.py +135 -0
  142. toil/test/src/fileStoreTest.py +539 -272
  143. toil/test/src/helloWorldTest.py +7 -4
  144. toil/test/src/importExportFileTest.py +61 -31
  145. toil/test/src/jobDescriptionTest.py +46 -21
  146. toil/test/src/jobEncapsulationTest.py +2 -0
  147. toil/test/src/jobFileStoreTest.py +74 -50
  148. toil/test/src/jobServiceTest.py +187 -73
  149. toil/test/src/jobTest.py +121 -71
  150. toil/test/src/miscTests.py +19 -18
  151. toil/test/src/promisedRequirementTest.py +82 -36
  152. toil/test/src/promisesTest.py +7 -6
  153. toil/test/src/realtimeLoggerTest.py +10 -6
  154. toil/test/src/regularLogTest.py +71 -37
  155. toil/test/src/resourceTest.py +80 -49
  156. toil/test/src/restartDAGTest.py +36 -22
  157. toil/test/src/resumabilityTest.py +9 -2
  158. toil/test/src/retainTempDirTest.py +45 -14
  159. toil/test/src/systemTest.py +12 -8
  160. toil/test/src/threadingTest.py +44 -25
  161. toil/test/src/toilContextManagerTest.py +10 -7
  162. toil/test/src/userDefinedJobArgTypeTest.py +8 -5
  163. toil/test/src/workerTest.py +73 -23
  164. toil/test/utils/toilDebugTest.py +103 -33
  165. toil/test/utils/toilKillTest.py +4 -5
  166. toil/test/utils/utilsTest.py +245 -106
  167. toil/test/wdl/wdltoil_test.py +818 -149
  168. toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
  169. toil/toilState.py +120 -35
  170. toil/utils/toilConfig.py +13 -4
  171. toil/utils/toilDebugFile.py +44 -27
  172. toil/utils/toilDebugJob.py +214 -27
  173. toil/utils/toilDestroyCluster.py +11 -6
  174. toil/utils/toilKill.py +8 -3
  175. toil/utils/toilLaunchCluster.py +256 -140
  176. toil/utils/toilMain.py +37 -16
  177. toil/utils/toilRsyncCluster.py +32 -14
  178. toil/utils/toilSshCluster.py +49 -22
  179. toil/utils/toilStats.py +356 -273
  180. toil/utils/toilStatus.py +292 -139
  181. toil/utils/toilUpdateEC2Instances.py +3 -1
  182. toil/version.py +12 -12
  183. toil/wdl/utils.py +5 -5
  184. toil/wdl/wdltoil.py +3913 -1033
  185. toil/worker.py +367 -184
  186. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
  187. toil-8.0.0.dist-info/METADATA +173 -0
  188. toil-8.0.0.dist-info/RECORD +253 -0
  189. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
  190. toil-6.1.0a1.dist-info/METADATA +0 -125
  191. toil-6.1.0a1.dist-info/RECORD +0 -237
  192. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
  193. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
@@ -14,34 +14,31 @@
14
14
  import logging
15
15
  import os
16
16
  from abc import ABC, abstractmethod
17
+ from collections.abc import Generator, Iterator
17
18
  from contextlib import contextmanager
18
19
  from tempfile import mkstemp
19
20
  from threading import Event, Semaphore
20
- from typing import (IO,
21
- TYPE_CHECKING,
22
- Any,
23
- Callable,
24
- ContextManager,
25
- Dict,
26
- Generator,
27
- Iterator,
28
- List,
29
- Literal,
30
- Optional,
31
- Set,
32
- Tuple,
33
- Type,
34
- Union,
35
- cast,
36
- overload)
21
+ from typing import (
22
+ IO,
23
+ TYPE_CHECKING,
24
+ Any,
25
+ Callable,
26
+ ContextManager,
27
+ Literal,
28
+ Optional,
29
+ Union,
30
+ cast,
31
+ overload,
32
+ )
37
33
 
38
34
  import dill
39
35
 
40
- from toil.common import Toil, cacheDirName
36
+ from toil.common import Toil, cacheDirName, getDirSizeRecursively
41
37
  from toil.fileStores import FileID
42
- from toil.job import Job, JobDescription
38
+ from toil.job import DebugStoppingPointReached, Job, JobDescription
43
39
  from toil.jobStores.abstractJobStore import AbstractJobStore
44
40
  from toil.lib.compatibility import deprecated
41
+ from toil.lib.conversions import bytes2human
45
42
  from toil.lib.io import WriteWatchingStream, mkdtemp
46
43
 
47
44
  logger = logging.getLogger(__name__)
@@ -75,9 +72,10 @@ class AbstractFileStore(ABC):
75
72
  Also responsible for committing completed jobs back to the job store with
76
73
  an update operation, and allowing that commit operation to be waited for.
77
74
  """
75
+
78
76
  # Variables used for syncing reads/writes
79
77
  _pendingFileWritesLock = Semaphore()
80
- _pendingFileWrites: Set[str] = set()
78
+ _pendingFileWrites: set[str] = set()
81
79
  _terminateEvent = Event() # Used to signify crashes in threads
82
80
 
83
81
  def __init__(
@@ -110,21 +108,28 @@ class AbstractFileStore(ABC):
110
108
  # This gets replaced with a subdirectory of itself on open()
111
109
  self.localTempDir: str = os.path.abspath(file_store_dir)
112
110
  assert self.jobStore.config.workflowID is not None
113
- self.workflow_dir: str = Toil.getLocalWorkflowDir(self.jobStore.config.workflowID, self.jobStore.config.workDir)
114
- self.coordination_dir: str =Toil.get_local_workflow_coordination_dir(self.jobStore.config.workflowID, self.jobStore.config.workDir, self.jobStore.config.coordination_dir)
115
- self.jobName: str = (
116
- self.jobDesc.command.split()[1] if self.jobDesc.command else ""
111
+ self.workflow_dir: str = Toil.getLocalWorkflowDir(
112
+ self.jobStore.config.workflowID, self.jobStore.config.workDir
113
+ )
114
+ self.coordination_dir: str = Toil.get_local_workflow_coordination_dir(
115
+ self.jobStore.config.workflowID,
116
+ self.jobStore.config.workDir,
117
+ self.jobStore.config.coordination_dir,
117
118
  )
119
+ self.jobName: str = str(self.jobDesc)
118
120
  self.waitForPreviousCommit = waitForPreviousCommit
119
- self.loggingMessages: List[Dict[str, Union[int, str]]] = []
121
+ self.logging_messages: list[dict[str, Union[int, str]]] = []
122
+ self.logging_user_streams: list[dict[str, str]] = []
120
123
  # Records file IDs of files deleted during the current job. Doesn't get
121
124
  # committed back until the job is completely successful, because if the
122
125
  # job is re-run it will need to be able to re-delete these files.
123
126
  # This is a set of str objects, not FileIDs.
124
- self.filesToDelete: Set[str] = set()
127
+ self.filesToDelete: set[str] = set()
125
128
  # Holds records of file ID, or file ID and local path, for reporting
126
129
  # the accessed files of failed jobs.
127
- self._accessLog: List[Tuple[str, ...]] = []
130
+ self._accessLog: list[tuple[str, ...]] = []
131
+ # Holds total bytes of observed disk usage for the last job run under open()
132
+ self._job_disk_used: Optional[int] = None
128
133
 
129
134
  @staticmethod
130
135
  def createFileStore(
@@ -139,13 +144,17 @@ class AbstractFileStore(ABC):
139
144
  from toil.fileStores.cachingFileStore import CachingFileStore
140
145
  from toil.fileStores.nonCachingFileStore import NonCachingFileStore
141
146
 
142
- fileStoreCls: Union[Type["CachingFileStore"], Type["NonCachingFileStore"]] = (
147
+ fileStoreCls: Union[type["CachingFileStore"], type["NonCachingFileStore"]] = (
143
148
  CachingFileStore if caching else NonCachingFileStore
144
149
  )
145
150
  return fileStoreCls(jobStore, jobDesc, file_store_dir, waitForPreviousCommit)
146
151
 
147
152
  @staticmethod
148
- def shutdownFileStore(workflowID: str, config_work_dir: Optional[str], config_coordination_dir: Optional[str]) -> None:
153
+ def shutdownFileStore(
154
+ workflowID: str,
155
+ config_work_dir: Optional[str],
156
+ config_coordination_dir: Optional[str],
157
+ ) -> None:
149
158
  """
150
159
  Carry out any necessary filestore-specific cleanup.
151
160
 
@@ -165,7 +174,9 @@ class AbstractFileStore(ABC):
165
174
  from toil.fileStores.nonCachingFileStore import NonCachingFileStore
166
175
 
167
176
  workflowDir = Toil.getLocalWorkflowDir(workflowID, config_work_dir)
168
- coordination_dir = Toil.get_local_workflow_coordination_dir(workflowID, config_work_dir, config_coordination_dir)
177
+ coordination_dir = Toil.get_local_workflow_coordination_dir(
178
+ workflowID, config_work_dir, config_coordination_dir
179
+ )
169
180
  cacheDir = os.path.join(workflowDir, cacheDirName(workflowID))
170
181
  if os.path.exists(cacheDir):
171
182
  # The presence of the cacheDir suggests this was a cached run. We don't need
@@ -187,15 +198,46 @@ class AbstractFileStore(ABC):
187
198
 
188
199
  :param job: The job instance of the toil job to run.
189
200
  """
190
- failed = True
201
+ job_requested_disk = job.disk
191
202
  try:
192
203
  yield
193
204
  failed = False
194
- finally:
195
- # Do a finally instead of an except/raise because we don't want
196
- # to appear as "another exception occurred" in the stack trace.
197
- if failed:
205
+ except BaseException as e:
206
+ if isinstance(e, DebugStoppingPointReached):
207
+ self._dumpAccessLogs(job_type="Debugged", log_level=logging.INFO)
208
+ else:
198
209
  self._dumpAccessLogs()
210
+ raise
211
+ finally:
212
+ # See how much disk space is used at the end of the job.
213
+ # Not a real peak disk usage, but close enough to be useful for warning the user.
214
+ self._job_disk_used = getDirSizeRecursively(self.localTempDir)
215
+
216
+ # Report disk usage
217
+ percent: float = 0.0
218
+ if job_requested_disk and job_requested_disk > 0:
219
+ percent = float(self._job_disk_used) / job_requested_disk * 100
220
+ disk_usage: str = (
221
+ f"Job {self.jobName} used {percent:.2f}% disk ({bytes2human(self._job_disk_used)}B [{self._job_disk_used}B] used, "
222
+ f"{bytes2human(job_requested_disk)}B [{job_requested_disk}B] requested)."
223
+ )
224
+ if self._job_disk_used > job_requested_disk:
225
+ self.log_to_leader(
226
+ "Job used more disk than requested. For CWL, consider increasing the outdirMin "
227
+ f"requirement, otherwise, consider increasing the disk requirement. {disk_usage}",
228
+ level=logging.WARNING,
229
+ )
230
+ else:
231
+ self.log_to_leader(disk_usage, level=logging.DEBUG)
232
+
233
+ def get_disk_usage(self) -> Optional[int]:
234
+ """
235
+ Get the number of bytes of disk used by the last job run under open().
236
+
237
+ Disk usage is measured at the end of the job.
238
+ TODO: Sample periodically and record peak usage.
239
+ """
240
+ return self._job_disk_used
199
241
 
200
242
  # Functions related to temp files and directories
201
243
  def getLocalTempDir(self) -> str:
@@ -211,7 +253,9 @@ class AbstractFileStore(ABC):
211
253
  """
212
254
  return os.path.abspath(mkdtemp(dir=self.localTempDir))
213
255
 
214
- def getLocalTempFile(self, suffix: Optional[str] = None, prefix: Optional[str] = None) -> str:
256
+ def getLocalTempFile(
257
+ self, suffix: Optional[str] = None, prefix: Optional[str] = None
258
+ ) -> str:
215
259
  """
216
260
  Get a new local temporary file that will persist for the duration of the job.
217
261
 
@@ -228,12 +272,14 @@ class AbstractFileStore(ABC):
228
272
  handle, tmpFile = mkstemp(
229
273
  suffix=".tmp" if suffix is None else suffix,
230
274
  prefix="tmp" if prefix is None else prefix,
231
- dir=self.localTempDir
275
+ dir=self.localTempDir,
232
276
  )
233
277
  os.close(handle)
234
278
  return os.path.abspath(tmpFile)
235
279
 
236
- def getLocalTempFileName(self, suffix: Optional[str] = None, prefix: Optional[str] = None) -> str:
280
+ def getLocalTempFileName(
281
+ self, suffix: Optional[str] = None, prefix: Optional[str] = None
282
+ ) -> str:
237
283
  """
238
284
  Get a valid name for a new local file. Don't actually create a file at the path.
239
285
 
@@ -287,7 +333,7 @@ class AbstractFileStore(ABC):
287
333
  basename: Optional[str] = None,
288
334
  encoding: Optional[str] = None,
289
335
  errors: Optional[str] = None,
290
- ) -> Iterator[Tuple[WriteWatchingStream, FileID]]:
336
+ ) -> Iterator[tuple[WriteWatchingStream, FileID]]:
291
337
  """
292
338
  Similar to writeGlobalFile, but allows the writing of a stream to the job store.
293
339
  The yielded file handle does not need to and should not be closed explicitly.
@@ -327,18 +373,23 @@ class AbstractFileStore(ABC):
327
373
  def handle(numBytes: int) -> None:
328
374
  # No scope problem here, because we don't assign to a fileID local
329
375
  fileID.size += numBytes
376
+
330
377
  wrappedStream.onWrite(handle)
331
378
 
332
379
  yield wrappedStream, fileID
333
380
 
334
- def _dumpAccessLogs(self) -> None:
381
+ def _dumpAccessLogs(
382
+ self, job_type: str = "Failed", log_level: int = logging.WARNING
383
+ ) -> None:
335
384
  """
336
- When something goes wrong, log a report.
385
+ Log a report of the files accessed.
337
386
 
338
387
  Includes the files that were accessed while the file store was open.
388
+
389
+ :param job_type: Adjective to describe the job in the report.
339
390
  """
340
391
  if len(self._accessLog) > 0:
341
- logger.warning('Failed job accessed files:')
392
+ logger.log(log_level, "%s job accessed files:", job_type)
342
393
 
343
394
  for item in self._accessLog:
344
395
  # For each access record
@@ -347,14 +398,29 @@ class AbstractFileStore(ABC):
347
398
  file_id, dest_path = item
348
399
  if os.path.exists(dest_path):
349
400
  if os.path.islink(dest_path):
350
- logger.warning('Symlinked file \'%s\' to path \'%s\'', file_id, dest_path)
401
+ logger.log(
402
+ log_level,
403
+ "Symlinked file '%s' to path '%s'",
404
+ file_id,
405
+ dest_path,
406
+ )
351
407
  else:
352
- logger.warning('Downloaded file \'%s\' to path \'%s\'', file_id, dest_path)
408
+ logger.log(
409
+ log_level,
410
+ "Downloaded file '%s' to path '%s'",
411
+ file_id,
412
+ dest_path,
413
+ )
353
414
  else:
354
- logger.warning('Downloaded file \'%s\' to path \'%s\' (gone!)', file_id, dest_path)
415
+ logger.log(
416
+ log_level,
417
+ "Downloaded file '%s' to path '%s' (gone!)",
418
+ file_id,
419
+ dest_path,
420
+ )
355
421
  else:
356
422
  # Otherwise dump without the name
357
- logger.warning('Streamed file \'%s\'', *item)
423
+ logger.log(log_level, "Streamed file '%s'", *item)
358
424
 
359
425
  def logAccess(
360
426
  self, fileStoreID: Union[FileID, str], destination: Union[str, None] = None
@@ -421,14 +487,12 @@ class AbstractFileStore(ABC):
421
487
  fileStoreID: str,
422
488
  encoding: Literal[None] = None,
423
489
  errors: Optional[str] = None,
424
- ) -> ContextManager[IO[bytes]]:
425
- ...
490
+ ) -> ContextManager[IO[bytes]]: ...
426
491
 
427
492
  @overload
428
493
  def readGlobalFileStream(
429
494
  self, fileStoreID: str, encoding: str, errors: Optional[str] = None
430
- ) -> ContextManager[IO[str]]:
431
- ...
495
+ ) -> ContextManager[IO[str]]: ...
432
496
 
433
497
  @abstractmethod
434
498
  def readGlobalFileStream(
@@ -472,7 +536,7 @@ class AbstractFileStore(ABC):
472
536
  :return: File's size in bytes, as stored in the job store
473
537
  """
474
538
  # First try and see if the size is still attached
475
- size = getattr(fileStoreID, 'size', None)
539
+ size = getattr(fileStoreID, "size", None)
476
540
 
477
541
  if size is None:
478
542
  # It fell off
@@ -525,7 +589,7 @@ class AbstractFileStore(ABC):
525
589
  ) -> Optional[FileID]:
526
590
  return self.jobStore.import_file(src_uri, shared_file_name=shared_file_name)
527
591
 
528
- @deprecated(new_function_name='export_file')
592
+ @deprecated(new_function_name="export_file")
529
593
  def exportFile(self, jobStoreFileID: FileID, dstUrl: str) -> None:
530
594
  return self.export_file(jobStoreFileID, dstUrl)
531
595
 
@@ -554,7 +618,7 @@ class AbstractFileStore(ABC):
554
618
  class _StateFile:
555
619
  """Read and write dill-ed state dictionaries from/to a file into a namespace."""
556
620
 
557
- def __init__(self, stateDict: Dict[str, Any]):
621
+ def __init__(self, stateDict: dict[str, Any]):
558
622
  assert isinstance(stateDict, dict)
559
623
  self.__dict__.update(stateDict)
560
624
 
@@ -582,7 +646,7 @@ class AbstractFileStore(ABC):
582
646
  """
583
647
  # Read the value from the cache state file then initialize and instance of
584
648
  # _CacheState with it.
585
- with open(fileName, 'rb') as fH:
649
+ with open(fileName, "rb") as fH:
586
650
  infoDict = dill.load(fH)
587
651
  return cls(infoDict)
588
652
 
@@ -592,14 +656,14 @@ class AbstractFileStore(ABC):
592
656
 
593
657
  :param fileName: Path to the state file.
594
658
  """
595
- with open(fileName + '.tmp', 'wb') as fH:
659
+ with open(fileName + ".tmp", "wb") as fH:
596
660
  # Based on answer by user "Mark" at:
597
661
  # http://stackoverflow.com/questions/2709800/how-to-pickle-yourself
598
662
  # We can't pickle nested classes. So we have to pickle the variables
599
663
  # of the class.
600
664
  # If we ever change this, we need to ensure it doesn't break FileID
601
665
  dill.dump(self.__dict__, fH)
602
- os.rename(fileName + '.tmp', fileName)
666
+ os.rename(fileName + ".tmp", fileName)
603
667
 
604
668
  # Functions related to logging
605
669
  def log_to_leader(self, text: str, level: int = logging.INFO) -> None:
@@ -611,13 +675,29 @@ class AbstractFileStore(ABC):
611
675
  :param level: The logging level.
612
676
  """
613
677
  logger.log(level=level, msg=("LOG-TO-MASTER: " + text))
614
- self.loggingMessages.append(dict(text=text, level=level))
615
-
678
+ self.logging_messages.append(dict(text=text, level=level))
616
679
 
617
- @deprecated(new_function_name='export_file')
680
+ @deprecated(new_function_name="export_file")
618
681
  def logToMaster(self, text: str, level: int = logging.INFO) -> None:
619
682
  self.log_to_leader(text, level)
620
-
683
+
684
+ def log_user_stream(self, name: str, stream: IO[bytes]) -> None:
685
+ """
686
+ Send a stream of UTF-8 text to the leader as a named log stream.
687
+
688
+ Useful for things like the error logs of Docker containers. The leader
689
+ will show it to the user or organize it appropriately for user-level
690
+ log information.
691
+
692
+ :param name: A hierarchical, .-delimited string.
693
+ :param stream: A stream of encoded text. Encoding errors will be
694
+ tolerated.
695
+ """
696
+
697
+ # Read the whole stream into memory
698
+ steam_data = stream.read().decode("utf-8", errors="replace")
699
+ # And remember it for the worker to fish out
700
+ self.logging_user_streams.append(dict(name=name, text=steam_data))
621
701
 
622
702
  # Functions run after the completion of the job.
623
703
  @abstractmethod