toil 9.1.1__py3-none-any.whl → 9.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toil/__init__.py +5 -9
- toil/batchSystems/abstractBatchSystem.py +23 -22
- toil/batchSystems/abstractGridEngineBatchSystem.py +17 -12
- toil/batchSystems/awsBatch.py +8 -8
- toil/batchSystems/cleanup_support.py +4 -4
- toil/batchSystems/contained_executor.py +3 -3
- toil/batchSystems/gridengine.py +3 -4
- toil/batchSystems/htcondor.py +5 -5
- toil/batchSystems/kubernetes.py +65 -63
- toil/batchSystems/local_support.py +2 -3
- toil/batchSystems/lsf.py +6 -7
- toil/batchSystems/mesos/batchSystem.py +11 -7
- toil/batchSystems/mesos/test/__init__.py +1 -2
- toil/batchSystems/options.py +9 -10
- toil/batchSystems/registry.py +3 -7
- toil/batchSystems/singleMachine.py +8 -11
- toil/batchSystems/slurm.py +49 -38
- toil/batchSystems/torque.py +3 -4
- toil/bus.py +36 -34
- toil/common.py +129 -89
- toil/cwl/cwltoil.py +857 -729
- toil/cwl/utils.py +44 -35
- toil/fileStores/__init__.py +3 -1
- toil/fileStores/abstractFileStore.py +28 -30
- toil/fileStores/cachingFileStore.py +8 -8
- toil/fileStores/nonCachingFileStore.py +10 -21
- toil/job.py +159 -158
- toil/jobStores/abstractJobStore.py +68 -69
- toil/jobStores/aws/jobStore.py +249 -213
- toil/jobStores/aws/utils.py +13 -24
- toil/jobStores/fileJobStore.py +28 -22
- toil/jobStores/googleJobStore.py +21 -17
- toil/jobStores/utils.py +3 -7
- toil/leader.py +17 -22
- toil/lib/accelerators.py +6 -4
- toil/lib/aws/__init__.py +9 -10
- toil/lib/aws/ami.py +33 -19
- toil/lib/aws/iam.py +6 -6
- toil/lib/aws/s3.py +259 -157
- toil/lib/aws/session.py +76 -76
- toil/lib/aws/utils.py +51 -43
- toil/lib/checksum.py +19 -15
- toil/lib/compatibility.py +3 -2
- toil/lib/conversions.py +45 -18
- toil/lib/directory.py +29 -26
- toil/lib/docker.py +93 -99
- toil/lib/dockstore.py +77 -50
- toil/lib/ec2.py +39 -38
- toil/lib/ec2nodes.py +11 -4
- toil/lib/exceptions.py +8 -5
- toil/lib/ftp_utils.py +9 -14
- toil/lib/generatedEC2Lists.py +161 -20
- toil/lib/history.py +141 -97
- toil/lib/history_submission.py +163 -72
- toil/lib/io.py +27 -17
- toil/lib/memoize.py +2 -1
- toil/lib/misc.py +15 -11
- toil/lib/pipes.py +40 -25
- toil/lib/plugins.py +12 -8
- toil/lib/resources.py +1 -0
- toil/lib/retry.py +32 -38
- toil/lib/threading.py +12 -12
- toil/lib/throttle.py +1 -2
- toil/lib/trs.py +113 -51
- toil/lib/url.py +14 -23
- toil/lib/web.py +7 -2
- toil/options/common.py +18 -15
- toil/options/cwl.py +2 -2
- toil/options/runner.py +9 -5
- toil/options/wdl.py +1 -3
- toil/provisioners/__init__.py +9 -9
- toil/provisioners/abstractProvisioner.py +22 -20
- toil/provisioners/aws/__init__.py +20 -14
- toil/provisioners/aws/awsProvisioner.py +10 -8
- toil/provisioners/clusterScaler.py +19 -18
- toil/provisioners/gceProvisioner.py +2 -3
- toil/provisioners/node.py +11 -13
- toil/realtimeLogger.py +4 -4
- toil/resource.py +5 -5
- toil/server/app.py +2 -2
- toil/server/cli/wes_cwl_runner.py +11 -11
- toil/server/utils.py +18 -21
- toil/server/wes/abstract_backend.py +9 -8
- toil/server/wes/amazon_wes_utils.py +3 -3
- toil/server/wes/tasks.py +3 -5
- toil/server/wes/toil_backend.py +17 -21
- toil/server/wsgi_app.py +3 -3
- toil/serviceManager.py +3 -4
- toil/statsAndLogging.py +12 -13
- toil/test/__init__.py +33 -24
- toil/test/batchSystems/batchSystemTest.py +12 -11
- toil/test/batchSystems/batch_system_plugin_test.py +3 -5
- toil/test/batchSystems/test_slurm.py +38 -24
- toil/test/cwl/conftest.py +5 -6
- toil/test/cwl/cwlTest.py +194 -78
- toil/test/cwl/download_file_uri.json +6 -0
- toil/test/cwl/download_file_uri_no_hostname.json +6 -0
- toil/test/docs/scripts/tutorial_staging.py +1 -0
- toil/test/jobStores/jobStoreTest.py +9 -7
- toil/test/lib/aws/test_iam.py +1 -3
- toil/test/lib/aws/test_s3.py +1 -1
- toil/test/lib/dockerTest.py +9 -9
- toil/test/lib/test_ec2.py +12 -11
- toil/test/lib/test_history.py +4 -4
- toil/test/lib/test_trs.py +16 -14
- toil/test/lib/test_url.py +7 -6
- toil/test/lib/url_plugin_test.py +12 -18
- toil/test/provisioners/aws/awsProvisionerTest.py +10 -8
- toil/test/provisioners/clusterScalerTest.py +2 -5
- toil/test/provisioners/clusterTest.py +1 -3
- toil/test/server/serverTest.py +13 -4
- toil/test/sort/restart_sort.py +2 -6
- toil/test/sort/sort.py +3 -8
- toil/test/src/deferredFunctionTest.py +7 -7
- toil/test/src/environmentTest.py +1 -2
- toil/test/src/fileStoreTest.py +5 -5
- toil/test/src/importExportFileTest.py +5 -6
- toil/test/src/jobServiceTest.py +22 -14
- toil/test/src/jobTest.py +121 -25
- toil/test/src/miscTests.py +5 -7
- toil/test/src/promisedRequirementTest.py +8 -7
- toil/test/src/regularLogTest.py +2 -3
- toil/test/src/resourceTest.py +5 -8
- toil/test/src/restartDAGTest.py +5 -6
- toil/test/src/resumabilityTest.py +2 -2
- toil/test/src/retainTempDirTest.py +3 -3
- toil/test/src/systemTest.py +3 -3
- toil/test/src/threadingTest.py +1 -1
- toil/test/src/workerTest.py +1 -2
- toil/test/utils/toilDebugTest.py +6 -4
- toil/test/utils/toilKillTest.py +1 -1
- toil/test/utils/utilsTest.py +15 -14
- toil/test/wdl/wdltoil_test.py +247 -124
- toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
- toil/toilState.py +2 -3
- toil/utils/toilDebugFile.py +3 -8
- toil/utils/toilDebugJob.py +1 -2
- toil/utils/toilLaunchCluster.py +1 -2
- toil/utils/toilSshCluster.py +2 -0
- toil/utils/toilStats.py +19 -24
- toil/utils/toilStatus.py +11 -14
- toil/version.py +10 -10
- toil/wdl/wdltoil.py +313 -209
- toil/worker.py +18 -12
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/METADATA +11 -14
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/RECORD +150 -153
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/WHEEL +1 -1
- toil/test/cwl/staging_cat.cwl +0 -27
- toil/test/cwl/staging_make_file.cwl +0 -25
- toil/test/cwl/staging_workflow.cwl +0 -43
- toil/test/cwl/zero_default.cwl +0 -61
- toil/test/utils/ABCWorkflowDebug/ABC.txt +0 -1
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/entry_points.txt +0 -0
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/licenses/LICENSE +0 -0
- {toil-9.1.1.dist-info → toil-9.2.0.dist-info}/top_level.txt +0 -0
toil/lib/io.py
CHANGED
|
@@ -6,15 +6,15 @@ import stat
|
|
|
6
6
|
import sys
|
|
7
7
|
import tempfile
|
|
8
8
|
import uuid
|
|
9
|
-
from collections.abc import
|
|
9
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from io import BytesIO
|
|
12
|
-
from typing import IO, Any,
|
|
12
|
+
from typing import IO, Any, Protocol
|
|
13
13
|
|
|
14
|
-
from toil.lib.directory import
|
|
15
|
-
from toil.lib.url import URLAccess
|
|
14
|
+
from toil.lib.directory import TOIL_DIR_URI_SCHEME, get_directory_item
|
|
16
15
|
from toil.lib.memoize import memoize
|
|
17
16
|
from toil.lib.misc import StrPath
|
|
17
|
+
from toil.lib.url import URLAccess
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ def get_toil_home() -> str:
|
|
|
27
27
|
Raises an error if it does not exist and cannot be created. Safe to run
|
|
28
28
|
simultaneously in multiple processes.
|
|
29
29
|
"""
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
# TODO: should this use an XDG config directory or ~/.config to not clutter the
|
|
32
32
|
# base home directory?
|
|
33
33
|
toil_home_dir = os.path.join(os.path.expanduser("~"), ".toil")
|
|
@@ -39,24 +39,28 @@ def get_toil_home() -> str:
|
|
|
39
39
|
)
|
|
40
40
|
return dir_path
|
|
41
41
|
|
|
42
|
+
|
|
42
43
|
TOIL_URI_SCHEME = "toilfile:"
|
|
43
44
|
|
|
44
45
|
STANDARD_SCHEMES = ["http:", "https:", "s3:", "gs:", "ftp:"]
|
|
45
46
|
REMOTE_SCHEMES = STANDARD_SCHEMES + [TOIL_URI_SCHEME, TOIL_DIR_URI_SCHEME]
|
|
46
47
|
ALL_SCHEMES = REMOTE_SCHEMES + ["file:"]
|
|
47
48
|
|
|
49
|
+
|
|
48
50
|
def is_standard_url(filename: str) -> bool:
|
|
49
51
|
"""
|
|
50
52
|
Return True if the given URL is a non-Toil, non-file: URL.
|
|
51
53
|
"""
|
|
52
54
|
return is_url_with_scheme(filename, STANDARD_SCHEMES)
|
|
53
55
|
|
|
56
|
+
|
|
54
57
|
def is_remote_url(filename: str) -> bool:
|
|
55
58
|
"""
|
|
56
59
|
Decide if a filename is a known, non-file kind of URL
|
|
57
60
|
"""
|
|
58
61
|
return is_url_with_scheme(filename, REMOTE_SCHEMES)
|
|
59
62
|
|
|
63
|
+
|
|
60
64
|
def is_any_url(filename: str) -> bool:
|
|
61
65
|
"""
|
|
62
66
|
Decide if a string is a URI like http:// or file://.
|
|
@@ -65,6 +69,7 @@ def is_any_url(filename: str) -> bool:
|
|
|
65
69
|
"""
|
|
66
70
|
return is_url_with_scheme(filename, ALL_SCHEMES)
|
|
67
71
|
|
|
72
|
+
|
|
68
73
|
def is_url_with_scheme(filename: str, schemes: list[str]) -> bool:
|
|
69
74
|
"""
|
|
70
75
|
Return True if filename is a URL with any of the given schemes and False otherwise.
|
|
@@ -75,26 +80,30 @@ def is_url_with_scheme(filename: str, schemes: list[str]) -> bool:
|
|
|
75
80
|
return True
|
|
76
81
|
return False
|
|
77
82
|
|
|
83
|
+
|
|
78
84
|
def is_toil_url(filename: str) -> bool:
|
|
79
85
|
"""
|
|
80
86
|
Return True if a URL is a toilfile: or toildir: URL.
|
|
81
87
|
"""
|
|
82
88
|
return is_url_with_scheme(filename, [TOIL_URI_SCHEME, TOIL_DIR_URI_SCHEME])
|
|
83
89
|
|
|
90
|
+
|
|
84
91
|
def is_toil_file_url(filename: str) -> bool:
|
|
85
92
|
"""
|
|
86
93
|
Return True if a URL is a toilfile: URL.
|
|
87
94
|
"""
|
|
88
95
|
return is_url_with_scheme(filename, [TOIL_URI_SCHEME])
|
|
89
96
|
|
|
97
|
+
|
|
90
98
|
def is_toil_dir_url(filename: str) -> bool:
|
|
91
99
|
"""
|
|
92
100
|
Return True if a URL is a toildir: URL.
|
|
93
101
|
|
|
94
|
-
Note that this may point to either a
|
|
102
|
+
Note that this may point to either a directory or a leaf file.
|
|
95
103
|
"""
|
|
96
104
|
return is_url_with_scheme(filename, [TOIL_DIR_URI_SCHEME])
|
|
97
105
|
|
|
106
|
+
|
|
98
107
|
def is_file_url(filename: str) -> bool:
|
|
99
108
|
"""
|
|
100
109
|
Return True if a URL is a file: URL.
|
|
@@ -103,6 +112,7 @@ def is_file_url(filename: str) -> bool:
|
|
|
103
112
|
"""
|
|
104
113
|
return is_url_with_scheme(filename, ["file:"])
|
|
105
114
|
|
|
115
|
+
|
|
106
116
|
def is_directory_url(filename: str) -> bool:
|
|
107
117
|
"""
|
|
108
118
|
Return True if a URL points to a directory.
|
|
@@ -119,10 +129,11 @@ def is_directory_url(filename: str) -> bool:
|
|
|
119
129
|
return not isinstance(get_directory_item(filename), str)
|
|
120
130
|
return URLAccess.get_is_directory(filename)
|
|
121
131
|
|
|
132
|
+
|
|
122
133
|
def mkdtemp(
|
|
123
|
-
suffix:
|
|
124
|
-
prefix:
|
|
125
|
-
dir:
|
|
134
|
+
suffix: str | None = None,
|
|
135
|
+
prefix: str | None = None,
|
|
136
|
+
dir: StrPath | None = None,
|
|
126
137
|
) -> str:
|
|
127
138
|
"""
|
|
128
139
|
Make a temporary directory like tempfile.mkdtemp, but with relaxed permissions.
|
|
@@ -147,7 +158,7 @@ def mkdtemp(
|
|
|
147
158
|
return result
|
|
148
159
|
|
|
149
160
|
|
|
150
|
-
def robust_rmtree(path:
|
|
161
|
+
def robust_rmtree(path: str | bytes) -> None:
|
|
151
162
|
"""
|
|
152
163
|
Robustly tries to delete paths.
|
|
153
164
|
|
|
@@ -257,9 +268,7 @@ def AtomicFileCreate(final_path: StrPath, keep: bool = False) -> Iterator[str]:
|
|
|
257
268
|
raise
|
|
258
269
|
|
|
259
270
|
|
|
260
|
-
def atomic_copy(
|
|
261
|
-
src_path: str, dest_path: str, executable: Optional[bool] = None
|
|
262
|
-
) -> None:
|
|
271
|
+
def atomic_copy(src_path: str, dest_path: str, executable: bool | None = None) -> None:
|
|
263
272
|
"""Copy a file using posix atomic creations semantics."""
|
|
264
273
|
if executable is None:
|
|
265
274
|
executable = os.stat(src_path).st_mode & stat.S_IXUSR != 0
|
|
@@ -280,7 +289,7 @@ def atomic_copyobj(
|
|
|
280
289
|
os.chmod(dest_path_tmp, os.stat(dest_path_tmp).st_mode | stat.S_IXUSR)
|
|
281
290
|
|
|
282
291
|
|
|
283
|
-
def make_public_dir(in_directory: str, suggested_name:
|
|
292
|
+
def make_public_dir(in_directory: str, suggested_name: str | None = None) -> str:
|
|
284
293
|
"""
|
|
285
294
|
Make a publicly-accessible directory in the given directory.
|
|
286
295
|
|
|
@@ -318,7 +327,7 @@ def make_public_dir(in_directory: str, suggested_name: Optional[str] = None) ->
|
|
|
318
327
|
return this_should_never_happen
|
|
319
328
|
|
|
320
329
|
|
|
321
|
-
def try_path(path: str, min_size: int = 100 * 1024 * 1024) ->
|
|
330
|
+
def try_path(path: str, min_size: int = 100 * 1024 * 1024) -> str | None:
|
|
322
331
|
"""
|
|
323
332
|
Try to use the given path. Return it if it exists or can be made,
|
|
324
333
|
and we can make things within it, or None otherwise.
|
|
@@ -416,6 +425,7 @@ class WriteWatchingStream:
|
|
|
416
425
|
|
|
417
426
|
self.backingStream.close()
|
|
418
427
|
|
|
428
|
+
|
|
419
429
|
class ReadableFileObj(Protocol):
|
|
420
430
|
"""
|
|
421
431
|
Protocol that is more specific than what file_digest takes as an argument.
|
|
@@ -423,10 +433,12 @@ class ReadableFileObj(Protocol):
|
|
|
423
433
|
Would extend the protocol from Typeshed for hashlib but those are only
|
|
424
434
|
declared for 3.11+.
|
|
425
435
|
"""
|
|
436
|
+
|
|
426
437
|
def readinto(self, buf: bytearray, /) -> int: ...
|
|
427
438
|
def readable(self) -> bool: ...
|
|
428
439
|
def read(self, number: int) -> bytes: ...
|
|
429
440
|
|
|
441
|
+
|
|
430
442
|
# hashlib._Hash seems to not appear at runtime
|
|
431
443
|
def file_digest(f: ReadableFileObj, alg_name: str) -> "hashlib._Hash":
|
|
432
444
|
"""
|
|
@@ -441,5 +453,3 @@ def file_digest(f: ReadableFileObj, alg_name: str) -> "hashlib._Hash":
|
|
|
441
453
|
hasher.update(buffer)
|
|
442
454
|
buffer = f.read(BUFFER_SIZE)
|
|
443
455
|
return hasher
|
|
444
|
-
|
|
445
|
-
|
toil/lib/memoize.py
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
# 5.14.2018: copied into Toil from https://github.com/BD2KGenomics/bd2k-python-lib
|
|
16
16
|
import datetime
|
|
17
17
|
import re
|
|
18
|
+
from collections.abc import Callable
|
|
18
19
|
from functools import lru_cache, wraps
|
|
19
20
|
from threading import Lock
|
|
20
|
-
from typing import Any,
|
|
21
|
+
from typing import Any, TypeVar
|
|
21
22
|
|
|
22
23
|
memoize = lru_cache(maxsize=None)
|
|
23
24
|
"""
|
toil/lib/misc.py
CHANGED
|
@@ -9,17 +9,14 @@ import sys
|
|
|
9
9
|
import time
|
|
10
10
|
from collections.abc import Iterator
|
|
11
11
|
from contextlib import closing
|
|
12
|
-
from typing import
|
|
13
|
-
if sys.version_info >= (3, 10):
|
|
14
|
-
from typing import TypeAlias
|
|
15
|
-
else:
|
|
16
|
-
from typing_extensions import TypeAlias
|
|
12
|
+
from typing import TypeAlias, Union
|
|
17
13
|
|
|
18
14
|
logger = logging.getLogger(__name__)
|
|
19
15
|
|
|
20
16
|
StrPath: TypeAlias = Union[str, os.PathLike[str]]
|
|
21
17
|
FileDescriptorOrPath: TypeAlias = Union[int, bytes, os.PathLike[bytes], StrPath]
|
|
22
18
|
|
|
19
|
+
|
|
23
20
|
def get_public_ip() -> str:
|
|
24
21
|
"""Get the IP that this machine uses to contact the internet.
|
|
25
22
|
|
|
@@ -69,17 +66,24 @@ def unix_now_ms() -> float:
|
|
|
69
66
|
"""Return the current time in milliseconds since the Unix epoch."""
|
|
70
67
|
return time.time() * 1000
|
|
71
68
|
|
|
69
|
+
|
|
72
70
|
def unix_seconds_to_timestamp(timestamp: float) -> str:
|
|
73
71
|
"""
|
|
74
72
|
Convert a time in seconds since the Unix epoch to an ISO 8601 string.
|
|
75
73
|
"""
|
|
76
|
-
return datetime.datetime.fromtimestamp(
|
|
74
|
+
return datetime.datetime.fromtimestamp(
|
|
75
|
+
timestamp, tz=datetime.timezone.utc
|
|
76
|
+
).isoformat()
|
|
77
|
+
|
|
77
78
|
|
|
78
79
|
def unix_seconds_to_local_time(timestamp: float) -> datetime.datetime:
|
|
79
80
|
"""
|
|
80
81
|
Returns a local time corresponding to the given number of seconds since the Unix epoch.
|
|
81
82
|
"""
|
|
82
|
-
return datetime.datetime.fromtimestamp(
|
|
83
|
+
return datetime.datetime.fromtimestamp(
|
|
84
|
+
timestamp, tz=datetime.timezone.utc
|
|
85
|
+
).astimezone()
|
|
86
|
+
|
|
83
87
|
|
|
84
88
|
def seconds_to_duration(time_difference: float) -> str:
|
|
85
89
|
"""
|
|
@@ -157,11 +161,11 @@ class CalledProcessErrorStderr(subprocess.CalledProcessError):
|
|
|
157
161
|
def call_command(
|
|
158
162
|
cmd: list[str],
|
|
159
163
|
*args: str,
|
|
160
|
-
input:
|
|
161
|
-
timeout:
|
|
164
|
+
input: str | None = None,
|
|
165
|
+
timeout: float | None = None,
|
|
162
166
|
useCLocale: bool = True,
|
|
163
|
-
env:
|
|
164
|
-
quiet:
|
|
167
|
+
env: dict[str, str] | None = None,
|
|
168
|
+
quiet: bool | None = False,
|
|
165
169
|
) -> str:
|
|
166
170
|
"""
|
|
167
171
|
Simplified calling of external commands.
|
toil/lib/pipes.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import errno
|
|
2
|
+
import hashlib
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
|
-
import hashlib
|
|
5
|
-
import threading
|
|
6
|
-
|
|
7
5
|
from abc import ABC, abstractmethod
|
|
8
|
-
from typing import
|
|
6
|
+
from typing import IO, Any
|
|
9
7
|
|
|
10
8
|
from toil.lib.checksum import ChecksumError
|
|
11
9
|
from toil.lib.threading import ExceptionalThread
|
|
@@ -75,7 +73,7 @@ class WritablePipe(ABC):
|
|
|
75
73
|
True
|
|
76
74
|
"""
|
|
77
75
|
|
|
78
|
-
def __init__(self, encoding:
|
|
76
|
+
def __init__(self, encoding: str | None = None, errors: str | None = None) -> None:
|
|
79
77
|
"""
|
|
80
78
|
The specified encoding and errors apply to the writable end of the pipe.
|
|
81
79
|
|
|
@@ -86,11 +84,11 @@ class WritablePipe(ABC):
|
|
|
86
84
|
are the same as for open(). Defaults to 'strict' when an encoding is specified.
|
|
87
85
|
"""
|
|
88
86
|
super().__init__()
|
|
89
|
-
self.encoding:
|
|
90
|
-
self.errors:
|
|
91
|
-
self.readable_fh:
|
|
92
|
-
self.writable:
|
|
93
|
-
self.thread:
|
|
87
|
+
self.encoding: str | None = encoding
|
|
88
|
+
self.errors: str | None = errors
|
|
89
|
+
self.readable_fh: int | None = None
|
|
90
|
+
self.writable: IO[Any] | None = None
|
|
91
|
+
self.thread: ExceptionalThread | None = None
|
|
94
92
|
self.reader_done: bool = False
|
|
95
93
|
|
|
96
94
|
def __enter__(self) -> IO[Any]:
|
|
@@ -105,7 +103,9 @@ class WritablePipe(ABC):
|
|
|
105
103
|
self.thread.start()
|
|
106
104
|
return self.writable
|
|
107
105
|
|
|
108
|
-
def __exit__(
|
|
106
|
+
def __exit__(
|
|
107
|
+
self, exc_type: str | None, exc_val: str | None, exc_tb: str | None
|
|
108
|
+
) -> None:
|
|
109
109
|
# Closing the writable end will send EOF to the readable and cause the reader thread
|
|
110
110
|
# to finish.
|
|
111
111
|
# TODO: Can close() fail? If so, would we try and clean up after the reader?
|
|
@@ -160,7 +160,6 @@ class WritablePipe(ABC):
|
|
|
160
160
|
self.reader_done = True
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
|
|
164
163
|
class ReadablePipe(ABC):
|
|
165
164
|
"""
|
|
166
165
|
An object-oriented wrapper for os.pipe. Clients should subclass it, implement
|
|
@@ -245,7 +244,7 @@ class ReadablePipe(ABC):
|
|
|
245
244
|
if e.errno != errno.EPIPE:
|
|
246
245
|
raise
|
|
247
246
|
|
|
248
|
-
def __init__(self, encoding:
|
|
247
|
+
def __init__(self, encoding: str | None = None, errors: str | None = None) -> None:
|
|
249
248
|
"""
|
|
250
249
|
The specified encoding and errors apply to the readable end of the pipe.
|
|
251
250
|
|
|
@@ -256,11 +255,11 @@ class ReadablePipe(ABC):
|
|
|
256
255
|
are the same as for open(). Defaults to 'strict' when an encoding is specified.
|
|
257
256
|
"""
|
|
258
257
|
super().__init__()
|
|
259
|
-
self.encoding:
|
|
260
|
-
self.errors:
|
|
261
|
-
self.writable_fh:
|
|
262
|
-
self.readable:
|
|
263
|
-
self.thread:
|
|
258
|
+
self.encoding: str | None = encoding
|
|
259
|
+
self.errors: str | None = errors
|
|
260
|
+
self.writable_fh: int | None = None
|
|
261
|
+
self.readable: IO[Any] | None = None
|
|
262
|
+
self.thread: ExceptionalThread | None = None
|
|
264
263
|
|
|
265
264
|
def __enter__(self) -> IO[Any]:
|
|
266
265
|
readable_fh, self.writable_fh = os.pipe()
|
|
@@ -274,7 +273,9 @@ class ReadablePipe(ABC):
|
|
|
274
273
|
self.thread.start()
|
|
275
274
|
return self.readable
|
|
276
275
|
|
|
277
|
-
def __exit__(
|
|
276
|
+
def __exit__(
|
|
277
|
+
self, exc_type: str | None, exc_val: str | None, exc_tb: str | None
|
|
278
|
+
) -> None:
|
|
278
279
|
# Close the read end of the pipe. The writing thread may
|
|
279
280
|
# still be writing to the other end, but this will wake it up
|
|
280
281
|
# if that's the case.
|
|
@@ -323,7 +324,12 @@ class ReadableTransformingPipe(ReadablePipe):
|
|
|
323
324
|
|
|
324
325
|
"""
|
|
325
326
|
|
|
326
|
-
def __init__(
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
source: IO[Any],
|
|
330
|
+
encoding: str | None = None,
|
|
331
|
+
errors: str | None = None,
|
|
332
|
+
) -> None:
|
|
327
333
|
"""
|
|
328
334
|
:param str encoding: the name of the encoding used to encode the file. Encodings are the same
|
|
329
335
|
as for encode(). Defaults to None which represents binary mode.
|
|
@@ -357,7 +363,14 @@ class HashingPipe(ReadableTransformingPipe):
|
|
|
357
363
|
|
|
358
364
|
Assumes info actually has a checksum.
|
|
359
365
|
"""
|
|
360
|
-
|
|
366
|
+
|
|
367
|
+
def __init__(
|
|
368
|
+
self,
|
|
369
|
+
source: IO[Any],
|
|
370
|
+
encoding: str | None = None,
|
|
371
|
+
errors: str | None = None,
|
|
372
|
+
checksum_to_verify: str | None = None,
|
|
373
|
+
) -> None:
|
|
361
374
|
"""
|
|
362
375
|
:param str encoding: the name of the encoding used to encode the file. Encodings are the same
|
|
363
376
|
as for encode(). Defaults to None which represents binary mode.
|
|
@@ -365,13 +378,13 @@ class HashingPipe(ReadableTransformingPipe):
|
|
|
365
378
|
:param str errors: an optional string that specifies how encoding errors are to be handled. Errors
|
|
366
379
|
are the same as for open(). Defaults to 'strict' when an encoding is specified.
|
|
367
380
|
"""
|
|
368
|
-
super(
|
|
381
|
+
super().__init__(source=source, encoding=encoding, errors=errors)
|
|
369
382
|
self.checksum_to_verify = checksum_to_verify
|
|
370
383
|
|
|
371
384
|
def transform(self, readable: IO[Any], writable: IO[Any]) -> None:
|
|
372
385
|
hash_object = hashlib.sha1()
|
|
373
386
|
contents = readable.read(1024 * 1024)
|
|
374
|
-
while contents != b
|
|
387
|
+
while contents != b"":
|
|
375
388
|
hash_object.update(contents)
|
|
376
389
|
try:
|
|
377
390
|
writable.write(contents)
|
|
@@ -380,6 +393,8 @@ class HashingPipe(ReadableTransformingPipe):
|
|
|
380
393
|
# Can't check the checksum.
|
|
381
394
|
return
|
|
382
395
|
contents = readable.read(1024 * 1024)
|
|
383
|
-
final_computed_checksum = f
|
|
396
|
+
final_computed_checksum = f"sha1${hash_object.hexdigest()}"
|
|
384
397
|
if not self.checksum_to_verify == final_computed_checksum:
|
|
385
|
-
raise ChecksumError(
|
|
398
|
+
raise ChecksumError(
|
|
399
|
+
f"Checksum mismatch. Expected: {self.checksum_to_verify} Actual: {final_computed_checksum}"
|
|
400
|
+
)
|
toil/lib/plugins.py
CHANGED
|
@@ -26,16 +26,17 @@ themselves.
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import importlib
|
|
29
|
-
from typing import Any, Literal, Union
|
|
30
29
|
import pkgutil
|
|
31
|
-
from
|
|
30
|
+
from typing import Any, Literal, Union
|
|
32
31
|
|
|
32
|
+
from toil.lib.memoize import memoize
|
|
33
33
|
|
|
34
34
|
PluginType = Union[Literal["batch_system"], Literal["url_access"]]
|
|
35
35
|
plugin_types: list[PluginType] = ["batch_system", "url_access"]
|
|
36
36
|
|
|
37
37
|
_registry: dict[str, dict[str, Any]] = {k: {} for k in plugin_types}
|
|
38
38
|
|
|
39
|
+
|
|
39
40
|
def register_plugin(
|
|
40
41
|
plugin_type: PluginType, plugin_name: str, plugin_being_registered: Any
|
|
41
42
|
) -> None:
|
|
@@ -51,13 +52,13 @@ def register_plugin(
|
|
|
51
52
|
the resulting type must extend
|
|
52
53
|
:class:`toil.batchSystems.abstractBatchSystem.AbstractBatchSystem`. For
|
|
53
54
|
URL access plugins, it must extend :class:`toil.lib.url.URLAccess`.
|
|
54
|
-
Note that the function used here should return the class
|
|
55
|
+
Note that the function used here should return the class itself; it
|
|
55
56
|
should not construct an instance of the class.
|
|
56
57
|
"""
|
|
57
58
|
_registry[plugin_type][plugin_name] = plugin_being_registered
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
def remove_plugin(plugin_type: PluginType, plugin_name: str) -> None:
|
|
61
62
|
"""
|
|
62
63
|
Removes a plugin from the registry for the given type of plugin.
|
|
63
64
|
"""
|
|
@@ -67,13 +68,15 @@ def remove_plugin(
|
|
|
67
68
|
# If the plugin does not exist, it can be ignored
|
|
68
69
|
pass
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
|
|
72
|
+
def get_plugin_names(plugin_type: PluginType) -> list[str]:
|
|
71
73
|
"""
|
|
72
74
|
Get the names of all the available plugins of the given type.
|
|
73
75
|
"""
|
|
74
76
|
_load_all_plugins(plugin_type)
|
|
75
77
|
return list(_registry[plugin_type].keys())
|
|
76
78
|
|
|
79
|
+
|
|
77
80
|
def get_plugin(plugin_type: PluginType, plugin_name: str) -> Any:
|
|
78
81
|
"""
|
|
79
82
|
Get a plugin class factory function by name.
|
|
@@ -87,12 +90,13 @@ def get_plugin(plugin_type: PluginType, plugin_name: str) -> Any:
|
|
|
87
90
|
|
|
88
91
|
def _plugin_name_prefix(plugin_type: PluginType) -> str:
|
|
89
92
|
"""
|
|
90
|
-
Get prefix for plugin type.
|
|
91
|
-
|
|
93
|
+
Get prefix for plugin type.
|
|
94
|
+
|
|
92
95
|
Any packages with prefix will count as toil plugins of that type.
|
|
93
96
|
"""
|
|
94
97
|
return f"toil_{plugin_type}_"
|
|
95
98
|
|
|
99
|
+
|
|
96
100
|
@memoize
|
|
97
101
|
def _load_all_plugins(plugin_type: PluginType) -> None:
|
|
98
102
|
"""
|
toil/lib/resources.py
CHANGED
toil/lib/retry.py
CHANGED
|
@@ -131,9 +131,9 @@ import sqlite3
|
|
|
131
131
|
import time
|
|
132
132
|
import traceback
|
|
133
133
|
import urllib.error
|
|
134
|
-
from collections.abc import Generator, Iterable, Sequence
|
|
134
|
+
from collections.abc import Callable, Generator, Iterable, Sequence
|
|
135
135
|
from contextlib import contextmanager
|
|
136
|
-
from typing import
|
|
136
|
+
from typing import ContextManager, TypeVar
|
|
137
137
|
|
|
138
138
|
import requests.exceptions
|
|
139
139
|
import urllib3.exceptions
|
|
@@ -172,7 +172,7 @@ class ErrorCondition:
|
|
|
172
172
|
|
|
173
173
|
def __init__(
|
|
174
174
|
self,
|
|
175
|
-
error:
|
|
175
|
+
error: type[BaseException] | None = None,
|
|
176
176
|
error_codes: list[int] = None,
|
|
177
177
|
boto_error_codes: list[str] = None,
|
|
178
178
|
error_message_must_include: str = None,
|
|
@@ -227,11 +227,11 @@ RT = TypeVar("RT")
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def retry(
|
|
230
|
-
intervals:
|
|
230
|
+
intervals: list | None = None,
|
|
231
231
|
infinite_retries: bool = False,
|
|
232
|
-
errors:
|
|
233
|
-
log_message:
|
|
234
|
-
prepare:
|
|
232
|
+
errors: Sequence[ErrorCondition | type[Exception]] | None = None,
|
|
233
|
+
log_message: tuple[Callable, str] | None = None,
|
|
234
|
+
prepare: list[Callable] | None = None,
|
|
235
235
|
) -> Callable[[Callable[..., RT]], Callable[..., RT]]:
|
|
236
236
|
"""
|
|
237
237
|
Retry a function if it fails with any Exception defined in "errors".
|
|
@@ -332,17 +332,14 @@ def return_status_code(e):
|
|
|
332
332
|
if botocore:
|
|
333
333
|
if isinstance(e, botocore.exceptions.ClientError):
|
|
334
334
|
return e.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return e.code
|
|
344
|
-
else:
|
|
345
|
-
raise ValueError(f"Unsupported error type; cannot grok status code: {e}.")
|
|
335
|
+
match e:
|
|
336
|
+
case requests.exceptions.HTTPError():
|
|
337
|
+
return e.response.status_code
|
|
338
|
+
case http.client.HTTPException() | urllib3.exceptions.HTTPError():
|
|
339
|
+
return e.status
|
|
340
|
+
case urllib.error.HTTPError():
|
|
341
|
+
return e.code
|
|
342
|
+
raise ValueError(f"Unsupported error type; cannot grok status code: {e}.")
|
|
346
343
|
|
|
347
344
|
|
|
348
345
|
def get_error_code(e: Exception) -> str:
|
|
@@ -441,7 +438,7 @@ def get_error_body(e: Exception) -> str:
|
|
|
441
438
|
return f"{get_error_code(e)}: {get_error_message(e)}"
|
|
442
439
|
|
|
443
440
|
|
|
444
|
-
def meets_error_message_condition(e: Exception, error_message:
|
|
441
|
+
def meets_error_message_condition(e: Exception, error_message: str | None):
|
|
445
442
|
if error_message:
|
|
446
443
|
if kubernetes:
|
|
447
444
|
if isinstance(e, kubernetes.client.rest.ApiException):
|
|
@@ -450,26 +447,25 @@ def meets_error_message_condition(e: Exception, error_message: Optional[str]):
|
|
|
450
447
|
if botocore:
|
|
451
448
|
if isinstance(e, botocore.exceptions.ClientError):
|
|
452
449
|
return error_message in str(e)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return error_message in traceback.format_exc()
|
|
450
|
+
match e:
|
|
451
|
+
case (
|
|
452
|
+
http.client.HTTPException() | urllib3.exceptions.HTTPError()
|
|
453
|
+
) as c_exc:
|
|
454
|
+
return error_message in c_exc.reason
|
|
455
|
+
case sqlite3.OperationalError():
|
|
456
|
+
return error_message in str(e)
|
|
457
|
+
case urllib.error.HTTPError() as u_err:
|
|
458
|
+
return error_message in u_err.msg
|
|
459
|
+
case requests.exceptions.HTTPError() as r_err:
|
|
460
|
+
return error_message in r_err.raw
|
|
461
|
+
case Exception() as exc if hasattr(exc, "msg"):
|
|
462
|
+
return error_message in exc.msg
|
|
463
|
+
return error_message in traceback.format_exc()
|
|
468
464
|
else: # there is no error message, so the user is not expecting it to be present
|
|
469
465
|
return True
|
|
470
466
|
|
|
471
467
|
|
|
472
|
-
def meets_error_code_condition(e: Exception, error_codes:
|
|
468
|
+
def meets_error_code_condition(e: Exception, error_codes: list[int] | None):
|
|
473
469
|
"""These are expected to be normal HTTP error codes, like 404 or 500."""
|
|
474
470
|
if error_codes:
|
|
475
471
|
status_code = get_error_status(e)
|
|
@@ -478,9 +474,7 @@ def meets_error_code_condition(e: Exception, error_codes: Optional[list[int]]):
|
|
|
478
474
|
return True
|
|
479
475
|
|
|
480
476
|
|
|
481
|
-
def meets_boto_error_code_condition(
|
|
482
|
-
e: Exception, boto_error_codes: Optional[list[str]]
|
|
483
|
-
):
|
|
477
|
+
def meets_boto_error_code_condition(e: Exception, boto_error_codes: list[str] | None):
|
|
484
478
|
"""These are expected to be AWS's custom error aliases, like 'BucketNotFound' or 'AccessDenied'."""
|
|
485
479
|
if boto_error_codes:
|
|
486
480
|
status_code = get_error_code(e)
|