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.
Files changed (104) hide show
  1. toil/__init__.py +1 -232
  2. toil/batchSystems/abstractBatchSystem.py +41 -17
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +79 -65
  4. toil/batchSystems/awsBatch.py +8 -8
  5. toil/batchSystems/cleanup_support.py +7 -3
  6. toil/batchSystems/contained_executor.py +4 -5
  7. toil/batchSystems/gridengine.py +1 -1
  8. toil/batchSystems/htcondor.py +5 -5
  9. toil/batchSystems/kubernetes.py +25 -11
  10. toil/batchSystems/local_support.py +3 -3
  11. toil/batchSystems/lsf.py +9 -9
  12. toil/batchSystems/mesos/batchSystem.py +4 -4
  13. toil/batchSystems/mesos/executor.py +3 -2
  14. toil/batchSystems/options.py +9 -0
  15. toil/batchSystems/singleMachine.py +11 -10
  16. toil/batchSystems/slurm.py +129 -16
  17. toil/batchSystems/torque.py +1 -1
  18. toil/bus.py +45 -3
  19. toil/common.py +56 -31
  20. toil/cwl/cwltoil.py +442 -371
  21. toil/deferred.py +1 -1
  22. toil/exceptions.py +1 -1
  23. toil/fileStores/abstractFileStore.py +69 -20
  24. toil/fileStores/cachingFileStore.py +6 -22
  25. toil/fileStores/nonCachingFileStore.py +6 -15
  26. toil/job.py +270 -86
  27. toil/jobStores/abstractJobStore.py +37 -31
  28. toil/jobStores/aws/jobStore.py +280 -218
  29. toil/jobStores/aws/utils.py +60 -31
  30. toil/jobStores/conftest.py +2 -2
  31. toil/jobStores/fileJobStore.py +3 -3
  32. toil/jobStores/googleJobStore.py +3 -4
  33. toil/leader.py +89 -38
  34. toil/lib/aws/__init__.py +26 -10
  35. toil/lib/aws/iam.py +2 -2
  36. toil/lib/aws/session.py +62 -22
  37. toil/lib/aws/utils.py +73 -37
  38. toil/lib/conversions.py +24 -1
  39. toil/lib/ec2.py +118 -69
  40. toil/lib/expando.py +1 -1
  41. toil/lib/generatedEC2Lists.py +8 -8
  42. toil/lib/io.py +42 -4
  43. toil/lib/misc.py +1 -3
  44. toil/lib/resources.py +57 -16
  45. toil/lib/retry.py +12 -5
  46. toil/lib/threading.py +29 -14
  47. toil/lib/throttle.py +1 -1
  48. toil/options/common.py +31 -30
  49. toil/options/wdl.py +5 -0
  50. toil/provisioners/__init__.py +9 -3
  51. toil/provisioners/abstractProvisioner.py +12 -2
  52. toil/provisioners/aws/__init__.py +20 -15
  53. toil/provisioners/aws/awsProvisioner.py +406 -329
  54. toil/provisioners/gceProvisioner.py +2 -2
  55. toil/provisioners/node.py +13 -5
  56. toil/server/app.py +1 -1
  57. toil/statsAndLogging.py +93 -23
  58. toil/test/__init__.py +27 -12
  59. toil/test/batchSystems/batchSystemTest.py +40 -33
  60. toil/test/batchSystems/batch_system_plugin_test.py +79 -0
  61. toil/test/batchSystems/test_slurm.py +22 -7
  62. toil/test/cactus/__init__.py +0 -0
  63. toil/test/cactus/test_cactus_integration.py +58 -0
  64. toil/test/cwl/cwlTest.py +245 -236
  65. toil/test/cwl/seqtk_seq.cwl +1 -1
  66. toil/test/docs/scriptsTest.py +11 -14
  67. toil/test/jobStores/jobStoreTest.py +40 -54
  68. toil/test/lib/aws/test_iam.py +2 -2
  69. toil/test/lib/test_ec2.py +1 -1
  70. toil/test/options/__init__.py +13 -0
  71. toil/test/options/options.py +37 -0
  72. toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
  73. toil/test/provisioners/clusterTest.py +99 -16
  74. toil/test/server/serverTest.py +2 -2
  75. toil/test/src/autoDeploymentTest.py +1 -1
  76. toil/test/src/dockerCheckTest.py +2 -1
  77. toil/test/src/environmentTest.py +125 -0
  78. toil/test/src/fileStoreTest.py +1 -1
  79. toil/test/src/jobDescriptionTest.py +18 -8
  80. toil/test/src/jobTest.py +1 -1
  81. toil/test/src/realtimeLoggerTest.py +4 -0
  82. toil/test/src/workerTest.py +52 -19
  83. toil/test/utils/toilDebugTest.py +62 -4
  84. toil/test/utils/utilsTest.py +23 -21
  85. toil/test/wdl/wdltoil_test.py +49 -21
  86. toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
  87. toil/toilState.py +68 -9
  88. toil/utils/toilDebugFile.py +1 -1
  89. toil/utils/toilDebugJob.py +153 -26
  90. toil/utils/toilLaunchCluster.py +12 -2
  91. toil/utils/toilRsyncCluster.py +7 -2
  92. toil/utils/toilSshCluster.py +7 -3
  93. toil/utils/toilStats.py +310 -266
  94. toil/utils/toilStatus.py +98 -52
  95. toil/version.py +11 -11
  96. toil/wdl/wdltoil.py +644 -225
  97. toil/worker.py +125 -83
  98. {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
  99. toil-7.0.0.dist-info/METADATA +158 -0
  100. {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/RECORD +103 -96
  101. {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
  102. toil-6.1.0a1.dist-info/METADATA +0 -125
  103. {toil-6.1.0a1.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
  104. {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
- def client(self, region: Optional[str], service_name: str, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> botocore.client.BaseClient:
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) # type: ignore
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) # type: ignore
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) # type: ignore
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) # type: ignore
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
- def client(service_name: str, region_name: Optional[str] = None, endpoint_url: Optional[str] = None, config: Optional[Config] = None) -> botocore.client.BaseClient:
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
- def resource(service_name: str, region_name: Optional[str] = None, endpoint_url: Optional[str] = None) -> boto3.resources.base.ServiceResource:
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 toil.lib.aws import session
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 boto.exception import BotoServerError, S3ResponseError
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=[BotoServerError])
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 = cast(IAMClient, session.client('iam', region_name=region))
96
- iam_resource = cast(IAMServiceResource, session.resource('iam', region_name=region))
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
- # couldn't find an easy way to remove inline policies with boto3; use boto
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=[BotoServerError])
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 = cast(IAMServiceResource, session.resource("iam", region_name=region))
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=[BotoServerError])
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 = cast(SimpleDBClient, session.client("sdb", region_name=region))
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 (connection_reset(e)
150
- or (isinstance(e, BotoServerError) and e.status in (429, 500))
151
- or (isinstance(e, BotoServerError) and e.code in THROTTLED_ERROR_CODES)
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, (S3ResponseError, ClientError)) and get_error_code(e) in THROTTLED_ERROR_CODES)
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=[BotoServerError])
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: Union["BucketLocationConstraintType", Literal["us-east-1"]],
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 = cast(S3Client, session.client('s3'))
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 = cast(S3Client, session.client('s3', endpoint_url=endpoint_url))
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 = cast(S3Client, session.client('s3', region_name='us-east-1'))
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 = cast(S3ServiceResource, session.resource('s3', region_name=region, endpoint_url=endpoint_url))
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 = cast(S3ServiceResource, session.resource('s3', endpoint_url=endpoint_url))
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=[BotoServerError])
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 = cast(S3Client, session.client('s3', endpoint_url=endpoint_url))
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)