toil 9.0.0__py3-none-any.whl → 9.1.1__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 (71) hide show
  1. toil/batchSystems/abstractBatchSystem.py +13 -5
  2. toil/batchSystems/abstractGridEngineBatchSystem.py +17 -5
  3. toil/batchSystems/kubernetes.py +13 -2
  4. toil/batchSystems/mesos/batchSystem.py +33 -2
  5. toil/batchSystems/slurm.py +191 -16
  6. toil/cwl/cwltoil.py +17 -82
  7. toil/fileStores/__init__.py +1 -1
  8. toil/fileStores/abstractFileStore.py +5 -2
  9. toil/fileStores/cachingFileStore.py +1 -1
  10. toil/job.py +30 -14
  11. toil/jobStores/abstractJobStore.py +24 -19
  12. toil/jobStores/aws/jobStore.py +862 -1963
  13. toil/jobStores/aws/utils.py +24 -270
  14. toil/jobStores/googleJobStore.py +25 -9
  15. toil/jobStores/utils.py +0 -327
  16. toil/leader.py +27 -22
  17. toil/lib/aws/config.py +22 -0
  18. toil/lib/aws/s3.py +477 -9
  19. toil/lib/aws/utils.py +22 -33
  20. toil/lib/checksum.py +88 -0
  21. toil/lib/conversions.py +33 -31
  22. toil/lib/directory.py +217 -0
  23. toil/lib/ec2.py +97 -29
  24. toil/lib/exceptions.py +2 -1
  25. toil/lib/expando.py +2 -2
  26. toil/lib/generatedEC2Lists.py +73 -16
  27. toil/lib/io.py +33 -2
  28. toil/lib/memoize.py +21 -7
  29. toil/lib/pipes.py +385 -0
  30. toil/lib/retry.py +1 -1
  31. toil/lib/threading.py +1 -1
  32. toil/lib/web.py +4 -5
  33. toil/provisioners/__init__.py +5 -2
  34. toil/provisioners/aws/__init__.py +43 -36
  35. toil/provisioners/aws/awsProvisioner.py +22 -13
  36. toil/provisioners/node.py +60 -12
  37. toil/resource.py +3 -13
  38. toil/test/__init__.py +14 -16
  39. toil/test/batchSystems/test_slurm.py +103 -14
  40. toil/test/cwl/staging_cat.cwl +27 -0
  41. toil/test/cwl/staging_make_file.cwl +25 -0
  42. toil/test/cwl/staging_workflow.cwl +43 -0
  43. toil/test/cwl/zero_default.cwl +61 -0
  44. toil/test/docs/scripts/tutorial_staging.py +17 -8
  45. toil/test/jobStores/jobStoreTest.py +23 -133
  46. toil/test/lib/aws/test_iam.py +7 -7
  47. toil/test/lib/aws/test_s3.py +30 -33
  48. toil/test/lib/aws/test_utils.py +9 -9
  49. toil/test/provisioners/aws/awsProvisionerTest.py +59 -6
  50. toil/test/src/autoDeploymentTest.py +2 -3
  51. toil/test/src/fileStoreTest.py +89 -87
  52. toil/test/utils/ABCWorkflowDebug/ABC.txt +1 -0
  53. toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +4 -4
  54. toil/test/utils/toilKillTest.py +35 -28
  55. toil/test/wdl/md5sum/md5sum.json +1 -1
  56. toil/test/wdl/testfiles/gather.wdl +52 -0
  57. toil/test/wdl/wdltoil_test.py +120 -38
  58. toil/test/wdl/wdltoil_test_kubernetes.py +9 -0
  59. toil/utils/toilDebugFile.py +6 -3
  60. toil/utils/toilStats.py +17 -2
  61. toil/version.py +6 -6
  62. toil/wdl/wdltoil.py +1038 -549
  63. toil/worker.py +5 -2
  64. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/METADATA +12 -12
  65. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/RECORD +69 -61
  66. toil/lib/iterables.py +0 -112
  67. toil/test/docs/scripts/stagingExampleFiles/in.txt +0 -1
  68. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/WHEEL +0 -0
  69. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/entry_points.txt +0 -0
  70. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/licenses/LICENSE +0 -0
  71. {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@ import logging
16
16
  from collections import namedtuple
17
17
  from operator import attrgetter
18
18
  from statistics import mean, stdev
19
- from typing import Optional
19
+ from typing import Optional, TYPE_CHECKING
20
20
 
21
21
  from botocore.client import BaseClient
22
22
 
@@ -26,11 +26,15 @@ from toil.lib.aws import (
26
26
  get_aws_zone_from_environment_region,
27
27
  get_aws_zone_from_metadata,
28
28
  )
29
+ from toil.lib.aws.utils import boto3_pager
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
32
33
  ZoneTuple = namedtuple("ZoneTuple", ["name", "price_deviation"])
33
34
 
35
+ if TYPE_CHECKING:
36
+ from mypy_boto3_ec2.type_defs import SpotPriceTypeDef
37
+
34
38
 
35
39
  def get_aws_zone_from_spot_market(
36
40
  spotBid: Optional[float],
@@ -109,7 +113,7 @@ def get_best_aws_zone(
109
113
  def choose_spot_zone(
110
114
  zones: list[str],
111
115
  bid: float,
112
- spot_history: list["boto.ec2.spotpricehistory.SpotPriceHistory"],
116
+ spot_history: list["SpotPriceTypeDef"],
113
117
  ) -> str:
114
118
  """
115
119
  Returns the zone to put the spot request based on, in order of priority:
@@ -120,27 +124,26 @@ def choose_spot_zone(
120
124
 
121
125
  :return: the name of the selected zone
122
126
 
123
- >>> from collections import namedtuple
124
- >>> FauxHistory = namedtuple('FauxHistory', ['price', 'availability_zone'])
125
127
  >>> zones = ['us-west-2a', 'us-west-2b']
126
- >>> spot_history = [FauxHistory(0.1, 'us-west-2a'), \
127
- FauxHistory(0.2, 'us-west-2a'), \
128
- FauxHistory(0.3, 'us-west-2b'), \
129
- FauxHistory(0.6, 'us-west-2b')]
128
+ >>> FauxHistory = lambda p, z: {"SpotPrice": p, "AvailabilityZone": z}
129
+ >>> spot_history = [FauxHistory("0.1", 'us-west-2a'), \
130
+ FauxHistory("0.2", 'us-west-2a'), \
131
+ FauxHistory("0.3", 'us-west-2b'), \
132
+ FauxHistory("0.6", 'us-west-2b')]
130
133
  >>> choose_spot_zone(zones, 0.15, spot_history)
131
134
  'us-west-2a'
132
135
 
133
- >>> spot_history=[FauxHistory(0.3, 'us-west-2a'), \
134
- FauxHistory(0.2, 'us-west-2a'), \
135
- FauxHistory(0.1, 'us-west-2b'), \
136
- FauxHistory(0.6, 'us-west-2b')]
136
+ >>> spot_history=[FauxHistory("0.3", 'us-west-2a'), \
137
+ FauxHistory("0.2", 'us-west-2a'), \
138
+ FauxHistory("0.1", 'us-west-2b'), \
139
+ FauxHistory("0.6", 'us-west-2b')]
137
140
  >>> choose_spot_zone(zones, 0.15, spot_history)
138
141
  'us-west-2b'
139
142
 
140
- >>> spot_history=[FauxHistory(0.1, 'us-west-2a'), \
141
- FauxHistory(0.7, 'us-west-2a'), \
142
- FauxHistory(0.1, 'us-west-2b'), \
143
- FauxHistory(0.6, 'us-west-2b')]
143
+ >>> spot_history=[FauxHistory("0.1", 'us-west-2a'), \
144
+ FauxHistory("0.7", 'us-west-2a'), \
145
+ FauxHistory("0.1", 'us-west-2b'), \
146
+ FauxHistory("0.6", 'us-west-2b')]
144
147
  >>> choose_spot_zone(zones, 0.15, spot_history)
145
148
  'us-west-2b'
146
149
  """
@@ -152,11 +155,11 @@ def choose_spot_zone(
152
155
  zone_histories = [
153
156
  zone_history
154
157
  for zone_history in spot_history
155
- if zone_history.availability_zone == zone
158
+ if zone_history["AvailabilityZone"] == zone
156
159
  ]
157
160
  if zone_histories:
158
- price_deviation = stdev([history.price for history in zone_histories])
159
- recent_price = zone_histories[0].price
161
+ price_deviation = stdev([float(history["SpotPrice"]) for history in zone_histories])
162
+ recent_price = float(zone_histories[0]["SpotPrice"])
160
163
  else:
161
164
  price_deviation, recent_price = 0.0, bid
162
165
  zone_tuple = ZoneTuple(name=zone, price_deviation=price_deviation)
@@ -169,7 +172,7 @@ def choose_spot_zone(
169
172
 
170
173
  def optimize_spot_bid(
171
174
  boto3_ec2: BaseClient, instance_type: str, spot_bid: float, zone_options: list[str]
172
- ):
175
+ ) -> str:
173
176
  """
174
177
  Check whether the bid is in line with history and makes an effort to place
175
178
  the instance in a sensible zone.
@@ -179,30 +182,29 @@ def optimize_spot_bid(
179
182
  """
180
183
  spot_history = _get_spot_history(boto3_ec2, instance_type)
181
184
  if spot_history:
182
- _check_spot_bid(spot_bid, spot_history)
185
+ _check_spot_bid(spot_bid, spot_history, name=instance_type)
183
186
  most_stable_zone = choose_spot_zone(zone_options, spot_bid, spot_history)
184
187
  logger.debug("Placing spot instances in zone %s.", most_stable_zone)
185
188
  return most_stable_zone
186
189
 
187
190
 
188
- def _check_spot_bid(spot_bid, spot_history):
191
+ def _check_spot_bid(spot_bid: float, spot_history: list["SpotPriceTypeDef"], name: Optional[str] = None) -> None:
189
192
  """
190
193
  Prevents users from potentially over-paying for instances
191
194
 
192
195
  Note: this checks over the whole region, not a particular zone
193
196
 
194
- :param spot_bid: float
197
+ :param spot_bid: The proposed bid in dollars per hour.
195
198
 
196
- :type spot_history: list[SpotPriceHistory]
199
+ :type spot_history: The recent history of the spot price
197
200
 
198
201
  :raises UserError: if bid is > 2X the spot price's average
199
202
 
200
- >>> from collections import namedtuple
201
- >>> FauxHistory = namedtuple( "FauxHistory", [ "price", "availability_zone" ] )
202
- >>> spot_data = [ FauxHistory( 0.1, "us-west-2a" ), \
203
- FauxHistory( 0.2, "us-west-2a" ), \
204
- FauxHistory( 0.3, "us-west-2b" ), \
205
- FauxHistory( 0.6, "us-west-2b" ) ]
203
+ >>> FauxHistory = lambda p, z: {"SpotPrice": p, "AvailabilityZone": z}
204
+ >>> spot_data = [ FauxHistory( "0.1", "us-west-2a" ), \
205
+ FauxHistory( "0.2", "us-west-2a" ), \
206
+ FauxHistory( "0.3", "us-west-2b" ), \
207
+ FauxHistory( "0.6", "us-west-2b" ) ]
206
208
  >>> # noinspection PyProtectedMember
207
209
  >>> _check_spot_bid( 0.1, spot_data )
208
210
  >>> # noinspection PyProtectedMember
@@ -212,17 +214,21 @@ def _check_spot_bid(spot_bid, spot_history):
212
214
  ...
213
215
  UserError: Your bid $ 2.000000 is more than double this instance type's average spot price ($ 0.300000) over the last week
214
216
  """
215
- average = mean([datum.price for datum in spot_history])
217
+ if name is None:
218
+ # Describe the instance as something
219
+ name = "this instance type"
220
+ average = mean([float(datum["SpotPrice"]) for datum in spot_history])
216
221
  if spot_bid > average * 2:
217
222
  logger.warning(
218
- "Your bid $ %f is more than double this instance type's average "
223
+ "Your bid $ %f is more than double %s's average "
219
224
  "spot price ($ %f) over the last week",
220
225
  spot_bid,
226
+ name,
221
227
  average,
222
228
  )
223
229
 
224
230
 
225
- def _get_spot_history(boto3_ec2: BaseClient, instance_type: str):
231
+ def _get_spot_history(boto3_ec2: BaseClient, instance_type: str) -> list["SpotPriceTypeDef"]:
226
232
  """
227
233
  Returns list of 1,000 most recent spot market data points represented as SpotPriceHistory
228
234
  objects. Note: The most recent object/data point will be first in the list.
@@ -230,10 +236,11 @@ def _get_spot_history(boto3_ec2: BaseClient, instance_type: str):
230
236
  :rtype: list[SpotPriceHistory]
231
237
  """
232
238
  one_week_ago = datetime.datetime.now() - datetime.timedelta(days=7)
233
- spot_data = boto3_ec2.describe_spot_price_history(
239
+ spot_data = boto3_pager(
240
+ boto3_ec2.describe_spot_price_history,
241
+ "SpotPriceHistory",
234
242
  StartTime=one_week_ago.isoformat(),
235
243
  InstanceTypes=[instance_type],
236
244
  ProductDescriptions=["Linux/UNIX"],
237
245
  )
238
- spot_data.sort(key=attrgetter("timestamp"), reverse=True)
239
- return spot_data
246
+ return sorted(spot_data, key=lambda d: d["Timestamp"], reverse=True)
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import base64
16
17
  import json
17
18
  import logging
18
19
  import os
@@ -25,7 +26,7 @@ import uuid
25
26
  from collections.abc import Collection, Iterable
26
27
  from functools import wraps
27
28
  from shlex import quote
28
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
29
+ from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast
29
30
 
30
31
  # We need these to exist as attributes we can get off of the boto object
31
32
  from botocore.exceptions import ClientError
@@ -449,7 +450,7 @@ class AWSProvisioner(AbstractProvisioner):
449
450
  :return: None
450
451
  """
451
452
 
452
- if "network" in kwargs:
453
+ if kwargs.get("network") is not None:
453
454
  logger.warning(
454
455
  "AWS provisioner does not support a network parameter. Ignoring %s!",
455
456
  kwargs["network"],
@@ -1018,28 +1019,29 @@ class AWSProvisioner(AbstractProvisioner):
1018
1019
  userData: str = self._getIgnitionUserData(
1019
1020
  "worker", keyPath, preemptible, self._architecture
1020
1021
  )
1021
- userDataBytes: bytes = b""
1022
- if isinstance(userData, str):
1023
- # Spot-market provisioning requires bytes for user data.
1024
- userDataBytes = userData.encode("utf-8")
1025
-
1026
- spot_kwargs = {
1027
- "KeyName": self._keyName,
1022
+ # Boto 3 demands we base64 the user data ourselves *only* for spot
1023
+ # instances, and still wants a str.
1024
+ spot_user_data = base64.b64encode(
1025
+ userData.encode("utf-8")
1026
+ ).decode("utf-8")
1027
+ spot_kwargs: dict[Literal["LaunchSpecification"], dict[str, Any]] = {
1028
1028
  "LaunchSpecification": {
1029
+ "KeyName": self._keyName,
1029
1030
  "SecurityGroupIds": self._getSecurityGroupIDs(),
1030
1031
  "InstanceType": type_info.name,
1031
- "UserData": userDataBytes,
1032
+ "UserData": spot_user_data,
1032
1033
  "BlockDeviceMappings": bdm,
1033
1034
  "IamInstanceProfile": {"Arn": self._leaderProfileArn},
1034
1035
  "Placement": {"AvailabilityZone": zone},
1035
1036
  "SubnetId": subnet_id,
1036
1037
  },
1037
1038
  }
1039
+
1038
1040
  on_demand_kwargs = {
1039
1041
  "KeyName": self._keyName,
1040
1042
  "SecurityGroupIds": self._getSecurityGroupIDs(),
1041
1043
  "InstanceType": type_info.name,
1042
- "UserData": userDataBytes,
1044
+ "UserData": userData,
1043
1045
  "BlockDeviceMappings": bdm,
1044
1046
  "IamInstanceProfile": {"Arn": self._leaderProfileArn},
1045
1047
  "Placement": {"AvailabilityZone": zone},
@@ -1055,6 +1057,10 @@ class AWSProvisioner(AbstractProvisioner):
1055
1057
  # every request in this method
1056
1058
  if not preemptible:
1057
1059
  logger.debug("Launching %s non-preemptible nodes", numNodes)
1060
+ # TODO: Use create_instances() instead, which requires
1061
+ # refactoring both ondemand and spot sides here to use
1062
+ # mypy_boto3_ec2.service_resource.Instance objects instead
1063
+ # of mypy_boto3_ec2.type_defs.InstanceTypeDef
1058
1064
  instancesLaunched = create_ondemand_instances(
1059
1065
  boto3_ec2=boto3_ec2,
1060
1066
  image_id=self._discoverAMI(),
@@ -1082,7 +1088,6 @@ class AWSProvisioner(AbstractProvisioner):
1082
1088
  reservation
1083
1089
  for subdict in generatedInstancesLaunched
1084
1090
  for reservation in subdict["Reservations"]
1085
- for key, value in subdict.items()
1086
1091
  ]
1087
1092
  # get a flattened list of all requested instances, as before instancesLaunched is a dict of reservations which is a dict of instance requests
1088
1093
  instancesLaunched = [
@@ -1532,11 +1537,15 @@ class AWSProvisioner(AbstractProvisioner):
1532
1537
  tags: list[TagDescriptionTypeDef] = ec2.describe_tags(Filters=[tag_filter])[
1533
1538
  "Tags"
1534
1539
  ]
1540
+ # TODO: Does this reference instance or spot request? Or can it be either?
1535
1541
  idsToCancel = [tag["ResourceId"] for tag in tags]
1536
1542
  return [
1537
1543
  request["SpotInstanceRequestId"]
1538
1544
  for request in requests
1539
- if request["InstanceId"] in idsToCancel
1545
+ if (
1546
+ request.get("InstanceId") in idsToCancel
1547
+ or request["SpotInstanceRequestId"] in idsToCancel
1548
+ )
1540
1549
  ]
1541
1550
 
1542
1551
  def _createSecurityGroups(self) -> list[str]:
toil/provisioners/node.py CHANGED
@@ -41,6 +41,21 @@ class Node:
41
41
  tags: Optional[dict[str, str]] = None,
42
42
  use_private_ip: Optional[bool] = None,
43
43
  ) -> None:
44
+ """
45
+ Create a new node.
46
+
47
+ :param launchTime: Time when the node was launched. If a naive
48
+ datetime, or a string without timezone information, is assumed to
49
+ be in UTC.
50
+
51
+ :raises ValueError: If launchTime is an improperly formatted string.
52
+
53
+ >>> node = Node("127.0.0.1", "127.0.0.1", "localhost",
54
+ ... "Decembruary Eleventeenth", None, False)
55
+ Traceback (most recent call last):
56
+ ...
57
+ ValueError: Invalid isoformat string: 'Decembruary Eleventeenth'
58
+ """
44
59
  self.publicIP = publicIP
45
60
  self.privateIP = privateIP
46
61
  if use_private_ip:
@@ -48,13 +63,25 @@ class Node:
48
63
  else:
49
64
  self.effectiveIP = self.publicIP or self.privateIP
50
65
  self.name = name
66
+ # Typing should prevent an empty launch time, but just to make sure,
67
+ # check it at runtime.
68
+ assert launchTime is not None, (
69
+ f"Attempted to create a Node {name} without a launch time"
70
+ )
51
71
  if isinstance(launchTime, datetime.datetime):
52
72
  self.launchTime = launchTime
53
73
  else:
54
74
  try:
75
+ # Parse an RFC 3339 ISO 8601 UTC datetime
55
76
  self.launchTime = parse_iso_utc(launchTime)
56
77
  except ValueError:
78
+ # Parse (almost) any ISO 8601 datetime
57
79
  self.launchTime = datetime.datetime.fromisoformat(launchTime)
80
+ if self.launchTime.tzinfo is None:
81
+ # Read naive datatimes as in UTC
82
+ self.launchTime = self.launchTime.replace(
83
+ tzinfo=datetime.timezone.utc
84
+ )
58
85
  self.nodeType = nodeType
59
86
  self.preemptible = preemptible
60
87
  self.tags = tags
@@ -70,22 +97,43 @@ class Node:
70
97
 
71
98
  def remainingBillingInterval(self) -> float:
72
99
  """
73
- If the node has a launch time, this function returns a floating point value
74
- between 0 and 1.0 representing how far we are into the
75
- current billing cycle for the given instance. If the return value is .25, we are one
76
- quarter into the billing cycle, with three quarters remaining before we will be charged
77
- again for that instance.
100
+ Returns a floating point value between 0 and 1.0 representing how much
101
+ time is left in the current billing cycle for the given instance. If
102
+ the return value is .25, we are three quarters into the billing cycle,
103
+ with one quarters remaining before we will be charged again for that
104
+ instance.
78
105
 
79
106
  Assumes a billing cycle of one hour.
80
107
 
81
- :return: Float from 0 -> 1.0 representing percentage of pre-paid time left in cycle.
108
+ :return: Float from 1.0 -> 0.0 representing fraction of pre-paid time
109
+ remaining in cycle.
110
+
111
+ >>> node = Node("127.0.0.1", "127.0.0.1", "localhost",
112
+ ... datetime.datetime.utcnow(), None, False)
113
+ >>> node.remainingBillingInterval() >= 0
114
+ True
115
+ >>> node.remainingBillingInterval() <= 1.0
116
+ True
117
+ >>> node.remainingBillingInterval() > 0.5
118
+ True
119
+ >>> interval1 = node.remainingBillingInterval()
120
+ >>> time.sleep(1)
121
+ >>> interval2 = node.remainingBillingInterval()
122
+ >>> interval2 < interval1
123
+ True
124
+
125
+ >>> node = Node("127.0.0.1", "127.0.0.1", "localhost",
126
+ ... datetime.datetime.now(datetime.timezone.utc) -
127
+ ... datetime.timedelta(minutes=5), None, False)
128
+ >>> node.remainingBillingInterval() < 0.99
129
+ True
130
+ >>> node.remainingBillingInterval() > 0.9
131
+ True
132
+
82
133
  """
83
- if self.launchTime:
84
- now = datetime.datetime.utcnow()
85
- delta = now - self.launchTime
86
- return 1 - delta.total_seconds() / 3600.0 % 1.0
87
- else:
88
- return 1
134
+ now = datetime.datetime.now(datetime.timezone.utc)
135
+ delta = now - self.launchTime
136
+ return 1 - delta.total_seconds() / 3600.0 % 1.0
89
137
 
90
138
  def waitForNode(self, role: str, keyName: str = "core") -> None:
91
139
  self._waitForSSHPort()
toil/resource.py CHANGED
@@ -32,7 +32,6 @@ from zipfile import ZipFile
32
32
 
33
33
  from toil import inVirtualEnv
34
34
  from toil.lib.io import mkdtemp
35
- from toil.lib.iterables import concat
36
35
  from toil.lib.memoize import strict_bool
37
36
  from toil.lib.retry import ErrorCondition, retry
38
37
  from toil.version import exactPython
@@ -619,18 +618,9 @@ class ModuleDescriptor(
619
618
  initName = self._initModuleName(self.dirPath)
620
619
  if initName:
621
620
  raise ResourceException(
622
- "Toil does not support loading a user script from a package directory. You "
623
- "may want to remove %s from %s or invoke the user script as a module via "
624
- "'PYTHONPATH=\"%s\" %s -m %s.%s'."
625
- % tuple(
626
- concat(
627
- initName,
628
- self.dirPath,
629
- exactPython,
630
- os.path.split(self.dirPath),
631
- self.name,
632
- )
633
- )
621
+ f"Toil does not support loading a user script from a package directory. You "
622
+ f"may want to remove {initName} from {self.dirPath} or invoke the user script as a module via: "
623
+ f"PYTHONPATH='{self.dirPath}' {exactPython} -m {self.dirPath}.{self.name}"
634
624
  )
635
625
  return self.dirPath
636
626
 
toil/test/__init__.py CHANGED
@@ -49,7 +49,6 @@ from toil.lib.accelerators import (
49
49
  have_working_nvidia_smi,
50
50
  )
51
51
  from toil.lib.io import mkdtemp
52
- from toil.lib.iterables import concat
53
52
  from toil.lib.memoize import memoize
54
53
  from toil.lib.threading import ExceptionalThread, cpu_count
55
54
  from toil.version import distVersion
@@ -224,7 +223,7 @@ class ToilTest(unittest.TestCase):
224
223
 
225
224
  :return: The output of the process' stdout if capture=True was passed, None otherwise.
226
225
  """
227
- argl = list(concat(command, args))
226
+ argl = [command] + list(args)
228
227
  logger.info("Running %r", argl)
229
228
  capture = kwargs.pop("capture", False)
230
229
  _input = kwargs.pop("input", None)
@@ -395,6 +394,8 @@ def _aws_s3_avail() -> bool:
395
394
  boto3_credentials = session.get_credentials()
396
395
  except ImportError:
397
396
  return False
397
+ except ProxyConnectionError:
398
+ return False
398
399
  from toil.lib.aws import running_on_ec2
399
400
 
400
401
  if not (
@@ -440,7 +441,7 @@ def needs_aws_batch(test_item: MT) -> MT:
440
441
  test_item
441
442
  )
442
443
  test_item = needs_env_var(
443
- "TOIL_AWS_BATCH_JOB_ROLE_ARN", "an IAM role ARN that grants S3 and SDB access"
444
+ "TOIL_AWS_BATCH_JOB_ROLE_ARN", "an IAM role ARN that grants S3 access"
444
445
  )(test_item)
445
446
  try:
446
447
  from toil.lib.aws import get_current_aws_region
@@ -1249,19 +1250,16 @@ class ApplianceTestSupport(ToilTest):
1249
1250
  with self.lock:
1250
1251
  image = applianceSelf()
1251
1252
  # Omitting --rm, it's unreliable, see https://github.com/docker/docker/issues/16575
1252
- args = list(
1253
- concat(
1254
- "docker",
1255
- "run",
1256
- "--entrypoint=" + self._entryPoint(),
1257
- "--net=host",
1258
- "-i",
1259
- "--name=" + self.containerName,
1260
- ["--volume=%s:%s" % mount for mount in self.mounts.items()],
1261
- image,
1262
- self._containerCommand(),
1263
- )
1264
- )
1253
+ args = [
1254
+ "docker",
1255
+ "run",
1256
+ f"--entrypoint={self._entryPoint()}",
1257
+ "--net=host",
1258
+ "-i",
1259
+ f"--name={self.containerName}"] + \
1260
+ ["--volume=%s:%s" % mount for mount in self.mounts.items()] + \
1261
+ [image] + \
1262
+ self._containerCommand()
1265
1263
  logger.info("Running %r", args)
1266
1264
  self.popen = subprocess.Popen(args)
1267
1265
  self.start()