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.
Files changed (93) hide show
  1. toil/__init__.py +1 -232
  2. toil/batchSystems/abstractBatchSystem.py +22 -13
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +59 -45
  4. toil/batchSystems/awsBatch.py +8 -8
  5. toil/batchSystems/contained_executor.py +4 -5
  6. toil/batchSystems/gridengine.py +1 -1
  7. toil/batchSystems/htcondor.py +5 -5
  8. toil/batchSystems/kubernetes.py +25 -11
  9. toil/batchSystems/local_support.py +3 -3
  10. toil/batchSystems/lsf.py +2 -2
  11. toil/batchSystems/mesos/batchSystem.py +4 -4
  12. toil/batchSystems/mesos/executor.py +3 -2
  13. toil/batchSystems/options.py +9 -0
  14. toil/batchSystems/singleMachine.py +11 -10
  15. toil/batchSystems/slurm.py +64 -22
  16. toil/batchSystems/torque.py +1 -1
  17. toil/bus.py +7 -3
  18. toil/common.py +36 -13
  19. toil/cwl/cwltoil.py +365 -312
  20. toil/deferred.py +1 -1
  21. toil/fileStores/abstractFileStore.py +17 -17
  22. toil/fileStores/cachingFileStore.py +2 -2
  23. toil/fileStores/nonCachingFileStore.py +1 -1
  24. toil/job.py +228 -60
  25. toil/jobStores/abstractJobStore.py +18 -10
  26. toil/jobStores/aws/jobStore.py +280 -218
  27. toil/jobStores/aws/utils.py +57 -29
  28. toil/jobStores/conftest.py +2 -2
  29. toil/jobStores/fileJobStore.py +2 -2
  30. toil/jobStores/googleJobStore.py +3 -4
  31. toil/leader.py +72 -24
  32. toil/lib/aws/__init__.py +26 -10
  33. toil/lib/aws/iam.py +2 -2
  34. toil/lib/aws/session.py +62 -22
  35. toil/lib/aws/utils.py +73 -37
  36. toil/lib/conversions.py +5 -1
  37. toil/lib/ec2.py +118 -69
  38. toil/lib/expando.py +1 -1
  39. toil/lib/io.py +14 -2
  40. toil/lib/misc.py +1 -3
  41. toil/lib/resources.py +55 -21
  42. toil/lib/retry.py +12 -5
  43. toil/lib/threading.py +2 -2
  44. toil/lib/throttle.py +1 -1
  45. toil/options/common.py +27 -24
  46. toil/provisioners/__init__.py +9 -3
  47. toil/provisioners/abstractProvisioner.py +9 -7
  48. toil/provisioners/aws/__init__.py +20 -15
  49. toil/provisioners/aws/awsProvisioner.py +406 -329
  50. toil/provisioners/gceProvisioner.py +2 -2
  51. toil/provisioners/node.py +13 -5
  52. toil/server/app.py +1 -1
  53. toil/statsAndLogging.py +58 -16
  54. toil/test/__init__.py +27 -12
  55. toil/test/batchSystems/batchSystemTest.py +40 -33
  56. toil/test/batchSystems/batch_system_plugin_test.py +79 -0
  57. toil/test/batchSystems/test_slurm.py +1 -1
  58. toil/test/cwl/cwlTest.py +8 -91
  59. toil/test/cwl/seqtk_seq.cwl +1 -1
  60. toil/test/docs/scriptsTest.py +10 -13
  61. toil/test/jobStores/jobStoreTest.py +33 -49
  62. toil/test/lib/aws/test_iam.py +2 -2
  63. toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
  64. toil/test/provisioners/clusterTest.py +90 -8
  65. toil/test/server/serverTest.py +2 -2
  66. toil/test/src/autoDeploymentTest.py +1 -1
  67. toil/test/src/dockerCheckTest.py +2 -1
  68. toil/test/src/environmentTest.py +125 -0
  69. toil/test/src/fileStoreTest.py +1 -1
  70. toil/test/src/jobDescriptionTest.py +18 -8
  71. toil/test/src/jobTest.py +1 -1
  72. toil/test/src/realtimeLoggerTest.py +4 -0
  73. toil/test/src/workerTest.py +52 -19
  74. toil/test/utils/toilDebugTest.py +61 -3
  75. toil/test/utils/utilsTest.py +20 -18
  76. toil/test/wdl/wdltoil_test.py +24 -71
  77. toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
  78. toil/toilState.py +68 -9
  79. toil/utils/toilDebugJob.py +153 -26
  80. toil/utils/toilLaunchCluster.py +12 -2
  81. toil/utils/toilRsyncCluster.py +7 -2
  82. toil/utils/toilSshCluster.py +7 -3
  83. toil/utils/toilStats.py +2 -1
  84. toil/utils/toilStatus.py +97 -51
  85. toil/version.py +10 -10
  86. toil/wdl/wdltoil.py +318 -51
  87. toil/worker.py +96 -69
  88. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
  89. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/METADATA +55 -21
  90. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/RECORD +93 -90
  91. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
  92. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
  93. {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 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']
@@ -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)