toil 5.12.0__py3-none-any.whl → 6.1.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 (164) hide show
  1. toil/__init__.py +18 -13
  2. toil/batchSystems/abstractBatchSystem.py +39 -13
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +24 -24
  4. toil/batchSystems/awsBatch.py +14 -14
  5. toil/batchSystems/cleanup_support.py +7 -3
  6. toil/batchSystems/contained_executor.py +3 -3
  7. toil/batchSystems/htcondor.py +0 -1
  8. toil/batchSystems/kubernetes.py +34 -31
  9. toil/batchSystems/local_support.py +3 -1
  10. toil/batchSystems/lsf.py +7 -7
  11. toil/batchSystems/mesos/batchSystem.py +7 -7
  12. toil/batchSystems/options.py +32 -83
  13. toil/batchSystems/registry.py +104 -23
  14. toil/batchSystems/singleMachine.py +16 -13
  15. toil/batchSystems/slurm.py +87 -16
  16. toil/batchSystems/torque.py +0 -1
  17. toil/bus.py +44 -8
  18. toil/common.py +544 -753
  19. toil/cwl/__init__.py +28 -32
  20. toil/cwl/cwltoil.py +595 -574
  21. toil/cwl/utils.py +55 -10
  22. toil/exceptions.py +1 -1
  23. toil/fileStores/__init__.py +2 -2
  24. toil/fileStores/abstractFileStore.py +88 -14
  25. toil/fileStores/cachingFileStore.py +610 -549
  26. toil/fileStores/nonCachingFileStore.py +46 -22
  27. toil/job.py +182 -101
  28. toil/jobStores/abstractJobStore.py +161 -95
  29. toil/jobStores/aws/jobStore.py +23 -9
  30. toil/jobStores/aws/utils.py +6 -6
  31. toil/jobStores/fileJobStore.py +116 -18
  32. toil/jobStores/googleJobStore.py +16 -7
  33. toil/jobStores/utils.py +5 -6
  34. toil/leader.py +87 -56
  35. toil/lib/accelerators.py +10 -5
  36. toil/lib/aws/__init__.py +3 -14
  37. toil/lib/aws/ami.py +22 -9
  38. toil/lib/aws/iam.py +21 -13
  39. toil/lib/aws/session.py +2 -16
  40. toil/lib/aws/utils.py +4 -5
  41. toil/lib/compatibility.py +1 -1
  42. toil/lib/conversions.py +26 -3
  43. toil/lib/docker.py +22 -23
  44. toil/lib/ec2.py +10 -6
  45. toil/lib/ec2nodes.py +106 -100
  46. toil/lib/encryption/_nacl.py +2 -1
  47. toil/lib/generatedEC2Lists.py +325 -18
  48. toil/lib/io.py +49 -2
  49. toil/lib/misc.py +1 -1
  50. toil/lib/resources.py +9 -2
  51. toil/lib/threading.py +101 -38
  52. toil/options/common.py +736 -0
  53. toil/options/cwl.py +336 -0
  54. toil/options/wdl.py +37 -0
  55. toil/provisioners/abstractProvisioner.py +9 -4
  56. toil/provisioners/aws/__init__.py +3 -6
  57. toil/provisioners/aws/awsProvisioner.py +6 -0
  58. toil/provisioners/clusterScaler.py +3 -2
  59. toil/provisioners/gceProvisioner.py +2 -2
  60. toil/realtimeLogger.py +2 -1
  61. toil/resource.py +24 -18
  62. toil/server/app.py +2 -3
  63. toil/server/cli/wes_cwl_runner.py +4 -4
  64. toil/server/utils.py +1 -1
  65. toil/server/wes/abstract_backend.py +3 -2
  66. toil/server/wes/amazon_wes_utils.py +5 -4
  67. toil/server/wes/tasks.py +2 -3
  68. toil/server/wes/toil_backend.py +2 -10
  69. toil/server/wsgi_app.py +2 -0
  70. toil/serviceManager.py +12 -10
  71. toil/statsAndLogging.py +41 -9
  72. toil/test/__init__.py +29 -54
  73. toil/test/batchSystems/batchSystemTest.py +11 -111
  74. toil/test/batchSystems/test_slurm.py +24 -8
  75. toil/test/cactus/__init__.py +0 -0
  76. toil/test/cactus/test_cactus_integration.py +58 -0
  77. toil/test/cwl/cwlTest.py +438 -223
  78. toil/test/cwl/glob_dir.cwl +15 -0
  79. toil/test/cwl/preemptible.cwl +21 -0
  80. toil/test/cwl/preemptible_expression.cwl +28 -0
  81. toil/test/cwl/revsort.cwl +1 -1
  82. toil/test/cwl/revsort2.cwl +1 -1
  83. toil/test/docs/scriptsTest.py +2 -3
  84. toil/test/jobStores/jobStoreTest.py +34 -21
  85. toil/test/lib/aws/test_iam.py +4 -14
  86. toil/test/lib/aws/test_utils.py +0 -3
  87. toil/test/lib/dockerTest.py +4 -4
  88. toil/test/lib/test_ec2.py +12 -17
  89. toil/test/mesos/helloWorld.py +4 -5
  90. toil/test/mesos/stress.py +1 -1
  91. toil/test/{wdl/conftest.py → options/__init__.py} +0 -10
  92. toil/test/options/options.py +37 -0
  93. toil/test/provisioners/aws/awsProvisionerTest.py +9 -5
  94. toil/test/provisioners/clusterScalerTest.py +6 -4
  95. toil/test/provisioners/clusterTest.py +23 -11
  96. toil/test/provisioners/gceProvisionerTest.py +0 -6
  97. toil/test/provisioners/restartScript.py +3 -2
  98. toil/test/server/serverTest.py +1 -1
  99. toil/test/sort/restart_sort.py +2 -1
  100. toil/test/sort/sort.py +2 -1
  101. toil/test/sort/sortTest.py +2 -13
  102. toil/test/src/autoDeploymentTest.py +45 -45
  103. toil/test/src/busTest.py +5 -5
  104. toil/test/src/checkpointTest.py +2 -2
  105. toil/test/src/deferredFunctionTest.py +1 -1
  106. toil/test/src/fileStoreTest.py +32 -16
  107. toil/test/src/helloWorldTest.py +1 -1
  108. toil/test/src/importExportFileTest.py +1 -1
  109. toil/test/src/jobDescriptionTest.py +2 -1
  110. toil/test/src/jobServiceTest.py +1 -1
  111. toil/test/src/jobTest.py +18 -18
  112. toil/test/src/miscTests.py +5 -3
  113. toil/test/src/promisedRequirementTest.py +3 -3
  114. toil/test/src/realtimeLoggerTest.py +1 -1
  115. toil/test/src/resourceTest.py +2 -2
  116. toil/test/src/restartDAGTest.py +1 -1
  117. toil/test/src/resumabilityTest.py +36 -2
  118. toil/test/src/retainTempDirTest.py +1 -1
  119. toil/test/src/systemTest.py +2 -2
  120. toil/test/src/toilContextManagerTest.py +2 -2
  121. toil/test/src/userDefinedJobArgTypeTest.py +1 -1
  122. toil/test/utils/toilDebugTest.py +98 -32
  123. toil/test/utils/toilKillTest.py +2 -2
  124. toil/test/utils/utilsTest.py +23 -3
  125. toil/test/wdl/wdltoil_test.py +223 -45
  126. toil/toilState.py +7 -6
  127. toil/utils/toilClean.py +1 -1
  128. toil/utils/toilConfig.py +36 -0
  129. toil/utils/toilDebugFile.py +60 -33
  130. toil/utils/toilDebugJob.py +39 -12
  131. toil/utils/toilDestroyCluster.py +1 -1
  132. toil/utils/toilKill.py +1 -1
  133. toil/utils/toilLaunchCluster.py +13 -2
  134. toil/utils/toilMain.py +3 -2
  135. toil/utils/toilRsyncCluster.py +1 -1
  136. toil/utils/toilSshCluster.py +1 -1
  137. toil/utils/toilStats.py +445 -305
  138. toil/utils/toilStatus.py +2 -5
  139. toil/version.py +10 -10
  140. toil/wdl/utils.py +2 -122
  141. toil/wdl/wdltoil.py +1257 -492
  142. toil/worker.py +55 -46
  143. toil-6.1.0.dist-info/METADATA +124 -0
  144. toil-6.1.0.dist-info/RECORD +241 -0
  145. {toil-5.12.0.dist-info → toil-6.1.0.dist-info}/WHEEL +1 -1
  146. {toil-5.12.0.dist-info → toil-6.1.0.dist-info}/entry_points.txt +0 -1
  147. toil/batchSystems/parasol.py +0 -379
  148. toil/batchSystems/tes.py +0 -459
  149. toil/test/batchSystems/parasolTestSupport.py +0 -117
  150. toil/test/wdl/builtinTest.py +0 -506
  151. toil/test/wdl/toilwdlTest.py +0 -522
  152. toil/wdl/toilwdl.py +0 -141
  153. toil/wdl/versions/dev.py +0 -107
  154. toil/wdl/versions/draft2.py +0 -980
  155. toil/wdl/versions/v1.py +0 -794
  156. toil/wdl/wdl_analysis.py +0 -116
  157. toil/wdl/wdl_functions.py +0 -997
  158. toil/wdl/wdl_synthesis.py +0 -1011
  159. toil/wdl/wdl_types.py +0 -243
  160. toil-5.12.0.dist-info/METADATA +0 -118
  161. toil-5.12.0.dist-info/RECORD +0 -244
  162. /toil/{wdl/versions → options}/__init__.py +0 -0
  163. {toil-5.12.0.dist-info → toil-6.1.0.dist-info}/LICENSE +0 -0
  164. {toil-5.12.0.dist-info → toil-6.1.0.dist-info}/top_level.txt +0 -0
toil/cwl/utils.py CHANGED
@@ -16,6 +16,9 @@
16
16
 
17
17
  import logging
18
18
  import os
19
+ from pathlib import PurePosixPath
20
+ import posixpath
21
+ import stat
19
22
  from typing import (
20
23
  Any,
21
24
  Callable,
@@ -24,7 +27,6 @@ from typing import (
24
27
  List,
25
28
  MutableMapping,
26
29
  MutableSequence,
27
- Tuple,
28
30
  Type,
29
31
  TypeVar,
30
32
  Union,
@@ -32,6 +34,7 @@ from typing import (
32
34
 
33
35
  from toil.fileStores import FileID
34
36
  from toil.fileStores.abstractFileStore import AbstractFileStore
37
+ from toil.jobStores.abstractJobStore import AbstractJobStore
35
38
 
36
39
  logger = logging.getLogger(__name__)
37
40
 
@@ -129,6 +132,32 @@ def visit_cwl_class_and_reduce(
129
132
 
130
133
  DirectoryStructure = Dict[str, Union[str, "DirectoryStructure"]]
131
134
 
135
+ def get_from_structure(dir_dict: DirectoryStructure, path: str) -> Union[str, DirectoryStructure, None]:
136
+ """
137
+ Given a relative path, follow it in the given directory structure.
138
+
139
+ Return the string URI for files, the directory dict for
140
+ subdirectories, or None for nonexistent things.
141
+ """
142
+
143
+ # Resolve .. and split into path components
144
+ parts = PurePosixPath(posixpath.normpath(path)).parts
145
+ if len(parts) == 0:
146
+ return dir_dict
147
+ if parts[0] in ('..', '/'):
148
+ raise RuntimeError(f"Path {path} not resolvable in virtual directory")
149
+ found: Union[str, DirectoryStructure] = dir_dict
150
+ for part in parts:
151
+ # Go down by each path component in turn
152
+ if isinstance(found, str):
153
+ # Looking for a subdirectory of a file, which doesn't exist
154
+ return None
155
+ if part not in found:
156
+ return None
157
+ found = found[part]
158
+ # Now we're at the place we want to be.
159
+ return found
160
+
132
161
 
133
162
  def download_structure(
134
163
  file_store: AbstractFileStore,
@@ -140,17 +169,21 @@ def download_structure(
140
169
  """
141
170
  Download nested dictionary from the Toil file store to a local path.
142
171
 
172
+ Guaranteed to fill the structure with real files, and not symlinks out of
173
+ it to elsewhere. File URIs may be toilfile: URIs or any other URI that
174
+ Toil's job store system can read.
175
+
143
176
  :param file_store: The Toil file store to download from.
144
177
 
145
- :param index: Maps from downloaded file path back to input Toil URI.
178
+ :param index: Maps from downloaded file path back to input URI.
146
179
 
147
180
  :param existing: Maps from file_store_id URI to downloaded file path.
148
181
 
149
182
  :param dir_dict: a dict from string to string (for files) or dict (for
150
- subdirectories) describing a directory structure.
183
+ subdirectories) describing a directory structure.
151
184
 
152
185
  :param into_dir: The directory to download the top-level dict's files
153
- into.
186
+ into.
154
187
  """
155
188
  logger.debug("Downloading directory with %s items", len(dir_dict))
156
189
 
@@ -167,14 +200,26 @@ def download_structure(
167
200
  download_structure(file_store, index, existing, value, subdir)
168
201
  else:
169
202
  # This must be a file path uploaded to Toil.
170
- assert isinstance(value, str)
171
- assert value.startswith("toilfile:")
203
+ if not isinstance(value, str):
204
+ raise RuntimeError(f"Did not find a file at {value}.")
205
+
172
206
  logger.debug("Downloading contained file '%s'", name)
173
207
  dest_path = os.path.join(into_dir, name)
174
- # So download the file into place
175
- file_store.readGlobalFile(
176
- FileID.unpack(value[len("toilfile:") :]), dest_path, symlink=True
177
- )
208
+
209
+ if value.startswith("toilfile:"):
210
+ # So download the file into place.
211
+ # Make sure to get a real copy of the file because we may need to
212
+ # mount the directory into a container as a whole.
213
+ file_store.readGlobalFile(
214
+ FileID.unpack(value[len("toilfile:") :]), dest_path, symlink=False
215
+ )
216
+ else:
217
+ # We need to download from some other kind of URL.
218
+ size, executable = AbstractJobStore.read_from_url(value, open(dest_path, 'wb'))
219
+ if executable:
220
+ # Make the written file executable
221
+ os.chmod(dest_path, os.stat(dest_path).st_mode | stat.S_IXUSR)
222
+
178
223
  # Update the index dicts
179
224
  # TODO: why?
180
225
  index[dest_path] = value
toil/exceptions.py CHANGED
@@ -36,7 +36,7 @@ class FailedJobsException(Exception):
36
36
  for job_desc in failed_jobs:
37
37
  if job_desc.logJobStoreFileID:
38
38
  with job_desc.getLogFileHandle(job_store) as f:
39
- self.msg += "\n" + StatsAndLogging.formatLogStream(f, job_desc)
39
+ self.msg += "\n" + StatsAndLogging.formatLogStream(f, f'Log from job "{job_desc}"')
40
40
  # catch failures to prepare more complex details and only return the basics
41
41
  except Exception:
42
42
  logger.exception("Exception when compiling information about failed jobs")
@@ -40,7 +40,7 @@ class FileID(str):
40
40
 
41
41
  def pack(self) -> str:
42
42
  """Pack the FileID into a string so it can be passed through external code."""
43
- return f'{self.size}:{int(self.executable)}:{self}'
43
+ return f'{self.size}:{"1" if self.executable else "0"}:{self}'
44
44
 
45
45
  @classmethod
46
46
  def forPath(cls, fileStoreID: str, filePath: str) -> 'FileID':
@@ -54,7 +54,7 @@ class FileID(str):
54
54
  vals = packedFileStoreID.split(':', 2)
55
55
  # Break up the packed value
56
56
  size = int(vals[0])
57
- executable = bool(vals[1])
57
+ executable = (vals[1] == "1")
58
58
  value = vals[2]
59
59
  # Create the FileID
60
60
  return cls(value, size, executable)
@@ -13,9 +13,9 @@
13
13
  # limitations under the License.
14
14
  import logging
15
15
  import os
16
- import tempfile
17
16
  from abc import ABC, abstractmethod
18
17
  from contextlib import contextmanager
18
+ from tempfile import mkstemp
19
19
  from threading import Event, Semaphore
20
20
  from typing import (IO,
21
21
  TYPE_CHECKING,
@@ -26,21 +26,24 @@ from typing import (IO,
26
26
  Generator,
27
27
  Iterator,
28
28
  List,
29
+ Literal,
29
30
  Optional,
30
31
  Set,
31
32
  Tuple,
32
33
  Type,
33
34
  Union,
34
- cast)
35
+ cast,
36
+ overload)
35
37
 
36
38
  import dill
37
39
 
38
- from toil.common import Toil, cacheDirName
40
+ from toil.common import Toil, cacheDirName, getDirSizeRecursively
39
41
  from toil.fileStores import FileID
40
42
  from toil.job import Job, JobDescription
41
43
  from toil.jobStores.abstractJobStore import AbstractJobStore
42
44
  from toil.lib.compatibility import deprecated
43
- from toil.lib.io import WriteWatchingStream
45
+ from toil.lib.conversions import bytes2human
46
+ from toil.lib.io import WriteWatchingStream, mkdtemp
44
47
 
45
48
  logger = logging.getLogger(__name__)
46
49
 
@@ -114,19 +117,18 @@ class AbstractFileStore(ABC):
114
117
  self.jobDesc.command.split()[1] if self.jobDesc.command else ""
115
118
  )
116
119
  self.waitForPreviousCommit = waitForPreviousCommit
117
- self.loggingMessages: List[Dict[str, Union[int, str]]] = []
120
+ self.logging_messages: List[Dict[str, Union[int, str]]] = []
121
+ self.logging_user_streams: List[dict[str, str]] = []
118
122
  # Records file IDs of files deleted during the current job. Doesn't get
119
123
  # committed back until the job is completely successful, because if the
120
124
  # job is re-run it will need to be able to re-delete these files.
121
125
  # This is a set of str objects, not FileIDs.
122
126
  self.filesToDelete: Set[str] = set()
123
- # Records IDs of jobs that need to be deleted when the currently
124
- # running job is cleaned up.
125
- # May be modified by the worker to actually delete jobs!
126
- self.jobsToDelete: Set[str] = set()
127
127
  # Holds records of file ID, or file ID and local path, for reporting
128
128
  # the accessed files of failed jobs.
129
129
  self._accessLog: List[Tuple[str, ...]] = []
130
+ # Holds total bytes of observed disk usage for the last job run under open()
131
+ self._job_disk_used: Optional[int] = None
130
132
 
131
133
  @staticmethod
132
134
  def createFileStore(
@@ -190,6 +192,7 @@ class AbstractFileStore(ABC):
190
192
  :param job: The job instance of the toil job to run.
191
193
  """
192
194
  failed = True
195
+ job_requested_disk = job.disk
193
196
  try:
194
197
  yield
195
198
  failed = False
@@ -199,6 +202,33 @@ class AbstractFileStore(ABC):
199
202
  if failed:
200
203
  self._dumpAccessLogs()
201
204
 
205
+ # See how much disk space is used at the end of the job.
206
+ # Not a real peak disk usage, but close enough to be useful for warning the user.
207
+ self._job_disk_used = getDirSizeRecursively(self.localTempDir)
208
+
209
+ # Report disk usage
210
+ percent: float = 0.0
211
+ if job_requested_disk and job_requested_disk > 0:
212
+ percent = float(self._job_disk_used) / job_requested_disk * 100
213
+ disk_usage: str = (f"Job {self.jobName} used {percent:.2f}% disk ({bytes2human(self._job_disk_used)}B [{self._job_disk_used}B] used, "
214
+ f"{bytes2human(job_requested_disk)}B [{job_requested_disk}B] requested).")
215
+ if self._job_disk_used > job_requested_disk:
216
+ self.log_to_leader("Job used more disk than requested. For CWL, consider increasing the outdirMin "
217
+ f"requirement, otherwise, consider increasing the disk requirement. {disk_usage}",
218
+ level=logging.WARNING)
219
+ else:
220
+ self.log_to_leader(disk_usage, level=logging.DEBUG)
221
+
222
+ def get_disk_usage(self) -> Optional[int]:
223
+ """
224
+ Get the number of bytes of disk used by the last job run under open().
225
+
226
+ Disk usage is measured at the end of the job.
227
+ TODO: Sample periodically and record peak usage.
228
+ """
229
+ return self._job_disk_used
230
+
231
+
202
232
  # Functions related to temp files and directories
203
233
  def getLocalTempDir(self) -> str:
204
234
  """
@@ -211,7 +241,7 @@ class AbstractFileStore(ABC):
211
241
  to be deleted once the job terminates, removing all files it
212
242
  contains recursively.
213
243
  """
214
- return os.path.abspath(tempfile.mkdtemp(dir=self.localTempDir))
244
+ return os.path.abspath(mkdtemp(dir=self.localTempDir))
215
245
 
216
246
  def getLocalTempFile(self, suffix: Optional[str] = None, prefix: Optional[str] = None) -> str:
217
247
  """
@@ -227,7 +257,7 @@ class AbstractFileStore(ABC):
227
257
  for the duration of the job only, and is guaranteed to be deleted
228
258
  once the job terminates.
229
259
  """
230
- handle, tmpFile = tempfile.mkstemp(
260
+ handle, tmpFile = mkstemp(
231
261
  suffix=".tmp" if suffix is None else suffix,
232
262
  prefix="tmp" if prefix is None else prefix,
233
263
  dir=self.localTempDir
@@ -417,6 +447,21 @@ class AbstractFileStore(ABC):
417
447
  """
418
448
  raise NotImplementedError()
419
449
 
450
+ @overload
451
+ def readGlobalFileStream(
452
+ self,
453
+ fileStoreID: str,
454
+ encoding: Literal[None] = None,
455
+ errors: Optional[str] = None,
456
+ ) -> ContextManager[IO[bytes]]:
457
+ ...
458
+
459
+ @overload
460
+ def readGlobalFileStream(
461
+ self, fileStoreID: str, encoding: str, errors: Optional[str] = None
462
+ ) -> ContextManager[IO[str]]:
463
+ ...
464
+
420
465
  @abstractmethod
421
466
  def readGlobalFileStream(
422
467
  self,
@@ -589,7 +634,7 @@ class AbstractFileStore(ABC):
589
634
  os.rename(fileName + '.tmp', fileName)
590
635
 
591
636
  # Functions related to logging
592
- def logToMaster(self, text: str, level: int = logging.INFO) -> None:
637
+ def log_to_leader(self, text: str, level: int = logging.INFO) -> None:
593
638
  """
594
639
  Send a logging message to the leader. The message will also be \
595
640
  logged by the worker at the same level.
@@ -598,7 +643,30 @@ class AbstractFileStore(ABC):
598
643
  :param level: The logging level.
599
644
  """
600
645
  logger.log(level=level, msg=("LOG-TO-MASTER: " + text))
601
- self.loggingMessages.append(dict(text=text, level=level))
646
+ self.logging_messages.append(dict(text=text, level=level))
647
+
648
+
649
+ @deprecated(new_function_name='export_file')
650
+ def logToMaster(self, text: str, level: int = logging.INFO) -> None:
651
+ self.log_to_leader(text, level)
652
+
653
+ def log_user_stream(self, name: str, stream: IO[bytes]) -> None:
654
+ """
655
+ Send a stream of UTF-8 text to the leader as a named log stream.
656
+
657
+ Useful for things like the error logs of Docker containers. The leader
658
+ will show it to the user or organize it appropriately for user-level
659
+ log information.
660
+
661
+ :param name: A hierarchical, .-delimited string.
662
+ :param stream: A stream of encoded text. Encoding errors will be
663
+ tolerated.
664
+ """
665
+
666
+ # Read the whole stream into memory
667
+ steam_data = stream.read().decode('utf-8', errors='replace')
668
+ # And remember it for the worker to fish out
669
+ self.logging_user_streams.append(dict(name=name, text=steam_data))
602
670
 
603
671
  # Functions run after the completion of the job.
604
672
  @abstractmethod
@@ -606,7 +674,13 @@ class AbstractFileStore(ABC):
606
674
  """
607
675
  Update the status of the job on the disk.
608
676
 
609
- May start an asynchronous process. Call waitForCommit() to wait on that process.
677
+ May bump the version number of the job.
678
+
679
+ May start an asynchronous process. Call waitForCommit() to wait on that
680
+ process. You must waitForCommit() before committing any further updates
681
+ to the job. During the asynchronous process, it is safe to modify the
682
+ job; modifications after this call will not be committed until the next
683
+ call.
610
684
 
611
685
  :param jobState: If True, commit the state of the FileStore's job,
612
686
  and file deletes. Otherwise, commit only file creates/updates.