Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nf-core Launch web GUI #638

Merged
merged 17 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 256 additions & 76 deletions nf_core/launch.py

Large diffs are not rendered by default.

118 changes: 30 additions & 88 deletions nf_core/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,100 +393,42 @@ def launch_web_builder(self):
'status': 'waiting_for_user',
'schema': json.dumps(self.schema)
}
web_response = nf_core.utils.poll_nfcore_web_api(self.web_schema_build_url, content)
try:
response = requests.post(url=self.web_schema_build_url, data=content)
except (requests.exceptions.Timeout):
raise AssertionError("Schema builder URL timed out: {}".format(self.web_schema_build_url))
except (requests.exceptions.ConnectionError):
raise AssertionError("Could not connect to schema builder URL: {}".format(self.web_schema_build_url))
assert 'api_url' in web_response
assert 'web_url' in web_response
assert web_response['status'] == 'recieved'
except (AssertionError) as e:
logging.debug("Response content:\n{}".format(json.dumps(web_response, indent=4)))
raise AssertionError("JSON Schema builder response not recognised: {}\n See verbose log for full response (nf-core -v schema)".format(self.web_schema_build_url))
else:
if response.status_code != 200:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("Could not access remote JSON Schema builder: {} (HTML {} Error)".format(self.web_schema_build_url, response.status_code))
else:
try:
web_response = json.loads(response.content)
assert 'status' in web_response
assert 'api_url' in web_response
assert 'web_url' in web_response
assert web_response['status'] == 'recieved'
except (json.decoder.JSONDecodeError, AssertionError) as e:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("JSON Schema builder response not recognised: {}\n See verbose log for full response (nf-core -v schema)".format(self.web_schema_build_url))
else:
self.web_schema_build_web_url = web_response['web_url']
self.web_schema_build_api_url = web_response['api_url']
logging.info("Opening URL: {}".format(web_response['web_url']))
webbrowser.open(web_response['web_url'])
logging.info("Waiting for form to be completed in the browser. Remember to click Finished when you're done.\n")
self.wait_web_builder_response()

def wait_web_builder_response(self):
try:
is_saved = False
check_count = 0
def spinning_cursor():
while True:
for cursor in '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏':
yield '{} Use ctrl+c to stop waiting and force exit. '.format(cursor)
spinner = spinning_cursor()
while not is_saved:
# Show the loading spinner every 0.1s
time.sleep(0.1)
loading_text = next(spinner)
sys.stdout.write(loading_text)
sys.stdout.flush()
sys.stdout.write('\b'*len(loading_text))
# Only check every 2 seconds, but update the spinner every 0.1s
check_count += 1
if check_count > 20:
is_saved = self.get_web_builder_response()
check_count = 0
except KeyboardInterrupt:
raise AssertionError("Cancelled!")

self.web_schema_build_web_url = web_response['web_url']
self.web_schema_build_api_url = web_response['api_url']
logging.info("Opening URL: {}".format(web_response['web_url']))
webbrowser.open(web_response['web_url'])
logging.info("Waiting for form to be completed in the browser. Remember to click Finished when you're done.\n")
nf_core.utils.wait_cli_function(self.get_web_builder_response)

def get_web_builder_response(self):
"""
Given a URL for a Schema build response, recursively query it until results are ready.
Once ready, validate Schema and write to disk.
"""
# Clear requests_cache so that we get the updated statuses
requests_cache.clear()
try:
response = requests.get(self.web_schema_build_api_url, headers={'Cache-Control': 'no-cache'})
except (requests.exceptions.Timeout):
raise AssertionError("Schema builder URL timed out: {}".format(self.web_schema_build_api_url))
except (requests.exceptions.ConnectionError):
raise AssertionError("Could not connect to schema builder URL: {}".format(self.web_schema_build_api_url))
else:
if response.status_code != 200:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("Could not access remote JSON Schema builder results: {} (HTML {} Error)".format(self.web_schema_build_api_url, response.status_code))
web_response = nf_core.utils.poll_nfcore_web_api(self.web_schema_build_api_url)
if web_response['status'] == 'error':
raise AssertionError("Got error from JSON Schema builder ( {} )".format(web_response.get('message')))
elif web_response['status'] == 'waiting_for_user':
return False
elif web_response['status'] == 'web_builder_edited':
logging.info("Found saved status from nf-core JSON Schema builder")
try:
self.schema = web_response['schema']
self.validate_schema(self.schema)
except AssertionError as e:
raise AssertionError("Response from JSON Builder did not pass validation:\n {}".format(e))
else:
try:
web_response = json.loads(response.content)
assert 'status' in web_response
except (json.decoder.JSONDecodeError, AssertionError) as e:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("JSON Schema builder results response not recognised: {}\n See verbose log for full response".format(self.web_schema_build_api_url))
else:
if web_response['status'] == 'error':
raise AssertionError("Got error from JSON Schema builder ( {} )".format(click.style(web_response.get('message'), fg='red')))
elif web_response['status'] == 'waiting_for_user':
return False
elif web_response['status'] == 'web_builder_edited':
logging.info("Found saved status from nf-core JSON Schema builder")
try:
self.schema = json.loads(web_response['schema'])
self.validate_schema(self.schema)
except json.decoder.JSONDecodeError as e:
raise AssertionError("Could not parse returned JSON:\n {}".format(e))
except AssertionError as e:
raise AssertionError("Response from JSON Builder did not pass validation:\n {}".format(e))
else:
self.save_schema()
return True
else:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("JSON Schema builder returned unexpected status ({}): {}\n See verbose log for full response".format(web_response['status'], self.web_schema_build_api_url))
self.save_schema()
return True
else:
logging.debug("Response content:\n{}".format(json.dumps(web_response, indent=4)))
raise AssertionError("JSON Schema builder returned unexpected status ({}): {}\n See verbose log for full response".format(web_response['status'], self.web_schema_build_api_url))
72 changes: 72 additions & 0 deletions nf_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import logging
import os
import re
import requests
import requests_cache
import subprocess
import sys
import time

def fetch_wf_config(wf_path):
"""Uses Nextflow to retrieve the the configuration variables
Expand Down Expand Up @@ -117,3 +120,72 @@ def setup_requests_cachedir():
expire_after=datetime.timedelta(hours=1),
backend='sqlite',
)

def wait_cli_function(poll_func, poll_every=20):
"""
Display a command-line spinner while calling a function repeatedly.

Keep waiting until that function returns True

Arguments:
poll_func (function): Function to call
poll_every (int): How many tenths of a second to wait between function calls. Default: 20.

Returns:
None. Just sits in an infite loop until the function returns True.
"""
try:
is_finished = False
check_count = 0
def spinning_cursor():
while True:
for cursor in '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏':
yield '{} Use ctrl+c to stop waiting and force exit. '.format(cursor)
spinner = spinning_cursor()
while not is_finished:
# Show the loading spinner every 0.1s
time.sleep(0.1)
loading_text = next(spinner)
sys.stdout.write(loading_text)
sys.stdout.flush()
sys.stdout.write('\b'*len(loading_text))
# Only check every 2 seconds, but update the spinner every 0.1s
check_count += 1
if check_count > poll_every:
is_finished = poll_func()
check_count = 0
except KeyboardInterrupt:
raise AssertionError("Cancelled!")

def poll_nfcore_web_api(api_url, post_data=None):
"""
Poll the nf-core website API

Takes argument api_url for URL

Expects API reponse to be valid JSON and contain a top-level 'status' key.
"""
# Clear requests_cache so that we get the updated statuses
requests_cache.clear()
try:
if post_data is None:
response = requests.get(api_url, headers={'Cache-Control': 'no-cache'})
else:
response = requests.post(url=api_url, data=post_data)
except (requests.exceptions.Timeout):
raise AssertionError("URL timed out: {}".format(api_url))
except (requests.exceptions.ConnectionError):
raise AssertionError("Could not connect to URL: {}".format(api_url))
else:
if response.status_code != 200:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("Could not access remote API results: {} (HTML {} Error)".format(api_url, response.status_code))
else:
try:
web_response = json.loads(response.content)
assert 'status' in web_response
except (json.decoder.JSONDecodeError, AssertionError) as e:
logging.debug("Response content:\n{}".format(response.content))
raise AssertionError("nf-core website API results response not recognised: {}\n See verbose log for full response".format(api_url))
else:
return web_response
17 changes: 14 additions & 3 deletions scripts/nf-core
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,13 @@ def list(keywords, sort, json):
@nf_core_cli.command(help_priority=2)
@click.argument(
'pipeline',
required = True,
required = False,
metavar = "<pipeline name>"
)
@click.option(
'-i', '--id',
help = "ID for web-gui launch parameter set"
)
@click.option(
'-r', '--revision',
help = "Release/branch/SHA of the project to run (if remote)"
Expand Down Expand Up @@ -127,9 +131,16 @@ def list(keywords, sort, json):
default = False,
help = "Show hidden parameters."
)
def launch(pipeline, revision, command_only, params_in, params_out, save_all, show_hidden):
@click.option(
'--url',
type = str,
default = 'https://nf-co.re/launch',
help = 'Customise the builder URL (for development work)'
)
def launch(pipeline, id, revision, command_only, params_in, params_out, save_all, show_hidden, url):
""" Run pipeline, interactive parameter prompts """
if nf_core.launch.launch_pipeline(pipeline, revision, command_only, params_in, params_out, save_all, show_hidden) == False:
launcher = nf_core.launch.Launch(pipeline, revision, command_only, params_in, params_out, save_all, show_hidden, url, id)
if launcher.launch_pipeline() == False:
sys.exit(1)

# nf-core download
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
'GitPython',
'jinja2',
'jsonschema',
'PyInquirer',
# 'PyInquirer>1.0.3',
# Need the new release of PyInquirer, see nf_core/launch.py for details
'PyInquirer @ https://github.com/CITGuru/PyInquirer/archive/master.zip',
'pyyaml',
'requests',
'requests_cache',
Expand Down
Loading