toil 7.0.0__py3-none-any.whl → 8.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 +121 -83
- toil/batchSystems/__init__.py +1 -0
- toil/batchSystems/abstractBatchSystem.py +137 -77
- toil/batchSystems/abstractGridEngineBatchSystem.py +211 -101
- toil/batchSystems/awsBatch.py +237 -128
- toil/batchSystems/cleanup_support.py +22 -16
- toil/batchSystems/contained_executor.py +30 -26
- toil/batchSystems/gridengine.py +85 -49
- toil/batchSystems/htcondor.py +164 -87
- toil/batchSystems/kubernetes.py +622 -386
- toil/batchSystems/local_support.py +17 -12
- toil/batchSystems/lsf.py +132 -79
- toil/batchSystems/lsfHelper.py +13 -11
- toil/batchSystems/mesos/__init__.py +41 -29
- toil/batchSystems/mesos/batchSystem.py +288 -149
- toil/batchSystems/mesos/executor.py +77 -49
- toil/batchSystems/mesos/test/__init__.py +31 -23
- toil/batchSystems/options.py +38 -29
- toil/batchSystems/registry.py +53 -19
- toil/batchSystems/singleMachine.py +293 -123
- toil/batchSystems/slurm.py +489 -137
- toil/batchSystems/torque.py +46 -32
- toil/bus.py +141 -73
- toil/common.py +630 -359
- toil/cwl/__init__.py +1 -1
- toil/cwl/cwltoil.py +1114 -532
- toil/cwl/utils.py +17 -22
- toil/deferred.py +62 -41
- toil/exceptions.py +5 -3
- toil/fileStores/__init__.py +5 -5
- toil/fileStores/abstractFileStore.py +88 -57
- toil/fileStores/cachingFileStore.py +711 -247
- toil/fileStores/nonCachingFileStore.py +113 -75
- toil/job.py +988 -315
- toil/jobStores/abstractJobStore.py +387 -243
- toil/jobStores/aws/jobStore.py +727 -403
- toil/jobStores/aws/utils.py +161 -109
- toil/jobStores/conftest.py +1 -0
- toil/jobStores/fileJobStore.py +289 -151
- toil/jobStores/googleJobStore.py +137 -70
- toil/jobStores/utils.py +36 -15
- toil/leader.py +614 -269
- toil/lib/accelerators.py +115 -18
- toil/lib/aws/__init__.py +55 -28
- toil/lib/aws/ami.py +122 -87
- toil/lib/aws/iam.py +284 -108
- toil/lib/aws/s3.py +31 -0
- toil/lib/aws/session.py +193 -58
- toil/lib/aws/utils.py +238 -218
- toil/lib/bioio.py +13 -5
- toil/lib/compatibility.py +11 -6
- toil/lib/conversions.py +83 -49
- toil/lib/docker.py +131 -103
- toil/lib/ec2.py +322 -209
- toil/lib/ec2nodes.py +174 -106
- toil/lib/encryption/_dummy.py +5 -3
- toil/lib/encryption/_nacl.py +10 -6
- toil/lib/encryption/conftest.py +1 -0
- toil/lib/exceptions.py +26 -7
- toil/lib/expando.py +4 -2
- toil/lib/ftp_utils.py +217 -0
- toil/lib/generatedEC2Lists.py +127 -19
- toil/lib/humanize.py +6 -2
- toil/lib/integration.py +341 -0
- toil/lib/io.py +99 -11
- toil/lib/iterables.py +4 -2
- toil/lib/memoize.py +12 -8
- toil/lib/misc.py +65 -18
- toil/lib/objects.py +2 -2
- toil/lib/resources.py +19 -7
- toil/lib/retry.py +115 -77
- toil/lib/threading.py +282 -80
- toil/lib/throttle.py +15 -14
- toil/options/common.py +834 -401
- toil/options/cwl.py +175 -90
- toil/options/runner.py +50 -0
- toil/options/wdl.py +70 -19
- toil/provisioners/__init__.py +111 -46
- toil/provisioners/abstractProvisioner.py +322 -157
- toil/provisioners/aws/__init__.py +62 -30
- toil/provisioners/aws/awsProvisioner.py +980 -627
- toil/provisioners/clusterScaler.py +541 -279
- toil/provisioners/gceProvisioner.py +282 -179
- toil/provisioners/node.py +147 -79
- toil/realtimeLogger.py +34 -22
- toil/resource.py +137 -75
- toil/server/app.py +127 -61
- toil/server/celery_app.py +3 -1
- toil/server/cli/wes_cwl_runner.py +82 -53
- toil/server/utils.py +54 -28
- toil/server/wes/abstract_backend.py +64 -26
- toil/server/wes/amazon_wes_utils.py +21 -15
- toil/server/wes/tasks.py +121 -63
- toil/server/wes/toil_backend.py +142 -107
- toil/server/wsgi_app.py +4 -3
- toil/serviceManager.py +58 -22
- toil/statsAndLogging.py +148 -64
- toil/test/__init__.py +263 -179
- toil/test/batchSystems/batchSystemTest.py +438 -195
- toil/test/batchSystems/batch_system_plugin_test.py +18 -7
- toil/test/batchSystems/test_gridengine.py +173 -0
- toil/test/batchSystems/test_lsf_helper.py +67 -58
- toil/test/batchSystems/test_slurm.py +93 -47
- toil/test/cactus/test_cactus_integration.py +20 -22
- toil/test/cwl/cwlTest.py +271 -71
- toil/test/cwl/measure_default_memory.cwl +12 -0
- toil/test/cwl/not_run_required_input.cwl +29 -0
- toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
- toil/test/docs/scriptsTest.py +60 -34
- toil/test/jobStores/jobStoreTest.py +412 -235
- toil/test/lib/aws/test_iam.py +116 -48
- toil/test/lib/aws/test_s3.py +16 -9
- toil/test/lib/aws/test_utils.py +5 -6
- toil/test/lib/dockerTest.py +118 -141
- toil/test/lib/test_conversions.py +113 -115
- toil/test/lib/test_ec2.py +57 -49
- toil/test/lib/test_integration.py +104 -0
- toil/test/lib/test_misc.py +12 -5
- toil/test/mesos/MesosDataStructuresTest.py +23 -10
- toil/test/mesos/helloWorld.py +7 -6
- toil/test/mesos/stress.py +25 -20
- toil/test/options/options.py +7 -2
- toil/test/provisioners/aws/awsProvisionerTest.py +293 -140
- toil/test/provisioners/clusterScalerTest.py +440 -250
- toil/test/provisioners/clusterTest.py +81 -42
- toil/test/provisioners/gceProvisionerTest.py +174 -100
- toil/test/provisioners/provisionerTest.py +25 -13
- toil/test/provisioners/restartScript.py +5 -4
- toil/test/server/serverTest.py +188 -141
- toil/test/sort/restart_sort.py +137 -68
- toil/test/sort/sort.py +134 -66
- toil/test/sort/sortTest.py +91 -49
- toil/test/src/autoDeploymentTest.py +140 -100
- toil/test/src/busTest.py +20 -18
- toil/test/src/checkpointTest.py +8 -2
- toil/test/src/deferredFunctionTest.py +49 -35
- toil/test/src/dockerCheckTest.py +33 -26
- toil/test/src/environmentTest.py +20 -10
- toil/test/src/fileStoreTest.py +538 -271
- toil/test/src/helloWorldTest.py +7 -4
- toil/test/src/importExportFileTest.py +61 -31
- toil/test/src/jobDescriptionTest.py +32 -17
- toil/test/src/jobEncapsulationTest.py +2 -0
- toil/test/src/jobFileStoreTest.py +74 -50
- toil/test/src/jobServiceTest.py +187 -73
- toil/test/src/jobTest.py +120 -70
- toil/test/src/miscTests.py +19 -18
- toil/test/src/promisedRequirementTest.py +82 -36
- toil/test/src/promisesTest.py +7 -6
- toil/test/src/realtimeLoggerTest.py +6 -6
- toil/test/src/regularLogTest.py +71 -37
- toil/test/src/resourceTest.py +80 -49
- toil/test/src/restartDAGTest.py +36 -22
- toil/test/src/resumabilityTest.py +9 -2
- toil/test/src/retainTempDirTest.py +45 -14
- toil/test/src/systemTest.py +12 -8
- toil/test/src/threadingTest.py +44 -25
- toil/test/src/toilContextManagerTest.py +10 -7
- toil/test/src/userDefinedJobArgTypeTest.py +8 -5
- toil/test/src/workerTest.py +33 -16
- toil/test/utils/toilDebugTest.py +70 -58
- toil/test/utils/toilKillTest.py +4 -5
- toil/test/utils/utilsTest.py +239 -102
- toil/test/wdl/wdltoil_test.py +789 -148
- toil/test/wdl/wdltoil_test_kubernetes.py +37 -23
- toil/toilState.py +52 -26
- toil/utils/toilConfig.py +13 -4
- toil/utils/toilDebugFile.py +44 -27
- toil/utils/toilDebugJob.py +85 -25
- toil/utils/toilDestroyCluster.py +11 -6
- toil/utils/toilKill.py +8 -3
- toil/utils/toilLaunchCluster.py +251 -145
- toil/utils/toilMain.py +37 -16
- toil/utils/toilRsyncCluster.py +27 -14
- toil/utils/toilSshCluster.py +45 -22
- toil/utils/toilStats.py +75 -36
- toil/utils/toilStatus.py +226 -119
- toil/utils/toilUpdateEC2Instances.py +3 -1
- toil/version.py +11 -11
- toil/wdl/utils.py +5 -5
- toil/wdl/wdltoil.py +3513 -1052
- toil/worker.py +269 -128
- toil-8.0.0.dist-info/METADATA +173 -0
- toil-8.0.0.dist-info/RECORD +253 -0
- {toil-7.0.0.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
- toil-7.0.0.dist-info/METADATA +0 -158
- toil-7.0.0.dist-info/RECORD +0 -244
- {toil-7.0.0.dist-info → toil-8.0.0.dist-info}/LICENSE +0 -0
- {toil-7.0.0.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
- {toil-7.0.0.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/server/app.py
CHANGED
|
@@ -29,49 +29,103 @@ logger = logging.getLogger(__name__)
|
|
|
29
29
|
def parser_with_server_options() -> argparse.ArgumentParser:
|
|
30
30
|
parser = ArgumentParser(description="Toil server mode.")
|
|
31
31
|
|
|
32
|
-
parser.add_argument(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--debug", action="store_true", default=False, help="Enable debug mode."
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--bypass_celery",
|
|
37
|
+
action="store_true",
|
|
38
|
+
default=False,
|
|
39
|
+
help="Skip sending workflows to Celery and just run them under the "
|
|
40
|
+
"server. For testing.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--host",
|
|
44
|
+
type=str,
|
|
45
|
+
default="127.0.0.1",
|
|
46
|
+
help="The host interface that the Toil server binds on. (default: '127.0.0.1').",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--port",
|
|
50
|
+
type=int,
|
|
51
|
+
default=8080,
|
|
52
|
+
help="The port that the Toil server listens on. (default: 8080).",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--swagger_ui",
|
|
56
|
+
action="store_true",
|
|
57
|
+
default=False,
|
|
58
|
+
help="If True, the swagger UI will be enabled and hosted on the "
|
|
59
|
+
"`{api_base_path}/ui` endpoint. (default: False)",
|
|
60
|
+
)
|
|
44
61
|
# CORS
|
|
45
|
-
parser.add_argument(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--cors",
|
|
64
|
+
action="store_true",
|
|
65
|
+
default=False,
|
|
66
|
+
help="Enable Cross Origin Resource Sharing (CORS). This should only be turned on "
|
|
67
|
+
"if the server is intended to be used by a website or domain.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--cors_origins",
|
|
71
|
+
type=str,
|
|
72
|
+
default="*",
|
|
73
|
+
help="Ignored if --cors is False. This sets the allowed origins for CORS. "
|
|
74
|
+
"For details about CORS and its security risks, see: "
|
|
75
|
+
"https://w3id.org/ga4gh/product-approval-support/cors. (default: '*')",
|
|
76
|
+
)
|
|
52
77
|
# production only
|
|
53
|
-
parser.add_argument(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
parser.add_argument(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
parser.add_argument(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"-w",
|
|
80
|
+
"--workers",
|
|
81
|
+
dest="workers",
|
|
82
|
+
type=int,
|
|
83
|
+
default=2,
|
|
84
|
+
help="Ignored if --debug is True. The number of worker processes launched by the "
|
|
85
|
+
"WSGI server. (default: 2).",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--work_dir",
|
|
90
|
+
type=str,
|
|
91
|
+
default=os.path.join(os.getcwd(), "workflows"),
|
|
92
|
+
help="The directory where workflows should be stored. This directory should be "
|
|
93
|
+
"empty or only contain previous workflows. (default: './workflows').",
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--state_store",
|
|
97
|
+
type=str,
|
|
98
|
+
default=None,
|
|
99
|
+
help="The local path or S3 URL where workflow state metadata should be stored. "
|
|
100
|
+
"(default: in --work_dir)",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--opt",
|
|
104
|
+
"-o",
|
|
105
|
+
type=str,
|
|
106
|
+
action="append",
|
|
107
|
+
default=[],
|
|
108
|
+
help="Specify the default parameters to be sent to the workflow engine for each "
|
|
109
|
+
"run. Options taking arguments must use = syntax. Accepts multiple values.\n"
|
|
110
|
+
"Example: '--opt=--logLevel=CRITICAL --opt=--workDir=/tmp'.",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--dest_bucket_base",
|
|
114
|
+
type=str,
|
|
115
|
+
default=None,
|
|
116
|
+
help="Direct CWL workflows to save output files to dynamically generated "
|
|
117
|
+
"unique paths under the given URL. Supports AWS S3.",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--wes_dialect",
|
|
121
|
+
type=str,
|
|
122
|
+
default="standard",
|
|
123
|
+
choices=["standard", "agc"],
|
|
124
|
+
help="Restrict WES responses to a dialect compatible with clients that do not fully "
|
|
125
|
+
"implement the WES standard. (default: 'standard')",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument("--version", action="version", version=version)
|
|
75
129
|
return parser
|
|
76
130
|
|
|
77
131
|
|
|
@@ -79,34 +133,43 @@ def create_app(args: argparse.Namespace) -> "connexion.FlaskApp":
|
|
|
79
133
|
"""
|
|
80
134
|
Create a "connexion.FlaskApp" instance with Toil server configurations.
|
|
81
135
|
"""
|
|
82
|
-
flask_app = connexion.FlaskApp(
|
|
83
|
-
|
|
84
|
-
|
|
136
|
+
flask_app = connexion.FlaskApp(
|
|
137
|
+
__name__, specification_dir="api_spec/", options={"swagger_ui": args.swagger_ui}
|
|
138
|
+
)
|
|
85
139
|
|
|
86
|
-
flask_app.app.config[
|
|
140
|
+
flask_app.app.config["JSON_SORT_KEYS"] = False
|
|
87
141
|
|
|
88
142
|
if args.cors:
|
|
89
143
|
# enable cross origin resource sharing
|
|
90
144
|
from flask_cors import CORS
|
|
145
|
+
|
|
91
146
|
CORS(flask_app.app, resources={r"/ga4gh/*": {"origins": args.cors_origins}})
|
|
92
147
|
|
|
93
148
|
# add workflow execution service (WES) API endpoints
|
|
94
|
-
backend = ToilBackend(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
backend = ToilBackend(
|
|
150
|
+
work_dir=args.work_dir,
|
|
151
|
+
state_store=args.state_store,
|
|
152
|
+
options=args.opt,
|
|
153
|
+
dest_bucket_base=args.dest_bucket_base,
|
|
154
|
+
bypass_celery=args.bypass_celery,
|
|
155
|
+
wes_dialect=args.wes_dialect,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
flask_app.add_api(
|
|
159
|
+
"workflow_execution_service.swagger.yaml",
|
|
160
|
+
resolver=connexion.Resolver(backend.resolve_operation_id),
|
|
161
|
+
) # noqa
|
|
103
162
|
|
|
104
163
|
# add custom endpoints
|
|
105
164
|
if isinstance(backend, ToilBackend):
|
|
106
165
|
# We extend the WES API to allow presenting log data
|
|
107
166
|
base_url = "/toil/wes/v1"
|
|
108
|
-
flask_app.app.add_url_rule(
|
|
109
|
-
|
|
167
|
+
flask_app.app.add_url_rule(
|
|
168
|
+
f"{base_url}/logs/<run_id>/stdout", view_func=backend.get_stdout
|
|
169
|
+
)
|
|
170
|
+
flask_app.app.add_url_rule(
|
|
171
|
+
f"{base_url}/logs/<run_id>/stderr", view_func=backend.get_stderr
|
|
172
|
+
)
|
|
110
173
|
# To be a well-behaved AGC engine we can implement the default status check endpoint
|
|
111
174
|
flask_app.app.add_url_rule("/engine/v1/status", view_func=backend.get_health)
|
|
112
175
|
# And we can provide lost humans some information on what they are looking at
|
|
@@ -116,7 +179,7 @@ def create_app(args: argparse.Namespace) -> "connexion.FlaskApp":
|
|
|
116
179
|
|
|
117
180
|
|
|
118
181
|
def start_server(args: argparse.Namespace) -> None:
|
|
119
|
-
"""
|
|
182
|
+
"""Start a Toil server."""
|
|
120
183
|
|
|
121
184
|
# Explain a bit about who and where we are
|
|
122
185
|
logger.info("Toil WES server version %s starting...", version)
|
|
@@ -137,7 +200,10 @@ def start_server(args: argparse.Namespace) -> None:
|
|
|
137
200
|
flask_app.run(host=host, port=port)
|
|
138
201
|
else:
|
|
139
202
|
# start a production WSGI server
|
|
140
|
-
run_app(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
203
|
+
run_app(
|
|
204
|
+
flask_app.app,
|
|
205
|
+
options={
|
|
206
|
+
"bind": f"{host}:{port}",
|
|
207
|
+
"workers": args.workers,
|
|
208
|
+
},
|
|
209
|
+
)
|
toil/server/celery_app.py
CHANGED
|
@@ -11,7 +11,9 @@ Also the entrypoint for starting celery workers.
|
|
|
11
11
|
|
|
12
12
|
def create_celery_app() -> Celery:
|
|
13
13
|
""" """
|
|
14
|
-
broker = os.environ.get(
|
|
14
|
+
broker = os.environ.get(
|
|
15
|
+
"TOIL_WES_BROKER_URL", "amqp://guest:guest@localhost:5672//"
|
|
16
|
+
)
|
|
15
17
|
app = Celery("toil_wes", broker=broker)
|
|
16
18
|
|
|
17
19
|
# Celery configurations
|
|
@@ -5,8 +5,9 @@ import subprocess
|
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
7
|
from base64 import b64encode
|
|
8
|
+
from collections.abc import Iterable
|
|
8
9
|
from io import BytesIO
|
|
9
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Optional, cast
|
|
10
11
|
from urllib.parse import urldefrag, urljoin, urlparse
|
|
11
12
|
|
|
12
13
|
import requests
|
|
@@ -56,7 +57,7 @@ cwltest --verbose \
|
|
|
56
57
|
logger = logging.getLogger(__name__)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def generate_attachment_path_names(paths:
|
|
60
|
+
def generate_attachment_path_names(paths: list[str]) -> tuple[str, list[str]]:
|
|
60
61
|
"""
|
|
61
62
|
Take in a list of path names and return a list of names with the common path
|
|
62
63
|
name stripped out, while preserving the input order. This guarantees that
|
|
@@ -105,7 +106,8 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
105
106
|
|
|
106
107
|
TODO: Propose a PR in wes-service to include workflow_engine_params.
|
|
107
108
|
"""
|
|
108
|
-
|
|
109
|
+
|
|
110
|
+
def __init__(self, endpoint: str, auth: Optional[tuple[str, str]] = None) -> None:
|
|
109
111
|
"""
|
|
110
112
|
:param endpoint: The http(s) URL of the WES server. Must include the
|
|
111
113
|
protocol.
|
|
@@ -113,12 +115,21 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
113
115
|
request to the WES server.
|
|
114
116
|
"""
|
|
115
117
|
proto, host = endpoint.split("://")
|
|
116
|
-
super().__init__(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
super().__init__(
|
|
119
|
+
{
|
|
120
|
+
# TODO: use the auth argument in requests.post so we don't need to encode it ourselves
|
|
121
|
+
"auth": (
|
|
122
|
+
{
|
|
123
|
+
"Authorization": "Basic "
|
|
124
|
+
+ b64encode(f"{auth[0]}:{auth[1]}".encode()).decode("utf-8")
|
|
125
|
+
}
|
|
126
|
+
if auth
|
|
127
|
+
else {}
|
|
128
|
+
),
|
|
129
|
+
"proto": proto,
|
|
130
|
+
"host": host,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
122
133
|
|
|
123
134
|
def get_version(self, extension: str, workflow_file: str) -> str:
|
|
124
135
|
"""Determines the version of a .py, .wdl, or .cwl file."""
|
|
@@ -140,7 +151,7 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
140
151
|
else:
|
|
141
152
|
raise RuntimeError(f"Invalid workflow extension: {extension}.")
|
|
142
153
|
|
|
143
|
-
def parse_params(self, workflow_params_file: str) ->
|
|
154
|
+
def parse_params(self, workflow_params_file: str) -> dict[str, Any]:
|
|
144
155
|
"""
|
|
145
156
|
Parse the CWL input file into a dictionary to be attached to the body of
|
|
146
157
|
the WES run request.
|
|
@@ -155,9 +166,11 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
155
166
|
workflow_params: Any
|
|
156
167
|
workflow_params, _ = loader.resolve_ref(workflow_params_file, checklinks=False)
|
|
157
168
|
|
|
158
|
-
return cast(
|
|
169
|
+
return cast(dict[str, Any], workflow_params)
|
|
159
170
|
|
|
160
|
-
def modify_param_paths(
|
|
171
|
+
def modify_param_paths(
|
|
172
|
+
self, base_dir: str, workflow_params: dict[str, Any]
|
|
173
|
+
) -> None:
|
|
161
174
|
"""
|
|
162
175
|
Modify the file paths in the input workflow parameters to be relative
|
|
163
176
|
to base_dir.
|
|
@@ -168,7 +181,7 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
168
181
|
:param workflow_params: A dict containing the workflow parameters.
|
|
169
182
|
"""
|
|
170
183
|
|
|
171
|
-
def replace(field: str, file_obj:
|
|
184
|
+
def replace(field: str, file_obj: dict[str, str]) -> None:
|
|
172
185
|
"""
|
|
173
186
|
Given a file object with the "location" or "path" field, replace it
|
|
174
187
|
to be relative to base_dir.
|
|
@@ -192,15 +205,16 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
192
205
|
replace_paths(file.values())
|
|
193
206
|
elif isinstance(file, list):
|
|
194
207
|
replace_paths(file)
|
|
208
|
+
|
|
195
209
|
replace_paths(workflow_params.values())
|
|
196
210
|
|
|
197
211
|
def build_wes_request(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
) ->
|
|
212
|
+
self,
|
|
213
|
+
workflow_file: str,
|
|
214
|
+
workflow_params_file: Optional[str],
|
|
215
|
+
attachments: Optional[list[str]],
|
|
216
|
+
workflow_engine_parameters: Optional[list[str]] = None,
|
|
217
|
+
) -> tuple[dict[str, str], Iterable[tuple[str, tuple[str, BytesIO]]]]:
|
|
204
218
|
"""
|
|
205
219
|
Build the workflow run request to submit to WES.
|
|
206
220
|
|
|
@@ -233,21 +247,21 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
233
247
|
|
|
234
248
|
workflow_type = wf_url.lower().split(".")[-1] # Grab the file extension
|
|
235
249
|
workflow_type_version = self.get_version(workflow_type, wf_url)
|
|
236
|
-
data:
|
|
250
|
+
data: dict[str, str] = {
|
|
237
251
|
"workflow_url": workflow_file,
|
|
238
252
|
"workflow_params": "", # to be set after attachments are processed
|
|
239
253
|
"workflow_type": workflow_type,
|
|
240
|
-
"workflow_type_version": workflow_type_version
|
|
254
|
+
"workflow_type_version": workflow_type_version,
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
# Convert engine arguments into a JSON object
|
|
244
258
|
if workflow_engine_parameters:
|
|
245
259
|
params = {}
|
|
246
260
|
for param in workflow_engine_parameters:
|
|
247
|
-
if
|
|
261
|
+
if "=" not in param: # flags like "--logDebug"
|
|
248
262
|
k, v = param, None
|
|
249
263
|
else:
|
|
250
|
-
k, v = param.split(
|
|
264
|
+
k, v = param.split("=", 1)
|
|
251
265
|
params[k] = v
|
|
252
266
|
data["workflow_engine_parameters"] = json.dumps(params)
|
|
253
267
|
|
|
@@ -278,12 +292,12 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
278
292
|
return data, [("workflow_attachment", val) for val in workflow_attachments]
|
|
279
293
|
|
|
280
294
|
def run_with_engine_options(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
) ->
|
|
295
|
+
self,
|
|
296
|
+
workflow_file: str,
|
|
297
|
+
workflow_params_file: Optional[str],
|
|
298
|
+
attachments: Optional[list[str]],
|
|
299
|
+
workflow_engine_parameters: Optional[list[str]],
|
|
300
|
+
) -> dict[str, Any]:
|
|
287
301
|
"""
|
|
288
302
|
Composes and sends a post request that signals the WES server to run a
|
|
289
303
|
workflow.
|
|
@@ -297,10 +311,9 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
297
311
|
|
|
298
312
|
:return: The body of the post result as a dictionary.
|
|
299
313
|
"""
|
|
300
|
-
data, files = self.build_wes_request(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
workflow_engine_parameters)
|
|
314
|
+
data, files = self.build_wes_request(
|
|
315
|
+
workflow_file, workflow_params_file, attachments, workflow_engine_parameters
|
|
316
|
+
)
|
|
304
317
|
post_result = requests.post(
|
|
305
318
|
urljoin(f"{self.proto}://{self.host}", "/ga4gh/wes/v1/runs"),
|
|
306
319
|
data=data,
|
|
@@ -308,10 +321,10 @@ class WESClientWithWorkflowEngineParameters(WESClient): # type: ignore
|
|
|
308
321
|
headers=self.auth,
|
|
309
322
|
)
|
|
310
323
|
|
|
311
|
-
return cast(
|
|
324
|
+
return cast(dict[str, Any], wes_response(post_result))
|
|
312
325
|
|
|
313
326
|
|
|
314
|
-
def get_deps_from_cwltool(cwl_file: str, input_file: Optional[str] = None) ->
|
|
327
|
+
def get_deps_from_cwltool(cwl_file: str, input_file: Optional[str] = None) -> list[str]:
|
|
315
328
|
"""
|
|
316
329
|
Return a list of dependencies of the given workflow from cwltool.
|
|
317
330
|
|
|
@@ -320,19 +333,21 @@ def get_deps_from_cwltool(cwl_file: str, input_file: Optional[str] = None) -> Li
|
|
|
320
333
|
this returns the dependencies from the input file.
|
|
321
334
|
"""
|
|
322
335
|
|
|
323
|
-
option =
|
|
336
|
+
option = "--print-input-deps" if input_file else "--print-deps"
|
|
324
337
|
|
|
325
|
-
args = [
|
|
338
|
+
args = ["cwltool", option, "--relative-deps", "cwd", cwl_file]
|
|
326
339
|
if input_file:
|
|
327
340
|
args.append(input_file)
|
|
328
341
|
|
|
329
|
-
p = subprocess.run(
|
|
342
|
+
p = subprocess.run(
|
|
343
|
+
args=args, check=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
|
|
344
|
+
)
|
|
330
345
|
|
|
331
346
|
result = p.stdout.decode()
|
|
332
347
|
if not result:
|
|
333
348
|
return []
|
|
334
349
|
|
|
335
|
-
json_result:
|
|
350
|
+
json_result: dict[str, Any] = json.loads(result)
|
|
336
351
|
deps = []
|
|
337
352
|
|
|
338
353
|
def get_deps(obj: Any) -> None:
|
|
@@ -368,10 +383,12 @@ def get_deps_from_cwltool(cwl_file: str, input_file: Optional[str] = None) -> Li
|
|
|
368
383
|
return deps
|
|
369
384
|
|
|
370
385
|
|
|
371
|
-
def submit_run(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
386
|
+
def submit_run(
|
|
387
|
+
client: WESClientWithWorkflowEngineParameters,
|
|
388
|
+
cwl_file: str,
|
|
389
|
+
input_file: Optional[str] = None,
|
|
390
|
+
engine_options: Optional[list[str]] = None,
|
|
391
|
+
) -> str:
|
|
375
392
|
"""
|
|
376
393
|
Given a CWL file, its input files, and an optional list of engine options,
|
|
377
394
|
submit the CWL workflow to the WES server via the WES client.
|
|
@@ -391,23 +408,32 @@ def submit_run(client: WESClientWithWorkflowEngineParameters,
|
|
|
391
408
|
if input_file:
|
|
392
409
|
attachments.extend(get_deps_from_cwltool(cwl_file, input_file))
|
|
393
410
|
|
|
394
|
-
run_result:
|
|
411
|
+
run_result: dict[str, Any] = client.run_with_engine_options(
|
|
395
412
|
cwl_file,
|
|
396
413
|
input_file,
|
|
397
414
|
attachments=attachments,
|
|
398
|
-
workflow_engine_parameters=engine_options
|
|
415
|
+
workflow_engine_parameters=engine_options,
|
|
416
|
+
)
|
|
399
417
|
return str(run_result["run_id"])
|
|
400
418
|
|
|
401
419
|
|
|
402
420
|
def poll_run(client: WESClientWithWorkflowEngineParameters, run_id: str) -> bool:
|
|
403
|
-
"""
|
|
421
|
+
"""Return True if the given workflow run is in a finished state."""
|
|
404
422
|
status_result = client.get_run_status(run_id)
|
|
405
423
|
state = status_result.get("state")
|
|
406
424
|
|
|
407
|
-
return state in (
|
|
425
|
+
return state in (
|
|
426
|
+
"COMPLETE",
|
|
427
|
+
"CANCELING",
|
|
428
|
+
"CANCELED",
|
|
429
|
+
"EXECUTOR_ERROR",
|
|
430
|
+
"SYSTEM_ERROR",
|
|
431
|
+
)
|
|
408
432
|
|
|
409
433
|
|
|
410
|
-
def print_logs_and_exit(
|
|
434
|
+
def print_logs_and_exit(
|
|
435
|
+
client: WESClientWithWorkflowEngineParameters, run_id: str
|
|
436
|
+
) -> None:
|
|
411
437
|
"""
|
|
412
438
|
Fetch the workflow logs from the WES server, print the results, then exit
|
|
413
439
|
the program with the same exit code as the workflow run.
|
|
@@ -431,9 +457,11 @@ def main() -> None:
|
|
|
431
457
|
parser.add_argument("cwl_file", type=str)
|
|
432
458
|
parser.add_argument("input_file", type=str, nargs="?", default=None)
|
|
433
459
|
# arguments used by the WES runner
|
|
434
|
-
parser.add_argument(
|
|
435
|
-
|
|
436
|
-
|
|
460
|
+
parser.add_argument(
|
|
461
|
+
"--wes_endpoint",
|
|
462
|
+
default=os.environ.get("TOIL_WES_ENDPOINT", "http://localhost:8080"),
|
|
463
|
+
help="The http(s) URL of the WES server. (default: %(default)s)",
|
|
464
|
+
)
|
|
437
465
|
# the rest of the arguments are passed as engine options to the WES server
|
|
438
466
|
options, rest = parser.parse_known_args()
|
|
439
467
|
|
|
@@ -449,7 +477,8 @@ def main() -> None:
|
|
|
449
477
|
|
|
450
478
|
client = WESClientWithWorkflowEngineParameters(
|
|
451
479
|
endpoint=endpoint,
|
|
452
|
-
auth=(wes_user, wes_password) if wes_user and wes_password else None
|
|
480
|
+
auth=(wes_user, wes_password) if wes_user and wes_password else None,
|
|
481
|
+
)
|
|
453
482
|
|
|
454
483
|
run_id = submit_run(client, cwl_file, input_file, engine_options=rest)
|
|
455
484
|
assert run_id
|
|
@@ -462,5 +491,5 @@ def main() -> None:
|
|
|
462
491
|
print_logs_and_exit(client, run_id)
|
|
463
492
|
|
|
464
493
|
|
|
465
|
-
if __name__ ==
|
|
494
|
+
if __name__ == "__main__":
|
|
466
495
|
main()
|