toil 6.1.0__py3-none-any.whl → 7.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.
- toil/__init__.py +1 -232
- toil/batchSystems/abstractBatchSystem.py +22 -13
- toil/batchSystems/abstractGridEngineBatchSystem.py +59 -45
- toil/batchSystems/awsBatch.py +8 -8
- toil/batchSystems/contained_executor.py +4 -5
- toil/batchSystems/gridengine.py +1 -1
- toil/batchSystems/htcondor.py +5 -5
- toil/batchSystems/kubernetes.py +25 -11
- toil/batchSystems/local_support.py +3 -3
- toil/batchSystems/lsf.py +2 -2
- toil/batchSystems/mesos/batchSystem.py +4 -4
- toil/batchSystems/mesos/executor.py +3 -2
- toil/batchSystems/options.py +9 -0
- toil/batchSystems/singleMachine.py +11 -10
- toil/batchSystems/slurm.py +64 -22
- toil/batchSystems/torque.py +1 -1
- toil/bus.py +7 -3
- toil/common.py +36 -13
- toil/cwl/cwltoil.py +365 -312
- toil/deferred.py +1 -1
- toil/fileStores/abstractFileStore.py +17 -17
- toil/fileStores/cachingFileStore.py +2 -2
- toil/fileStores/nonCachingFileStore.py +1 -1
- toil/job.py +228 -60
- toil/jobStores/abstractJobStore.py +18 -10
- toil/jobStores/aws/jobStore.py +280 -218
- toil/jobStores/aws/utils.py +57 -29
- toil/jobStores/conftest.py +2 -2
- toil/jobStores/fileJobStore.py +2 -2
- toil/jobStores/googleJobStore.py +3 -4
- toil/leader.py +72 -24
- toil/lib/aws/__init__.py +26 -10
- toil/lib/aws/iam.py +2 -2
- toil/lib/aws/session.py +62 -22
- toil/lib/aws/utils.py +73 -37
- toil/lib/conversions.py +5 -1
- toil/lib/ec2.py +118 -69
- toil/lib/expando.py +1 -1
- toil/lib/io.py +14 -2
- toil/lib/misc.py +1 -3
- toil/lib/resources.py +55 -21
- toil/lib/retry.py +12 -5
- toil/lib/threading.py +2 -2
- toil/lib/throttle.py +1 -1
- toil/options/common.py +27 -24
- toil/provisioners/__init__.py +9 -3
- toil/provisioners/abstractProvisioner.py +9 -7
- toil/provisioners/aws/__init__.py +20 -15
- toil/provisioners/aws/awsProvisioner.py +406 -329
- toil/provisioners/gceProvisioner.py +2 -2
- toil/provisioners/node.py +13 -5
- toil/server/app.py +1 -1
- toil/statsAndLogging.py +58 -16
- toil/test/__init__.py +27 -12
- toil/test/batchSystems/batchSystemTest.py +40 -33
- toil/test/batchSystems/batch_system_plugin_test.py +79 -0
- toil/test/batchSystems/test_slurm.py +1 -1
- toil/test/cwl/cwlTest.py +8 -91
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +10 -13
- toil/test/jobStores/jobStoreTest.py +33 -49
- toil/test/lib/aws/test_iam.py +2 -2
- toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
- toil/test/provisioners/clusterTest.py +90 -8
- toil/test/server/serverTest.py +2 -2
- toil/test/src/autoDeploymentTest.py +1 -1
- toil/test/src/dockerCheckTest.py +2 -1
- toil/test/src/environmentTest.py +125 -0
- toil/test/src/fileStoreTest.py +1 -1
- toil/test/src/jobDescriptionTest.py +18 -8
- toil/test/src/jobTest.py +1 -1
- toil/test/src/realtimeLoggerTest.py +4 -0
- toil/test/src/workerTest.py +52 -19
- toil/test/utils/toilDebugTest.py +61 -3
- toil/test/utils/utilsTest.py +20 -18
- toil/test/wdl/wdltoil_test.py +24 -71
- toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
- toil/toilState.py +68 -9
- toil/utils/toilDebugJob.py +153 -26
- toil/utils/toilLaunchCluster.py +12 -2
- toil/utils/toilRsyncCluster.py +7 -2
- toil/utils/toilSshCluster.py +7 -3
- toil/utils/toilStats.py +2 -1
- toil/utils/toilStatus.py +97 -51
- toil/version.py +10 -10
- toil/wdl/wdltoil.py +318 -51
- toil/worker.py +96 -69
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/METADATA +55 -21
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/RECORD +93 -90
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/top_level.txt +0 -0
toil/lib/aws/utils.py
CHANGED
|
@@ -15,7 +15,6 @@ import errno
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
import socket
|
|
18
|
-
import sys
|
|
19
18
|
from typing import (Any,
|
|
20
19
|
Callable,
|
|
21
20
|
ContextManager,
|
|
@@ -25,35 +24,29 @@ from typing import (Any,
|
|
|
25
24
|
List,
|
|
26
25
|
Optional,
|
|
27
26
|
Set,
|
|
28
|
-
Union,
|
|
29
27
|
cast)
|
|
30
28
|
from urllib.parse import ParseResult
|
|
31
29
|
|
|
32
|
-
from
|
|
30
|
+
from mypy_boto3_sdb.type_defs import AttributeTypeDef
|
|
31
|
+
from toil.lib.aws import session, AWSRegionName, AWSServerErrors
|
|
33
32
|
from toil.lib.misc import printq
|
|
34
33
|
from toil.lib.retry import (DEFAULT_DELAYS,
|
|
35
34
|
DEFAULT_TIMEOUT,
|
|
36
35
|
get_error_code,
|
|
37
36
|
get_error_status,
|
|
38
37
|
old_retry,
|
|
39
|
-
retry)
|
|
40
|
-
|
|
41
|
-
if sys.version_info >= (3, 8):
|
|
42
|
-
from typing import Literal
|
|
43
|
-
else:
|
|
44
|
-
from typing_extensions import Literal
|
|
38
|
+
retry, ErrorCondition)
|
|
45
39
|
|
|
46
40
|
try:
|
|
47
|
-
from
|
|
48
|
-
from botocore.exceptions import ClientError
|
|
41
|
+
from botocore.exceptions import ClientError, EndpointConnectionError
|
|
49
42
|
from mypy_boto3_iam import IAMClient, IAMServiceResource
|
|
50
43
|
from mypy_boto3_s3 import S3Client, S3ServiceResource
|
|
51
44
|
from mypy_boto3_s3.literals import BucketLocationConstraintType
|
|
52
45
|
from mypy_boto3_s3.service_resource import Bucket, Object
|
|
53
46
|
from mypy_boto3_sdb import SimpleDBClient
|
|
54
47
|
except ImportError:
|
|
55
|
-
BotoServerError = None # type: ignore
|
|
56
48
|
ClientError = None # type: ignore
|
|
49
|
+
EndpointConnectionError = None # type: ignore
|
|
57
50
|
# AWS/boto extra is not installed
|
|
58
51
|
|
|
59
52
|
logger = logging.getLogger(__name__)
|
|
@@ -77,12 +70,10 @@ THROTTLED_ERROR_CODES = [
|
|
|
77
70
|
'EC2ThrottledException',
|
|
78
71
|
]
|
|
79
72
|
|
|
80
|
-
@retry(errors=[
|
|
73
|
+
@retry(errors=[AWSServerErrors])
|
|
81
74
|
def delete_iam_role(
|
|
82
75
|
role_name: str, region: Optional[str] = None, quiet: bool = True
|
|
83
76
|
) -> None:
|
|
84
|
-
from boto.iam.connection import IAMConnection
|
|
85
|
-
|
|
86
77
|
# TODO: the Boto3 type hints are a bit oversealous here; they want hundreds
|
|
87
78
|
# of overloads of the client-getting methods to exist based on the literal
|
|
88
79
|
# string passed in, to return exactly the right kind of client or resource.
|
|
@@ -92,9 +83,8 @@ def delete_iam_role(
|
|
|
92
83
|
# we wanted MyPy to be able to understand us. So at some point we should
|
|
93
84
|
# consider revising our API here to be less annoying to explain to the type
|
|
94
85
|
# checker.
|
|
95
|
-
iam_client =
|
|
96
|
-
iam_resource =
|
|
97
|
-
boto_iam_connection = IAMConnection()
|
|
86
|
+
iam_client = session.client('iam', region_name=region)
|
|
87
|
+
iam_resource = session.resource('iam', region_name=region)
|
|
98
88
|
role = iam_resource.Role(role_name)
|
|
99
89
|
# normal policies
|
|
100
90
|
for attached_policy in role.attached_policies.all():
|
|
@@ -103,17 +93,16 @@ def delete_iam_role(
|
|
|
103
93
|
# inline policies
|
|
104
94
|
for inline_policy in role.policies.all():
|
|
105
95
|
printq(f'Deleting inline policy: {inline_policy.policy_name} from role {role.name}', quiet)
|
|
106
|
-
|
|
107
|
-
boto_iam_connection.delete_role_policy(role.name, inline_policy.policy_name)
|
|
96
|
+
iam_client.delete_role_policy(RoleName=role.name, PolicyName=inline_policy.policy_name)
|
|
108
97
|
iam_client.delete_role(RoleName=role_name)
|
|
109
98
|
printq(f'Role {role_name} successfully deleted.', quiet)
|
|
110
99
|
|
|
111
100
|
|
|
112
|
-
@retry(errors=[
|
|
101
|
+
@retry(errors=[AWSServerErrors])
|
|
113
102
|
def delete_iam_instance_profile(
|
|
114
103
|
instance_profile_name: str, region: Optional[str] = None, quiet: bool = True
|
|
115
104
|
) -> None:
|
|
116
|
-
iam_resource =
|
|
105
|
+
iam_resource = session.resource("iam", region_name=region)
|
|
117
106
|
instance_profile = iam_resource.InstanceProfile(instance_profile_name)
|
|
118
107
|
if instance_profile.roles is not None:
|
|
119
108
|
for role in instance_profile.roles:
|
|
@@ -123,11 +112,11 @@ def delete_iam_instance_profile(
|
|
|
123
112
|
printq(f'Instance profile "{instance_profile_name}" successfully deleted.', quiet)
|
|
124
113
|
|
|
125
114
|
|
|
126
|
-
@retry(errors=[
|
|
115
|
+
@retry(errors=[AWSServerErrors])
|
|
127
116
|
def delete_sdb_domain(
|
|
128
117
|
sdb_domain_name: str, region: Optional[str] = None, quiet: bool = True
|
|
129
118
|
) -> None:
|
|
130
|
-
sdb_client =
|
|
119
|
+
sdb_client = session.client("sdb", region_name=region)
|
|
131
120
|
sdb_client.delete_domain(DomainName=sdb_domain_name)
|
|
132
121
|
printq(f'SBD Domain: "{sdb_domain_name}" successfully deleted.', quiet)
|
|
133
122
|
|
|
@@ -141,16 +130,24 @@ def connection_reset(e: Exception) -> bool:
|
|
|
141
130
|
# errno is listed as 104. To be safe, we check for both:
|
|
142
131
|
return isinstance(e, socket.error) and e.errno in (errno.ECONNRESET, 104)
|
|
143
132
|
|
|
133
|
+
def connection_error(e: Exception) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Return True if an error represents a failure to make a network connection.
|
|
136
|
+
"""
|
|
137
|
+
return (connection_reset(e)
|
|
138
|
+
or isinstance(e, EndpointConnectionError))
|
|
139
|
+
|
|
140
|
+
|
|
144
141
|
# TODO: Replace with: @retry and ErrorCondition
|
|
145
142
|
def retryable_s3_errors(e: Exception) -> bool:
|
|
146
143
|
"""
|
|
147
144
|
Return true if this is an error from S3 that looks like we ought to retry our request.
|
|
148
145
|
"""
|
|
149
|
-
return (
|
|
150
|
-
or (isinstance(e,
|
|
151
|
-
or (isinstance(e,
|
|
146
|
+
return (connection_error(e)
|
|
147
|
+
or (isinstance(e, ClientError) and get_error_status(e) in (429, 500))
|
|
148
|
+
or (isinstance(e, ClientError) and get_error_code(e) in THROTTLED_ERROR_CODES)
|
|
152
149
|
# boto3 errors
|
|
153
|
-
or (isinstance(e,
|
|
150
|
+
or (isinstance(e, ClientError) and get_error_code(e) in THROTTLED_ERROR_CODES)
|
|
154
151
|
or (isinstance(e, ClientError) and 'BucketNotEmpty' in str(e))
|
|
155
152
|
or (isinstance(e, ClientError) and e.response.get('ResponseMetadata', {}).get('HTTPStatusCode') == 409 and 'try again' in str(e))
|
|
156
153
|
or (isinstance(e, ClientError) and e.response.get('ResponseMetadata', {}).get('HTTPStatusCode') in (404, 429, 500, 502, 503, 504)))
|
|
@@ -162,7 +159,7 @@ def retry_s3(delays: Iterable[float] = DEFAULT_DELAYS, timeout: float = DEFAULT_
|
|
|
162
159
|
"""
|
|
163
160
|
return old_retry(delays=delays, timeout=timeout, predicate=predicate)
|
|
164
161
|
|
|
165
|
-
@retry(errors=[
|
|
162
|
+
@retry(errors=[AWSServerErrors])
|
|
166
163
|
def delete_s3_bucket(
|
|
167
164
|
s3_resource: "S3ServiceResource",
|
|
168
165
|
bucket: str,
|
|
@@ -195,7 +192,7 @@ def delete_s3_bucket(
|
|
|
195
192
|
def create_s3_bucket(
|
|
196
193
|
s3_resource: "S3ServiceResource",
|
|
197
194
|
bucket_name: str,
|
|
198
|
-
region:
|
|
195
|
+
region: AWSRegionName,
|
|
199
196
|
) -> "Bucket":
|
|
200
197
|
"""
|
|
201
198
|
Create an AWS S3 bucket, using the given Boto3 S3 session, with the
|
|
@@ -238,7 +235,7 @@ def enable_public_objects(bucket_name: str) -> None:
|
|
|
238
235
|
would be a very awkward way to do it. So we restore the old behavior.
|
|
239
236
|
"""
|
|
240
237
|
|
|
241
|
-
s3_client =
|
|
238
|
+
s3_client = session.client('s3')
|
|
242
239
|
|
|
243
240
|
# Even though the new default is for public access to be prohibited, this
|
|
244
241
|
# is implemented by adding new things attached to the bucket. If we remove
|
|
@@ -261,7 +258,7 @@ def get_bucket_region(bucket_name: str, endpoint_url: Optional[str] = None, only
|
|
|
261
258
|
:param only_strategies: For testing, use only strategies with 1-based numbers in this set.
|
|
262
259
|
"""
|
|
263
260
|
|
|
264
|
-
s3_client =
|
|
261
|
+
s3_client = session.client('s3', endpoint_url=endpoint_url)
|
|
265
262
|
|
|
266
263
|
def attempt_get_bucket_location() -> Optional[str]:
|
|
267
264
|
"""
|
|
@@ -283,7 +280,7 @@ def get_bucket_region(bucket_name: str, endpoint_url: Optional[str] = None, only
|
|
|
283
280
|
# It could also be because AWS open data buckets (which we tend to
|
|
284
281
|
# encounter this problem for) tend to actually themselves be in
|
|
285
282
|
# us-east-1.
|
|
286
|
-
backup_s3_client =
|
|
283
|
+
backup_s3_client = session.client('s3', region_name='us-east-1')
|
|
287
284
|
return backup_s3_client.get_bucket_location(Bucket=bucket_name).get('LocationConstraint', None)
|
|
288
285
|
|
|
289
286
|
def attempt_head_bucket() -> Optional[str]:
|
|
@@ -368,11 +365,11 @@ def get_object_for_url(url: ParseResult, existing: Optional[bool] = None) -> "Ob
|
|
|
368
365
|
try:
|
|
369
366
|
# Get the bucket's region to avoid a redirect per request
|
|
370
367
|
region = get_bucket_region(bucket_name, endpoint_url=endpoint_url)
|
|
371
|
-
s3 =
|
|
368
|
+
s3 = session.resource('s3', region_name=region, endpoint_url=endpoint_url)
|
|
372
369
|
except ClientError:
|
|
373
370
|
# Probably don't have permission.
|
|
374
371
|
# TODO: check if it is that
|
|
375
|
-
s3 =
|
|
372
|
+
s3 = session.resource('s3', endpoint_url=endpoint_url)
|
|
376
373
|
|
|
377
374
|
obj = s3.Object(bucket_name, key_name)
|
|
378
375
|
objExists = True
|
|
@@ -394,7 +391,7 @@ def get_object_for_url(url: ParseResult, existing: Optional[bool] = None) -> "Ob
|
|
|
394
391
|
return obj
|
|
395
392
|
|
|
396
393
|
|
|
397
|
-
@retry(errors=[
|
|
394
|
+
@retry(errors=[AWSServerErrors])
|
|
398
395
|
def list_objects_for_url(url: ParseResult) -> List[str]:
|
|
399
396
|
"""
|
|
400
397
|
Extracts a key (object) from a given parsed s3:// URL. The URL will be
|
|
@@ -419,7 +416,7 @@ def list_objects_for_url(url: ParseResult) -> List[str]:
|
|
|
419
416
|
if host:
|
|
420
417
|
endpoint_url = f'{protocol}://{host}' + f':{port}' if port else ''
|
|
421
418
|
|
|
422
|
-
client =
|
|
419
|
+
client = session.client('s3', endpoint_url=endpoint_url)
|
|
423
420
|
|
|
424
421
|
listing = []
|
|
425
422
|
|
|
@@ -444,3 +441,42 @@ def flatten_tags(tags: Dict[str, str]) -> List[Dict[str, str]]:
|
|
|
444
441
|
Convert tags from a key to value dict into a list of 'Key': xxx, 'Value': xxx dicts.
|
|
445
442
|
"""
|
|
446
443
|
return [{'Key': k, 'Value': v} for k, v in tags.items()]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def boto3_pager(requestor_callable: Callable[..., Any], result_attribute_name: str,
|
|
447
|
+
**kwargs: Any) -> Iterable[Any]:
|
|
448
|
+
"""
|
|
449
|
+
Yield all the results from calling the given Boto 3 method with the
|
|
450
|
+
given keyword arguments, paging through the results using the Marker or
|
|
451
|
+
NextToken, and fetching out and looping over the list in the response
|
|
452
|
+
with the given attribute name.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
# Recover the Boto3 client, and the name of the operation
|
|
456
|
+
client = requestor_callable.__self__ # type: ignore[attr-defined]
|
|
457
|
+
op_name = requestor_callable.__name__
|
|
458
|
+
|
|
459
|
+
# grab a Boto 3 built-in paginator. See
|
|
460
|
+
# <https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html>
|
|
461
|
+
paginator = client.get_paginator(op_name)
|
|
462
|
+
|
|
463
|
+
for page in paginator.paginate(**kwargs):
|
|
464
|
+
# Invoke it and go through the pages, yielding from them
|
|
465
|
+
yield from page.get(result_attribute_name, [])
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def get_item_from_attributes(attributes: List[AttributeTypeDef], name: str) -> Any:
|
|
469
|
+
"""
|
|
470
|
+
Given a list of attributes, find the attribute associated with the name and return its corresponding value.
|
|
471
|
+
|
|
472
|
+
The `attribute_list` will be a list of TypedDict's (which boto3 SDB functions commonly return),
|
|
473
|
+
where each TypedDict has a "Name" and "Value" key value pair.
|
|
474
|
+
This function grabs the value out of the associated TypedDict.
|
|
475
|
+
|
|
476
|
+
If the attribute with the name does not exist, the function will return None.
|
|
477
|
+
|
|
478
|
+
:param attributes: list of attributes as List[AttributeTypeDef]
|
|
479
|
+
:param name: name of the attribute
|
|
480
|
+
:return: value of the attribute
|
|
481
|
+
"""
|
|
482
|
+
return next((attribute["Value"] for attribute in attributes if attribute["Name"] == name), None)
|
toil/lib/conversions.py
CHANGED
|
@@ -4,7 +4,7 @@ Also contains general conversion functions
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import math
|
|
7
|
-
from typing import SupportsInt, Tuple, Union
|
|
7
|
+
from typing import SupportsInt, Tuple, Union, Optional
|
|
8
8
|
|
|
9
9
|
# See https://en.wikipedia.org/wiki/Binary_prefix
|
|
10
10
|
BINARY_PREFIXES = ['ki', 'mi', 'gi', 'ti', 'pi', 'ei', 'kib', 'mib', 'gib', 'tib', 'pib', 'eib']
|
|
@@ -147,3 +147,7 @@ def strtobool(val: str) -> bool:
|
|
|
147
147
|
return result
|
|
148
148
|
raise ValueError(f"Cannot convert \"{val}\" to a bool")
|
|
149
149
|
|
|
150
|
+
|
|
151
|
+
def opt_strtobool(b: Optional[str]) -> Optional[bool]:
|
|
152
|
+
"""Convert an optional string representation of bool to None or bool"""
|
|
153
|
+
return b if b is None else strtobool(b)
|