Skip to content

Commit 7d77a6c

Browse files
achave11-ucscdavid4096
authored andcommitted
Cwltool client fixes (#36)
* Added support for multi part upload, fixed test to account for changes, included new test for checking multipart upload * Adding development instructions to the readme * Fix multipart upload for cwl_runner Muted workflow_log for service. Arvados assumptions are being made yielding error logs when running cwl-runner. Minor flake8 changes in the whole infrastructure. * Flake8 compliance with fixes. Https request for tool definition in tool descriptor. Unable to provision local files to temp dir. * Nit changes.
1 parent 968b558 commit 7d77a6c

File tree

5 files changed

+81
-21
lines changed

5 files changed

+81
-21
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,12 @@ $ export WES_API_PROTO=http
9595

9696
Then, when you call `wes-client` these defaults will be used in place of the
9797
flags, `--host`, `--auth`, and `proto` respectively.
98+
99+
## Development
100+
If you would like to develop against `workflow-service` make sure you pass the provided test and it is flake8 compliant
101+
#### Run test
102+
From path `workflow-service` run
103+
104+
```
105+
$ pytest && flake8
106+
```

test/test_integration.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from __future__ import absolute_import
2+
3+
import json
24
import unittest
35
import time
46
import os
@@ -10,6 +12,7 @@
1012

1113
class IntegrationTest(unittest.TestCase):
1214
"""A baseclass that's inherited for use with different cwl backends."""
15+
1316
def setUp(self):
1417
"""Start a (local) wes-service server to make requests against."""
1518
raise NotImplementedError
@@ -30,28 +33,51 @@ def tearDown(self):
3033
def test_dockstore_md5sum(self):
3134
"""Fetch the md5sum cwl from dockstore, run it on the wes-service server, and check for the correct output."""
3235
cwl_dockstore_url = 'https://dockstore.org:8443/api/ga4gh/v2/tools/quay.io%2Fbriandoconnor%2Fdockstore-tool-md5sum/versions/master/plain-CWL/descriptor/%2FDockstore.cwl'
33-
output_filepath = run_md5sum(cwl_input=cwl_dockstore_url)
36+
output_filepath, _ = run_md5sum(cwl_input=cwl_dockstore_url)
3437

3538
self.assertTrue(check_for_file(output_filepath), 'Output file was not found: ' + str(output_filepath))
3639
shutil.rmtree('workflows')
3740

3841
def test_local_md5sum(self):
3942
"""Pass a local md5sum cwl to the wes-service server, and check for the correct output."""
4043
cwl_local_path = os.path.abspath('testdata/md5sum.cwl')
41-
output_filepath = run_md5sum(cwl_input='file://' + cwl_local_path)
44+
output_filepath, _ = run_md5sum(cwl_input='file://' + cwl_local_path)
4245

4346
self.assertTrue(check_for_file(output_filepath), 'Output file was not found: ' + str(output_filepath))
4447
shutil.rmtree('workflows')
4548

49+
def test_multipart_upload(self):
50+
"""Pass a local md5sum cwl to the wes-service server, and check for uploaded file in service."""
51+
cwl_local_path = os.path.abspath('testdata/md5sum.cwl')
52+
_, run_id = run_md5sum(cwl_input='file://' + cwl_local_path)
53+
54+
get_response = get_log_request(run_id)["request"]
55+
56+
self.assertTrue(check_for_file(get_response["workflow_url"][7:]), 'Output file was not found: '
57+
+ get_response["workflow_url"][:7])
58+
shutil.rmtree('workflows')
59+
4660

4761
def run_md5sum(cwl_input):
4862
"""Pass a local md5sum cwl to the wes-service server, and return the path of the output file that was created."""
4963
endpoint = 'http://localhost:8080/ga4gh/wes/v1/workflows'
50-
params = {'output_file': {'path': '/tmp/md5sum.txt', 'class': 'File'}, 'input_file': {'path': '../../testdata/md5sum.input', 'class': 'File'}}
51-
body = {'workflow_url': cwl_input, 'workflow_params': params, 'workflow_type': 'CWL', 'workflow_type_version': 'v1.0'}
52-
response = requests.post(endpoint, json=body).json()
64+
params = {'output_file': {'path': '/tmp/md5sum.txt', 'class': 'File'},
65+
'input_file': {'path': '../../testdata/md5sum.input', 'class': 'File'}}
66+
67+
parts = [("workflow_params", json.dumps(params)), ("workflow_type", "CWL"), ("workflow_type_version", "v1.0")]
68+
if cwl_input.startswith("file://"):
69+
parts.append(("workflow_descriptor", ("md5sum.cwl", open(cwl_input[7:], "rb"))))
70+
parts.append(("workflow_url", os.path.basename(cwl_input[7:])))
71+
else:
72+
parts.append(("workflow_url", cwl_input))
73+
response = requests.post(endpoint, files=parts).json()
5374
output_dir = os.path.abspath(os.path.join('workflows', response['workflow_id'], 'outdir'))
54-
return os.path.join(output_dir, 'md5sum.txt')
75+
return os.path.join(output_dir, 'md5sum.txt'), response['workflow_id']
76+
77+
78+
def get_log_request(run_id):
79+
endpoint = 'http://localhost:8080/ga4gh/wes/v1/workflows/{}'.format(run_id)
80+
return requests.get(endpoint).json()
5581

5682

5783
def get_server_pids():
@@ -77,18 +103,21 @@ def check_for_file(filepath, seconds=20):
77103

78104
class CwltoolTest(IntegrationTest):
79105
"""Test using cwltool."""
106+
80107
def setUp(self):
81108
"""
82109
Start a (local) wes-service server to make requests against.
83110
Use cwltool as the wes-service server 'backend'.
84111
"""
85-
self.wes_server_process = subprocess.Popen('python {}'.format(os.path.abspath('wes_service/wes_service_main.py')),
86-
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
112+
self.wes_server_process = subprocess.Popen(
113+
'python {}'.format(os.path.abspath('wes_service/wes_service_main.py')),
114+
shell=True)
87115
time.sleep(5)
88116

89117

90118
class ToilTest(IntegrationTest):
91119
"""Test using Toil."""
120+
92121
def setUp(self):
93122
"""
94123
Start a (local) wes-service server to make requests against.
@@ -97,13 +126,12 @@ def setUp(self):
97126
self.wes_server_process = subprocess.Popen('python {} '
98127
'--opt runner=cwltoil --opt extra=--logLevel=CRITICAL'
99128
''.format(os.path.abspath('wes_service/wes_service_main.py')),
100-
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
129+
shell=True)
101130
time.sleep(5)
102131

103132

104133
# Prevent pytest/unittest's discovery from attempting to discover the base test class.
105134
del IntegrationTest
106135

107-
108136
if __name__ == '__main__':
109137
unittest.main() # run all tests

testdata/md5sum.cwl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ outputs:
1111

1212
steps:
1313
md5sum:
14-
run: dockstore-tool-md5sum.cwl
14+
run: https://raw.githubusercontent.com/common-workflow-language/workflow-service/master/testdata/dockstore-tool-md5sum.cwl
1515
in:
1616
input_file: input_file
1717
out: [output_file]

wes_client/wes_client_main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import logging
1111
import schema_salad.ref_resolver
1212
import requests
13-
from requests.exceptions import MissingSchema
13+
from requests.exceptions import InvalidSchema
1414
from wes_service.util import visit
1515
from bravado.client import SwaggerClient
1616
from bravado.requests_client import RequestsClient
@@ -161,7 +161,7 @@ def fixpaths(d):
161161
logging.info(str(s["workflow_log"]["stderr"]))
162162
logs = requests.get(s["workflow_log"]["stderr"], headers={"Authorization": args.auth}).text
163163
logging.info("Workflow log:\n" + logs)
164-
except MissingSchema:
164+
except InvalidSchema:
165165
logging.info("Workflow log:\n" + str(s["workflow_log"]["stderr"]))
166166

167167
if "fields" in s["outputs"] and s["outputs"]["fields"] is None:

wes_service/cwl_runner.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
from __future__ import print_function
12
import json
23
import os
34
import subprocess
5+
import tempfile
46
import urllib
57
import uuid
68

9+
import connexion
10+
from werkzeug.utils import secure_filename
11+
712
from wes_service.util import WESBackend
813

914

@@ -41,14 +46,13 @@ def run(self, request, opts):
4146
with open(os.path.join(self.workdir, "request.json"), "w") as f:
4247
json.dump(request, f)
4348

44-
input_json = os.path.join(self.workdir, "cwl.input.json")
45-
with open(input_json, "w") as inputtemp:
49+
with open(os.path.join(
50+
self.workdir, "cwl.input.json"), "w") as inputtemp:
4651
json.dump(request["workflow_params"], inputtemp)
4752

4853
if request.get("workflow_descriptor"):
4954
workflow_descriptor = request.get('workflow_descriptor')
5055
with open(os.path.join(self.workdir, "workflow.cwl"), "w") as f:
51-
# FIXME #14 workflow_descriptor isn't defined
5256
f.write(workflow_descriptor)
5357
workflow_url = urllib.pathname2url(os.path.join(self.workdir, "workflow.cwl"))
5458
else:
@@ -58,8 +62,8 @@ def run(self, request, opts):
5862
stderr = open(os.path.join(self.workdir, "stderr"), "w")
5963

6064
runner = opts.getopt("runner", default="cwl-runner")
61-
extra = opts.getoptlist("extra") # if the user specified none, returns []
62-
command_args = [runner] + extra + [workflow_url, input_json]
65+
extra = opts.getoptlist("extra")
66+
command_args = [runner] + extra + [workflow_url, inputtemp.name]
6367
proc = subprocess.Popen(command_args,
6468
stdout=output,
6569
stderr=stderr,
@@ -178,12 +182,31 @@ def ListWorkflows(self):
178182
"next_page_token": ""
179183
}
180184

181-
def RunWorkflow(self, body):
182-
# FIXME Add error responses #16
183-
if body["workflow_type"] == "CWL" and body["workflow_type_version"] != "v1.0":
185+
def RunWorkflow(self):
186+
tempdir = tempfile.mkdtemp()
187+
body = {}
188+
for k, ls in connexion.request.files.iterlists():
189+
for v in ls:
190+
if k == "workflow_descriptor":
191+
filename = secure_filename(v.filename)
192+
v.save(os.path.join(tempdir, filename))
193+
elif k in ("workflow_params", "tags", "workflow_engine_parameters"):
194+
body[k] = json.loads(v.read())
195+
else:
196+
body[k] = v.read()
197+
198+
if body['workflow_type'] != "CWL" or \
199+
body['workflow_type_version'] != "v1.0":
184200
return
201+
202+
body["workflow_url"] = "file:///%s/%s" % (tempdir, body["workflow_url"])
203+
index = body["workflow_url"].find("http")
204+
if index > 0:
205+
body["workflow_url"] = body["workflow_url"][index:]
206+
185207
workflow_id = uuid.uuid4().hex
186208
job = Workflow(workflow_id)
209+
187210
job.run(body, self)
188211
return {"workflow_id": workflow_id}
189212

0 commit comments

Comments
 (0)