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.
- toil/batchSystems/abstractBatchSystem.py +13 -5
- toil/batchSystems/abstractGridEngineBatchSystem.py +17 -5
- toil/batchSystems/kubernetes.py +13 -2
- toil/batchSystems/mesos/batchSystem.py +33 -2
- toil/batchSystems/slurm.py +191 -16
- toil/cwl/cwltoil.py +17 -82
- toil/fileStores/__init__.py +1 -1
- toil/fileStores/abstractFileStore.py +5 -2
- toil/fileStores/cachingFileStore.py +1 -1
- toil/job.py +30 -14
- toil/jobStores/abstractJobStore.py +24 -19
- toil/jobStores/aws/jobStore.py +862 -1963
- toil/jobStores/aws/utils.py +24 -270
- toil/jobStores/googleJobStore.py +25 -9
- toil/jobStores/utils.py +0 -327
- toil/leader.py +27 -22
- toil/lib/aws/config.py +22 -0
- toil/lib/aws/s3.py +477 -9
- toil/lib/aws/utils.py +22 -33
- toil/lib/checksum.py +88 -0
- toil/lib/conversions.py +33 -31
- toil/lib/directory.py +217 -0
- toil/lib/ec2.py +97 -29
- toil/lib/exceptions.py +2 -1
- toil/lib/expando.py +2 -2
- toil/lib/generatedEC2Lists.py +73 -16
- toil/lib/io.py +33 -2
- toil/lib/memoize.py +21 -7
- toil/lib/pipes.py +385 -0
- toil/lib/retry.py +1 -1
- toil/lib/threading.py +1 -1
- toil/lib/web.py +4 -5
- toil/provisioners/__init__.py +5 -2
- toil/provisioners/aws/__init__.py +43 -36
- toil/provisioners/aws/awsProvisioner.py +22 -13
- toil/provisioners/node.py +60 -12
- toil/resource.py +3 -13
- toil/test/__init__.py +14 -16
- toil/test/batchSystems/test_slurm.py +103 -14
- toil/test/cwl/staging_cat.cwl +27 -0
- toil/test/cwl/staging_make_file.cwl +25 -0
- toil/test/cwl/staging_workflow.cwl +43 -0
- toil/test/cwl/zero_default.cwl +61 -0
- toil/test/docs/scripts/tutorial_staging.py +17 -8
- toil/test/jobStores/jobStoreTest.py +23 -133
- toil/test/lib/aws/test_iam.py +7 -7
- toil/test/lib/aws/test_s3.py +30 -33
- toil/test/lib/aws/test_utils.py +9 -9
- toil/test/provisioners/aws/awsProvisionerTest.py +59 -6
- toil/test/src/autoDeploymentTest.py +2 -3
- toil/test/src/fileStoreTest.py +89 -87
- toil/test/utils/ABCWorkflowDebug/ABC.txt +1 -0
- toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +4 -4
- toil/test/utils/toilKillTest.py +35 -28
- toil/test/wdl/md5sum/md5sum.json +1 -1
- toil/test/wdl/testfiles/gather.wdl +52 -0
- toil/test/wdl/wdltoil_test.py +120 -38
- toil/test/wdl/wdltoil_test_kubernetes.py +9 -0
- toil/utils/toilDebugFile.py +6 -3
- toil/utils/toilStats.py +17 -2
- toil/version.py +6 -6
- toil/wdl/wdltoil.py +1038 -549
- toil/worker.py +5 -2
- {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/METADATA +12 -12
- {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/RECORD +69 -61
- toil/lib/iterables.py +0 -112
- toil/test/docs/scripts/stagingExampleFiles/in.txt +0 -1
- {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/WHEEL +0 -0
- {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/entry_points.txt +0 -0
- {toil-9.0.0.dist-info → toil-9.1.1.dist-info}/licenses/LICENSE +0 -0
- {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["
|
|
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
|
-
>>>
|
|
127
|
-
|
|
128
|
-
FauxHistory(0.
|
|
129
|
-
FauxHistory(0.
|
|
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
|
|
158
|
+
if zone_history["AvailabilityZone"] == zone
|
|
156
159
|
]
|
|
157
160
|
if zone_histories:
|
|
158
|
-
price_deviation = stdev([history
|
|
159
|
-
recent_price = zone_histories[0]
|
|
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:
|
|
197
|
+
:param spot_bid: The proposed bid in dollars per hour.
|
|
195
198
|
|
|
196
|
-
:type spot_history:
|
|
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
|
-
>>>
|
|
201
|
-
>>>
|
|
202
|
-
|
|
203
|
-
FauxHistory( 0.
|
|
204
|
-
FauxHistory( 0.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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"
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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":
|
|
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":
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 ->
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
624
|
-
"'
|
|
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(
|
|
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
|
|
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 =
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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()
|