toil 6.1.0a1__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 +41 -17
- toil/batchSystems/abstractGridEngineBatchSystem.py +79 -65
- toil/batchSystems/awsBatch.py +8 -8
- toil/batchSystems/cleanup_support.py +7 -3
- 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 +9 -9
- 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 +129 -16
- toil/batchSystems/torque.py +1 -1
- toil/bus.py +45 -3
- toil/common.py +56 -31
- toil/cwl/cwltoil.py +442 -371
- toil/deferred.py +1 -1
- toil/exceptions.py +1 -1
- toil/fileStores/abstractFileStore.py +69 -20
- toil/fileStores/cachingFileStore.py +6 -22
- toil/fileStores/nonCachingFileStore.py +6 -15
- toil/job.py +270 -86
- toil/jobStores/abstractJobStore.py +37 -31
- toil/jobStores/aws/jobStore.py +280 -218
- toil/jobStores/aws/utils.py +60 -31
- toil/jobStores/conftest.py +2 -2
- toil/jobStores/fileJobStore.py +3 -3
- toil/jobStores/googleJobStore.py +3 -4
- toil/leader.py +89 -38
- 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 +24 -1
- toil/lib/ec2.py +118 -69
- toil/lib/expando.py +1 -1
- toil/lib/generatedEC2Lists.py +8 -8
- toil/lib/io.py +42 -4
- toil/lib/misc.py +1 -3
- toil/lib/resources.py +57 -16
- toil/lib/retry.py +12 -5
- toil/lib/threading.py +29 -14
- toil/lib/throttle.py +1 -1
- toil/options/common.py +31 -30
- toil/options/wdl.py +5 -0
- toil/provisioners/__init__.py +9 -3
- toil/provisioners/abstractProvisioner.py +12 -2
- 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 +93 -23
- 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 +22 -7
- toil/test/cactus/__init__.py +0 -0
- toil/test/cactus/test_cactus_integration.py +58 -0
- toil/test/cwl/cwlTest.py +245 -236
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +11 -14
- toil/test/jobStores/jobStoreTest.py +40 -54
- toil/test/lib/aws/test_iam.py +2 -2
- toil/test/lib/test_ec2.py +1 -1
- toil/test/options/__init__.py +13 -0
- toil/test/options/options.py +37 -0
- toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
- toil/test/provisioners/clusterTest.py +99 -16
- 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 +62 -4
- toil/test/utils/utilsTest.py +23 -21
- toil/test/wdl/wdltoil_test.py +49 -21
- toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
- toil/toilState.py +68 -9
- toil/utils/toilDebugFile.py +1 -1
- 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 +310 -266
- toil/utils/toilStatus.py +98 -52
- toil/version.py +11 -11
- toil/wdl/wdltoil.py +644 -225
- toil/worker.py +125 -83
- {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
- toil-7.0.0.dist-info/METADATA +158 -0
- {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/RECORD +103 -96
- {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
- toil-6.1.0a1.dist-info/METADATA +0 -125
- {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/top_level.txt +0 -0
toil/lib/aws/session.py
CHANGED
|
@@ -15,16 +15,21 @@ import collections
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
import threading
|
|
18
|
-
from typing import Dict, Optional, Tuple, cast
|
|
18
|
+
from typing import Dict, Optional, Tuple, cast, Union, Literal, overload, TypeVar
|
|
19
19
|
|
|
20
20
|
import boto3
|
|
21
21
|
import boto3.resources.base
|
|
22
|
-
import boto.connection
|
|
23
22
|
import botocore
|
|
24
23
|
from boto3 import Session
|
|
25
24
|
from botocore.client import Config
|
|
26
25
|
from botocore.session import get_session
|
|
27
26
|
from botocore.utils import JSONFileCache
|
|
27
|
+
from mypy_boto3_autoscaling import AutoScalingClient
|
|
28
|
+
from mypy_boto3_ec2 import EC2Client, EC2ServiceResource
|
|
29
|
+
from mypy_boto3_iam import IAMClient, IAMServiceResource
|
|
30
|
+
from mypy_boto3_s3 import S3Client, S3ServiceResource
|
|
31
|
+
from mypy_boto3_sdb import SimpleDBClient
|
|
32
|
+
from mypy_boto3_sts import STSClient
|
|
28
33
|
|
|
29
34
|
logger = logging.getLogger(__name__)
|
|
30
35
|
|
|
@@ -120,6 +125,13 @@ class AWSConnectionManager:
|
|
|
120
125
|
storage.item = _new_boto3_session(region_name=region)
|
|
121
126
|
return cast(boto3.session.Session, storage.item)
|
|
122
127
|
|
|
128
|
+
@overload
|
|
129
|
+
def resource(self, region: Optional[str], service_name: Literal["s3"], endpoint_url: Optional[str] = None) -> S3ServiceResource: ...
|
|
130
|
+
@overload
|
|
131
|
+
def resource(self, region: Optional[str], service_name: Literal["iam"], endpoint_url: Optional[str] = None) -> IAMServiceResource: ...
|
|
132
|
+
@overload
|
|
133
|
+
def resource(self, region: Optional[str], service_name: Literal["ec2"], endpoint_url: Optional[str] = None) -> EC2ServiceResource: ...
|
|
134
|
+
|
|
123
135
|
def resource(self, region: Optional[str], service_name: str, endpoint_url: Optional[str] = None) -> boto3.resources.base.ServiceResource:
|
|
124
136
|
"""
|
|
125
137
|
Get the Boto3 Resource to use with the given service (like 'ec2') in the given region.
|
|
@@ -146,7 +158,28 @@ class AWSConnectionManager:
|
|
|
146
158
|
|
|
147
159
|
return cast(boto3.resources.base.ServiceResource, storage.item)
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
@overload
|
|
162
|
+
def client(self, region: Optional[str], service_name: Literal["ec2"], endpoint_url: Optional[str] = None,
|
|
163
|
+
config: Optional[Config] = None) -> EC2Client: ...
|
|
164
|
+
@overload
|
|
165
|
+
def client(self, region: Optional[str], service_name: Literal["iam"], endpoint_url: Optional[str] = None,
|
|
166
|
+
config: Optional[Config] = None) -> IAMClient: ...
|
|
167
|
+
@overload
|
|
168
|
+
def client(self, region: Optional[str], service_name: Literal["s3"], endpoint_url: Optional[str] = None,
|
|
169
|
+
config: Optional[Config] = None) -> S3Client: ...
|
|
170
|
+
@overload
|
|
171
|
+
def client(self, region: Optional[str], service_name: Literal["sts"], endpoint_url: Optional[str] = None,
|
|
172
|
+
config: Optional[Config] = None) -> STSClient: ...
|
|
173
|
+
@overload
|
|
174
|
+
def client(self, region: Optional[str], service_name: Literal["sdb"], endpoint_url: Optional[str] = None,
|
|
175
|
+
config: Optional[Config] = None) -> SimpleDBClient: ...
|
|
176
|
+
@overload
|
|
177
|
+
def client(self, region: Optional[str], service_name: Literal["autoscaling"], endpoint_url: Optional[str] = None,
|
|
178
|
+
config: Optional[Config] = None) -> AutoScalingClient: ...
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def client(self, region: Optional[str], service_name: Literal["ec2", "iam", "s3", "sts", "sdb", "autoscaling"], endpoint_url: Optional[str] = None,
|
|
182
|
+
config: Optional[Config] = None) -> botocore.client.BaseClient:
|
|
150
183
|
"""
|
|
151
184
|
Get the Boto3 Client to use with the given service (like 'ec2') in the given region.
|
|
152
185
|
|
|
@@ -159,9 +192,9 @@ class AWSConnectionManager:
|
|
|
159
192
|
# Don't try and memoize if a custom config is used
|
|
160
193
|
with _init_lock:
|
|
161
194
|
if endpoint_url is not None:
|
|
162
|
-
return self.session(region).client(service_name, endpoint_url=endpoint_url, config=config)
|
|
195
|
+
return self.session(region).client(service_name, endpoint_url=endpoint_url, config=config)
|
|
163
196
|
else:
|
|
164
|
-
return self.session(region).client(service_name, config=config)
|
|
197
|
+
return self.session(region).client(service_name, config=config)
|
|
165
198
|
|
|
166
199
|
key = (region, service_name, endpoint_url)
|
|
167
200
|
storage = self.client_cache[key]
|
|
@@ -172,25 +205,12 @@ class AWSConnectionManager:
|
|
|
172
205
|
if endpoint_url is not None:
|
|
173
206
|
# The Boto3 stubs are probably missing an overload here too. See:
|
|
174
207
|
# <https://github.com/vemel/mypy_boto3_builder/issues/121#issuecomment-1011322636>
|
|
175
|
-
storage.item = self.session(region).client(service_name, endpoint_url=endpoint_url)
|
|
208
|
+
storage.item = self.session(region).client(service_name, endpoint_url=endpoint_url)
|
|
176
209
|
else:
|
|
177
210
|
# We might not be able to pass None to Boto3 and have it be the same as no argument.
|
|
178
|
-
storage.item = self.session(region).client(service_name)
|
|
211
|
+
storage.item = self.session(region).client(service_name)
|
|
179
212
|
return cast(botocore.client.BaseClient , storage.item)
|
|
180
213
|
|
|
181
|
-
def boto2(self, region: Optional[str], service_name: str) -> boto.connection.AWSAuthConnection:
|
|
182
|
-
"""
|
|
183
|
-
Get the connected boto2 connection for the given region and service.
|
|
184
|
-
"""
|
|
185
|
-
if service_name == 'iam':
|
|
186
|
-
# IAM connections are regionless
|
|
187
|
-
region = 'universal'
|
|
188
|
-
key = (region, service_name)
|
|
189
|
-
storage = self.boto2_cache[key]
|
|
190
|
-
if not hasattr(storage, 'item'):
|
|
191
|
-
with _init_lock:
|
|
192
|
-
storage.item = getattr(boto, service_name).connect_to_region(region, profile_name=os.environ.get("TOIL_AWS_PROFILE", None))
|
|
193
|
-
return cast(boto.connection.AWSAuthConnection, storage.item)
|
|
194
214
|
|
|
195
215
|
# If you don't want your own AWSConnectionManager, we have a global one and some global functions
|
|
196
216
|
_global_manager = AWSConnectionManager()
|
|
@@ -205,7 +225,20 @@ def establish_boto3_session(region_name: Optional[str] = None) -> Session:
|
|
|
205
225
|
# Just use a global version of the manager. Note that we change the argument order!
|
|
206
226
|
return _global_manager.session(region_name)
|
|
207
227
|
|
|
208
|
-
|
|
228
|
+
@overload
|
|
229
|
+
def client(service_name: Literal["ec2"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> EC2Client: ...
|
|
230
|
+
@overload
|
|
231
|
+
def client(service_name: Literal["iam"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> IAMClient: ...
|
|
232
|
+
@overload
|
|
233
|
+
def client(service_name: Literal["s3"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> S3Client: ...
|
|
234
|
+
@overload
|
|
235
|
+
def client(service_name: Literal["sts"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> STSClient: ...
|
|
236
|
+
@overload
|
|
237
|
+
def client(service_name: Literal["sdb"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> SimpleDBClient: ...
|
|
238
|
+
@overload
|
|
239
|
+
def client(service_name: Literal["autoscaling"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> AutoScalingClient: ...
|
|
240
|
+
|
|
241
|
+
def client(service_name: Literal["ec2", "iam", "s3", "sts", "sdb", "autoscaling"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> botocore.client.BaseClient:
|
|
209
242
|
"""
|
|
210
243
|
Get a Boto 3 client for a particular AWS service, usable by the current thread.
|
|
211
244
|
|
|
@@ -215,7 +248,14 @@ def client(service_name: str, region_name: Optional[str] = None, endpoint_url: O
|
|
|
215
248
|
# Just use a global version of the manager. Note that we change the argument order!
|
|
216
249
|
return _global_manager.client(region_name, service_name, endpoint_url=endpoint_url, config=config)
|
|
217
250
|
|
|
218
|
-
|
|
251
|
+
@overload
|
|
252
|
+
def resource(service_name: Literal["s3"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None) -> S3ServiceResource: ...
|
|
253
|
+
@overload
|
|
254
|
+
def resource(service_name: Literal["iam"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None) -> IAMServiceResource: ...
|
|
255
|
+
@overload
|
|
256
|
+
def resource(service_name: Literal["ec2"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None) -> EC2ServiceResource: ...
|
|
257
|
+
|
|
258
|
+
def resource(service_name: Literal["s3", "iam", "ec2"], region_name: Optional[str] = None, endpoint_url: Optional[str] = None) -> boto3.resources.base.ServiceResource:
|
|
219
259
|
"""
|
|
220
260
|
Get a Boto 3 resource for a particular AWS service, usable by the current thread.
|
|
221
261
|
|
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']
|
|
@@ -128,3 +128,26 @@ def hms_duration_to_seconds(hms: str) -> float:
|
|
|
128
128
|
seconds += float(vals_to_convert[2])
|
|
129
129
|
|
|
130
130
|
return seconds
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def strtobool(val: str) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Make a human-readable string into a bool.
|
|
136
|
+
|
|
137
|
+
Convert a string along the lines of "y", "1", "ON", "TrUe", or
|
|
138
|
+
"Yes" to True, and the corresponding false-ish values to False.
|
|
139
|
+
"""
|
|
140
|
+
# We only track prefixes, so "y" covers "y", "yes",
|
|
141
|
+
# and "yeah no" and makes them all True.
|
|
142
|
+
TABLE = {True: ["1", "on", "y", "t"], False: ["0", "off", "n", "f"]}
|
|
143
|
+
lowered = val.lower()
|
|
144
|
+
for result, prefixes in TABLE.items():
|
|
145
|
+
for prefix in prefixes:
|
|
146
|
+
if lowered.startswith(prefix):
|
|
147
|
+
return result
|
|
148
|
+
raise ValueError(f"Cannot convert \"{val}\" to a bool")
|
|
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)
|