Skip to content

Commit 968b558

Browse files
DailyDreamingdavid4096
authored andcommitted
Docstrings, --version, and a wes-client test. (#25)
* flake8 ignore line length. * Pass flake8. * Newline in .flake8 * Basic help descriptions. * Docstrings to util. * More docstrings. * More docstrings. * More docstrings. * Refactor. * Add --version to service. * Add test for client. * Add test to travis. * Remove test cruft. * Remove service test stub. * flake8 fix. * Change travis yml test run command. * Remove test cruft. * pip install future added to reqs. * Different relative path for travis. * Remove debugging ls from travis script. * Remove redundant SIGTERM. * Amend CLI entrypoints. * Add nosetests to dev-requirements.txt and rename the test file. * Add nosetests to the yml. * Pytest instead of nosetests. * Update requirements. * subprocess32 * Test server stdout to DEVNULL. * Test checks for output file and deletes outputs. * Run the correct test in travis. * Pytest. * Add local file test. * Amend local file path. * Explicit dockstore url. * Requirements given versions. Add pycache to gitignore. * Requirements.txt. * Add testing for Toil. * Minor naming. * Dependencies in setup.py. * Update .travis.yml * Update .travis.yml * Pytest in dev-requirements.txt * Revert new files. * Move files back out of init. * flake8 changes. * Better check.
1 parent 0bd64a6 commit 968b558

14 files changed

+446
-282
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ coverage.xml
5252
*.mo
5353
*.pot
5454

55+
# PyTest
56+
.pytest_cache
57+
5558
# PyCharm
5659
.idea/
5760

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ before_install:
55
- sudo apt-get update -qq
66
- pip install . --process-dependency-links
77
- pip install -r dev-requirements.txt
8+
- pip install toil[all]==3.16.0
89
script:
910
- flake8 wes_service wes_client
11+
- pytest
1012
deploy:
1113
provider: pypi
1214
on:

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
flake8
2+
pytest

passenger_wsgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import wes_service
1+
from wes_service.wes_service_main import setup
22

3-
application = wes_service.setup()
3+
application = setup()

setup.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,22 @@
2323
package_data={'wes_service': ['openapi/workflow_execution_service.swagger.yaml']},
2424
include_package_data=True,
2525
install_requires=[
26-
'connexion',
27-
'bravado',
26+
'future',
27+
'connexion==1.4.2',
28+
'bravado==10.1.0',
2829
'ruamel.yaml >= 0.12.4, < 0.15',
29-
'cwlref-runner',
30-
'schema-salad'
30+
'cwlref-runner==1.0',
31+
'schema-salad>=2.6, <3',
32+
'subprocess32==3.5.2'
3133
],
3234
entry_points={
33-
'console_scripts': ["wes-server=wes_service:main",
34-
"wes-client=wes_client:main"]
35+
'console_scripts': ["wes-server=wes_service.wes_service_main:main",
36+
"wes-client=wes_client.wes_client_main:main"]
3537
},
3638
extras_require={
3739
"arvados": ["arvados-cwl-runner"
38-
]},
40+
],
41+
"toil": ["toil[all]==3.16.0"
42+
]},
3943
zip_safe=False
4044
)

test/__init__.py

Whitespace-only changes.

test/test_integration.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import absolute_import
2+
import unittest
3+
import time
4+
import os
5+
import subprocess32 as subprocess
6+
import signal
7+
import requests
8+
import shutil
9+
10+
11+
class IntegrationTest(unittest.TestCase):
12+
"""A baseclass that's inherited for use with different cwl backends."""
13+
def setUp(self):
14+
"""Start a (local) wes-service server to make requests against."""
15+
raise NotImplementedError
16+
17+
def tearDown(self):
18+
"""Kill the wes-service server."""
19+
os.kill(self.wes_server_process.pid, signal.SIGTERM)
20+
while get_server_pids():
21+
for pid in get_server_pids():
22+
try:
23+
os.kill(int(pid), signal.SIGKILL)
24+
time.sleep(3)
25+
except OSError as e:
26+
print(e)
27+
28+
unittest.TestCase.tearDown(self)
29+
30+
def test_dockstore_md5sum(self):
31+
"""Fetch the md5sum cwl from dockstore, run it on the wes-service server, and check for the correct output."""
32+
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)
34+
35+
self.assertTrue(check_for_file(output_filepath), 'Output file was not found: ' + str(output_filepath))
36+
shutil.rmtree('workflows')
37+
38+
def test_local_md5sum(self):
39+
"""Pass a local md5sum cwl to the wes-service server, and check for the correct output."""
40+
cwl_local_path = os.path.abspath('testdata/md5sum.cwl')
41+
output_filepath = run_md5sum(cwl_input='file://' + cwl_local_path)
42+
43+
self.assertTrue(check_for_file(output_filepath), 'Output file was not found: ' + str(output_filepath))
44+
shutil.rmtree('workflows')
45+
46+
47+
def run_md5sum(cwl_input):
48+
"""Pass a local md5sum cwl to the wes-service server, and return the path of the output file that was created."""
49+
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()
53+
output_dir = os.path.abspath(os.path.join('workflows', response['workflow_id'], 'outdir'))
54+
return os.path.join(output_dir, 'md5sum.txt')
55+
56+
57+
def get_server_pids():
58+
try:
59+
pids = subprocess.check_output(['pgrep', '-f', 'wes_service_main.py']).strip().split()
60+
except subprocess.CalledProcessError:
61+
return None
62+
return pids
63+
64+
65+
def check_for_file(filepath, seconds=20):
66+
"""Return True if a file exists within a certain amount of time."""
67+
wait_counter = 0
68+
while not os.path.exists(filepath):
69+
time.sleep(1)
70+
wait_counter += 1
71+
if os.path.exists(filepath):
72+
return True
73+
if wait_counter > seconds:
74+
return False
75+
return True
76+
77+
78+
class CwltoolTest(IntegrationTest):
79+
"""Test using cwltool."""
80+
def setUp(self):
81+
"""
82+
Start a (local) wes-service server to make requests against.
83+
Use cwltool as the wes-service server 'backend'.
84+
"""
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)
87+
time.sleep(5)
88+
89+
90+
class ToilTest(IntegrationTest):
91+
"""Test using Toil."""
92+
def setUp(self):
93+
"""
94+
Start a (local) wes-service server to make requests against.
95+
Use toil as the wes-service server 'backend'.
96+
"""
97+
self.wes_server_process = subprocess.Popen('python {} '
98+
'--opt runner=cwltoil --opt extra=--logLevel=CRITICAL'
99+
''.format(os.path.abspath('wes_service/wes_service_main.py')),
100+
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
101+
time.sleep(5)
102+
103+
104+
# Prevent pytest/unittest's discovery from attempting to discover the base test class.
105+
del IntegrationTest
106+
107+
108+
if __name__ == '__main__':
109+
unittest.main() # run all tests

wes_client/__init__.py

Lines changed: 0 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,183 +0,0 @@
1-
#!/usr/bin/env python
2-
import urlparse
3-
import pkg_resources # part of setuptools
4-
import urllib
5-
import json
6-
import time
7-
import sys
8-
import os
9-
import argparse
10-
import logging
11-
import schema_salad.ref_resolver
12-
import requests
13-
from wes_service.util import visit
14-
from bravado.client import SwaggerClient
15-
from bravado.requests_client import RequestsClient
16-
17-
def main(argv=sys.argv[1:]):
18-
parser = argparse.ArgumentParser(description='Workflow Execution Service')
19-
parser.add_argument("--host", type=str, default=os.environ.get("WES_API_HOST"))
20-
parser.add_argument("--auth", type=str, default=os.environ.get("WES_API_AUTH"))
21-
parser.add_argument("--proto", type=str, default=os.environ.get("WES_API_PROTO", "https"))
22-
parser.add_argument("--quiet", action="store_true", default=False)
23-
parser.add_argument("--outdir", type=str)
24-
parser.add_argument("--page", type=str, default=None)
25-
parser.add_argument("--page-size", type=int, default=None)
26-
27-
exgroup = parser.add_mutually_exclusive_group()
28-
exgroup.add_argument("--run", action="store_true", default=False)
29-
exgroup.add_argument("--get", type=str, default=None)
30-
exgroup.add_argument("--log", type=str, default=None)
31-
exgroup.add_argument("--list", action="store_true", default=False)
32-
exgroup.add_argument("--info", action="store_true", default=False)
33-
exgroup.add_argument("--version", action="store_true", default=False)
34-
35-
exgroup = parser.add_mutually_exclusive_group()
36-
exgroup.add_argument("--wait", action="store_true", default=True, dest="wait")
37-
exgroup.add_argument("--no-wait", action="store_false", default=True, dest="wait")
38-
39-
parser.add_argument("workflow_url", type=str, nargs="?", default=None)
40-
parser.add_argument("job_order", type=str, nargs="?", default=None)
41-
args = parser.parse_args(argv)
42-
43-
if args.version:
44-
pkg = pkg_resources.require("wes_service")
45-
print(u"%s %s" % (sys.argv[0], pkg[0].version))
46-
exit(0)
47-
48-
http_client = RequestsClient()
49-
split = urlparse.urlsplit("%s://%s/" % (args.proto, args.host))
50-
51-
http_client.set_api_key(
52-
split.hostname, args.auth,
53-
param_name='Authorization', param_in='header')
54-
client = SwaggerClient.from_url(
55-
"%s://%s/ga4gh/wes/v1/swagger.json" % (args.proto, args.host),
56-
http_client=http_client, config={'use_models': False})
57-
58-
if args.list:
59-
response = client.WorkflowExecutionService.ListWorkflows(page_token=args.page, page_size=args.page_size)
60-
json.dump(response.result(), sys.stdout, indent=4)
61-
return 0
62-
63-
if args.log:
64-
response = client.WorkflowExecutionService.GetWorkflowLog(
65-
workflow_id=args.log)
66-
sys.stdout.write(response.result()["workflow_log"]["stderr"])
67-
return 0
68-
69-
if args.get:
70-
response = client.WorkflowExecutionService.GetWorkflowLog(
71-
workflow_id=args.get)
72-
json.dump(response.result(), sys.stdout, indent=4)
73-
return 0
74-
75-
if args.info:
76-
response = client.WorkflowExecutionService.GetServiceInfo()
77-
json.dump(response.result(), sys.stdout, indent=4)
78-
return 0
79-
80-
loader = schema_salad.ref_resolver.Loader({
81-
"location": {"@type": "@id"},
82-
"path": {"@type": "@id"}
83-
})
84-
input, _ = loader.resolve_ref(args.job_order)
85-
86-
basedir = os.path.dirname(args.job_order)
87-
88-
def fixpaths(d):
89-
if isinstance(d, dict):
90-
if "path" in d:
91-
if ":" not in d["path"]:
92-
local_path = os.path.normpath(
93-
os.path.join(os.getcwd(), basedir, d["path"]))
94-
d["location"] = urllib.pathname2url(local_path)
95-
else:
96-
d["location"] = d["path"]
97-
del d["path"]
98-
loc = d.get("location", "")
99-
if d.get("class") == "Directory":
100-
if loc.startswith("http:") or loc.startswith("https:"):
101-
logging.error("Directory inputs not supported with http references")
102-
exit(33)
103-
# if not (loc.startswith("http:") or loc.startswith("https:")
104-
# or args.job_order.startswith("http:") or args.job_order.startswith("https:")):
105-
# logging.error("Upload local files not supported, must use http: or https: references.")
106-
# exit(33)
107-
108-
visit(input, fixpaths)
109-
110-
workflow_url = args.workflow_url
111-
if not workflow_url.startswith("/") and ":" not in workflow_url:
112-
workflow_url = "file://" + os.path.abspath(workflow_url)
113-
114-
if args.quiet:
115-
logging.basicConfig(level=logging.WARNING)
116-
else:
117-
logging.basicConfig(level=logging.INFO)
118-
119-
parts = [
120-
("workflow_params", json.dumps(input)),
121-
("workflow_type", "CWL"),
122-
("workflow_type_version", "v1.0")
123-
]
124-
125-
if workflow_url.startswith("file://"):
126-
# with open(workflow_url[7:], "rb") as f:
127-
# body["workflow_descriptor"] = f.read()
128-
rootdir = os.path.dirname(workflow_url[7:])
129-
dirpath = rootdir
130-
#for dirpath, dirnames, filenames in os.walk(rootdir):
131-
for f in os.listdir(rootdir):
132-
if f.startswith("."):
133-
continue
134-
fn = os.path.join(dirpath, f)
135-
if os.path.isfile(fn):
136-
parts.append(('workflow_descriptor', (fn[len(rootdir)+1:], open(fn, "rb"))))
137-
parts.append(("workflow_url", os.path.basename(workflow_url[7:])))
138-
else:
139-
parts.append(("workflow_url", workflow_url))
140-
141-
postresult = http_client.session.post("%s://%s/ga4gh/wes/v1/workflows" % (args.proto, args.host),
142-
files=parts,
143-
headers={"Authorization": args.auth})
144-
145-
r = json.loads(postresult.text)
146-
147-
if postresult.status_code != 200:
148-
logging.error("%s", r)
149-
exit(1)
150-
151-
if args.wait:
152-
logging.info("Workflow id is %s", r["workflow_id"])
153-
else:
154-
sys.stdout.write(r["workflow_id"]+"\n")
155-
exit(0)
156-
157-
r = client.WorkflowExecutionService.GetWorkflowStatus(
158-
workflow_id=r["workflow_id"]).result()
159-
while r["state"] in ("QUEUED", "INITIALIZING", "RUNNING"):
160-
time.sleep(8)
161-
r = client.WorkflowExecutionService.GetWorkflowStatus(
162-
workflow_id=r["workflow_id"]).result()
163-
164-
logging.info("State is %s", r["state"])
165-
166-
s = client.WorkflowExecutionService.GetWorkflowLog(
167-
workflow_id=r["workflow_id"]).result()
168-
logging.info("%s", s["workflow_log"]["stderr"])
169-
logs = requests.get(s["workflow_log"]["stderr"], headers={"Authorization": args.auth}).text
170-
logging.info("Workflow log:\n"+logs)
171-
172-
if "fields" in s["outputs"] and s["outputs"]["fields"] is None:
173-
del s["outputs"]["fields"]
174-
json.dump(s["outputs"], sys.stdout, indent=4)
175-
176-
if r["state"] == "COMPLETE":
177-
return 0
178-
else:
179-
return 1
180-
181-
182-
if __name__ == "__main__":
183-
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)