diff --git a/aiida/backends/tests/__init__.py b/aiida/backends/tests/__init__.py index 1523d1450c..fc78ccc7d3 100644 --- a/aiida/backends/tests/__init__.py +++ b/aiida/backends/tests/__init__.py @@ -59,6 +59,7 @@ 'cmdline.commands.status': ['aiida.backends.tests.cmdline.commands.test_status'], 'cmdline.commands.user': ['aiida.backends.tests.cmdline.commands.test_user'], 'cmdline.commands.verdi': ['aiida.backends.tests.cmdline.commands.test_verdi'], + 'cmdline.commands.restapi': ['aiida.backends.tests.cmdline.commands.test_restapi'], 'cmdline.params.types.calculation': ['aiida.backends.tests.cmdline.params.types.test_calculation'], 'cmdline.params.types.code': ['aiida.backends.tests.cmdline.params.types.test_code'], 'cmdline.params.types.computer': ['aiida.backends.tests.cmdline.params.types.test_computer'], diff --git a/aiida/backends/tests/cmdline/commands/test_restapi.py b/aiida/backends/tests/cmdline/commands/test_restapi.py new file mode 100644 index 0000000000..da2de1e0a6 --- /dev/null +++ b/aiida/backends/tests/cmdline/commands/test_restapi.py @@ -0,0 +1,35 @@ +"""Tests for `verdi restapi`.""" +from __future__ import division +from __future__ import print_function +from __future__ import absolute_import + +from click.testing import CliRunner + +from aiida.backends.testbase import AiidaTestCase +from aiida.cmdline.commands.cmd_restapi import restapi + + +class TestVerdiRestapiCommand(AiidaTestCase): + """tests for verdi restapi command""" + + def setUp(self): + super(TestVerdiRestapiCommand, self).setUp() + self.cli_runner = CliRunner() + + def test_run_restapi(self): + """Test `verdi restapi`.""" + + options = ['--no-hookup', '--hostname', 'localhost', '--port', '6000', '--debug', '--wsgi-profile'] + + result = self.cli_runner.invoke(restapi, options) + self.assertIsNone(result.exception, result.output) + self.assertClickSuccess(result) + + def test_help(self): + """Tests help text for restapi command.""" + options = ['--help'] + + # verdi restapi + result = self.cli_runner.invoke(restapi, options) + self.assertIsNone(result.exception, result.output) + self.assertIn('Usage', result.output) diff --git a/aiida/backends/tests/test_restapi.py b/aiida/backends/tests/test_restapi.py index 717dba4193..6194d1fb0a 100644 --- a/aiida/backends/tests/test_restapi.py +++ b/aiida/backends/tests/test_restapi.py @@ -27,7 +27,7 @@ class RESTApiTestCase(AiidaTestCase): """ Setup of the tests for the AiiDA RESTful-api """ - _url_prefix = "/api/v2" + _url_prefix = "/api/v3" _dummy_data = {} _PERPAGE_DEFAULT = 20 _LIMIT_DEFAULT = 400 diff --git a/aiida/cmdline/commands/cmd_restapi.py b/aiida/cmdline/commands/cmd_restapi.py index 1b39964033..649e8851a3 100644 --- a/aiida/cmdline/commands/cmd_restapi.py +++ b/aiida/cmdline/commands/cmd_restapi.py @@ -21,39 +21,50 @@ import aiida.restapi from aiida.cmdline.commands.cmd_verdi import verdi +from aiida.cmdline.params.options import HOSTNAME, PORT -DEFAULT_CONFIG_DIR = os.path.join(os.path.split(os.path.abspath(aiida.restapi.__file__))[0], 'common') +CONFIG_DIR = os.path.join(os.path.split(os.path.abspath(aiida.restapi.__file__))[0], 'common') @verdi.command('restapi') -@click.option('-H', '--host', type=click.STRING, default='127.0.0.1', help='the hostname to use') -@click.option('-p', '--port', type=click.INT, default=5000, help='the port to use') +@HOSTNAME(default='127.0.0.1') +@PORT(default=5000) @click.option( '-c', '--config-dir', type=click.Path(exists=True), - default=DEFAULT_CONFIG_DIR, + default=CONFIG_DIR, help='the path of the configuration directory') -def restapi(host, port, config_dir): +@click.option('--debug', 'debug', is_flag=True, default=False, help='run app in debug mode') +@click.option( + '--wsgi-profile', + 'wsgi_profile', + is_flag=True, + default=False, + help='to use WSGI profiler middleware for finding bottlenecks in web application') +@click.option('--hookup/--no-hookup', 'hookup', is_flag=True, default=True, help='to hookup app') +def restapi(hostname, port, config_dir, debug, wsgi_profile, hookup): """ Run the AiiDA REST API server Example Usage: \b - verdi -p restapi --host 127.0.0.5 --port 6789 --config-dir + verdi -p restapi --hostname 127.0.0.5 --port 6789 --config-dir + --debug --wsgi-profile --hookup """ from aiida.restapi.api import App, AiidaApi from aiida.restapi.run_api import run_api # Construct parameter dictionary kwargs = dict( - hookup=True, prog_name='verdi-restapi', - default_host=host, - default_port=port, - default_config=config_dir, - parse_aiida_profile=False, + hostname=hostname, + port=port, + config=config_dir, + debug=debug, + wsgi_profile=wsgi_profile, + hookup=hookup, catch_internal_server=True) # Invoke the runner diff --git a/aiida/restapi/run_api.py b/aiida/restapi/run_api.py index 5e9d9022c9..db075a5022 100755 --- a/aiida/restapi/run_api.py +++ b/aiida/restapi/run_api.py @@ -16,127 +16,47 @@ from __future__ import print_function from __future__ import absolute_import -import argparse import imp import os from flask_cors import CORS -def run_api(flask_app, flask_api, *args, **kwargs): +def run_api(flask_app, flask_api, **kwargs): """ - Takes a flask.Flask instance and runs it. Parses - command-line flags to configure the app. + Takes a flask.Flask instance and runs it. flask_app: Class inheriting from Flask app class flask_api = flask_restful API class to be used to wrap the app - args: required by argparse - kwargs: List of valid parameters: prog_name: name of the command before arguments are parsed. Useful when api is embedded in a command, such as verdi restapi - default_host: self-explainatory - default_port: self-explainatory - default_config_dir = directory containing the config.py file used to + hostname: self-explainatory + port: self-explainatory + config: directory containing the config.py file used to configure the RESTapi - parse_aiida_profile= if True, parses an option to specify the AiiDA - profile + catch_internal_server: If true, catch and print all inter server errors + debug: self-explainatory + wsgi_profile:to use WSGI profiler middleware for finding bottlenecks in web application + hookup: to hookup app All other passed parameters are ignored. """ # pylint: disable=too-many-locals - import aiida # Mainly needed to locate the correct aiida path - from aiida.manage.configuration import load_profile - - # Unpack parameters and assign defaults if needed - prog_name = kwargs['prog_name'] if 'prog_name' in kwargs else "" - - default_host = kwargs['default_host'] if 'default_host' in kwargs else \ - "127.0.0.1" - - default_port = kwargs['default_port'] if 'default_port' in kwargs else \ - "5000" - - default_config_dir = kwargs['default_config_dir'] if \ - 'default_config_dir' in kwargs \ - else os.path.join(os.path.split(os.path.abspath( - aiida.restapi.__file__))[0], 'common') - - parse_aiida_profile = kwargs['parse_aiida_profile'] if \ - 'parse_aiida_profile' in kwargs else False - - catch_internal_server = kwargs['catch_internal_server'] if\ - 'catch_internal_server' in kwargs else False - - hookup = kwargs['hookup'] if 'hookup' in kwargs else False - - # Set up the command-line options - parser = argparse.ArgumentParser(prog=prog_name, description='Hook up the AiiDA ' 'RESTful API') - - parser.add_argument("-H", "--host", - help="Hostname of the Flask app " + \ - "[default %s]" % default_host, - dest='host', - default=default_host) - parser.add_argument("-P", "--port", - help="Port for the Flask app " + \ - "[default %s]" % default_port, - dest='port', - default=default_port) - parser.add_argument("-c", "--config-dir", - help="Directory with config.py for Flask app " + \ - "[default {}]".format(default_config_dir), - dest='config_dir', - default=default_config_dir) - - # This one is included only if necessary - if parse_aiida_profile: - parser.add_argument( - "-p", - "--aiida-profile", - help="AiiDA profile to expose through the RESTful " - "API [default: the default AiiDA profile]", - dest="aiida_profile", - default=None) - - # Two options useful for debugging purposes, but - # a bit dangerous so not exposed in the help message. - parser.add_argument("-d", "--debug", action="store_true", dest="debug", help=argparse.SUPPRESS) - parser.add_argument("-w", "--wsgi-profile", action="store_true", dest="wsgi_profile", help=argparse.SUPPRESS) - - parsed_args = parser.parse_args(args) - # Import the right configuration file - confs = imp.load_source( - os.path.join(parsed_args.config_dir, 'config'), os.path.join(parsed_args.config_dir, 'config.py')) - - # Set aiida profile - # - # General logic: - # - # if aiida_profile is parsed the following cases exist: - # - # aiida_profile: - # "default" --> default profile set in .aiida/config.json - # --> corresponding profile in .aiida/config.json - # None --> default restapi profile set in /config,py - # - # if aiida_profile is not parsed we assume - # - # default restapi profile set in /config.py - - if parse_aiida_profile and parsed_args.aiida_profile is not None: - aiida_profile = parsed_args.aiida_profile - - elif confs.DEFAULT_AIIDA_PROFILE is not None: - aiida_profile = confs.DEFAULT_AIIDA_PROFILE + # Unpack parameters + hostname = kwargs['hostname'] + port = kwargs['port'] + config = kwargs['config'] - else: - aiida_profile = None + catch_internal_server = kwargs.pop('catch_internal_server', False) + debug = kwargs['debug'] + wsgi_profile = kwargs['wsgi_profile'] + hookup = kwargs['hookup'] - # Load the default profile - load_profile(aiida_profile) + # Import the right configuration file + confs = imp.load_source(os.path.join(config, 'config'), os.path.join(config, 'config.py')) # Instantiate an app app_kwargs = dict(catch_internal_server=catch_internal_server) @@ -146,8 +66,8 @@ def run_api(flask_app, flask_api, *args, **kwargs): app.config.update(**confs.APP_CONFIG) # cors - cors_prefix = os.path.join(confs.PREFIX, "*") - CORS(app, resources={r"" + cors_prefix: {"origins": "*"}}) + cors_prefix = os.path.join(confs.PREFIX, '*') + CORS(app, resources={r"" + cors_prefix: {'origins': '*'}}) # Config the serializer used by the app if confs.SERIALIZER_CONFIG: @@ -156,7 +76,7 @@ def run_api(flask_app, flask_api, *args, **kwargs): # If the user selects the profiling option, then we need # to do a little extra setup - if parsed_args.wsgi_profile: + if wsgi_profile: from werkzeug.contrib.profiler import ProfilerMiddleware app.config['PROFILE'] = True @@ -168,7 +88,7 @@ def run_api(flask_app, flask_api, *args, **kwargs): # Check if the app has to be hooked-up or just returned if hookup: - api.app.run(debug=parsed_args.debug, host=parsed_args.host, port=int(parsed_args.port), threaded=True) + api.app.run(debug=debug, host=hostname, port=int(port), threaded=True) else: # here we return the app, and the api with no specifications on debug @@ -177,24 +97,3 @@ def run_api(flask_app, flask_api, *args, **kwargs): # the user-defined configuration of the app is ineffective (it only # affects the internal werkzeug server used by Flask). return (app, api) - - -# Standard boilerplate to run the api -if __name__ == '__main__': - - # I run the app via a wrapper that accepts arguments such as host and port - # e.g. python api.py --host=127.0.0.2 --port=6000 --config-dir=~/.restapi - # Default address is 127.0.0.1:5000, default config directory is - # /aiida/restapi/common - # - # Start the app by sliding the argvs to flaskrun, choose to take as an - # argument also whether to parse the aiida profile or not (in verdi - # restapi this would not be the case) - - import sys - from aiida.restapi.api import AiidaApi, App - - # Or, equivalently, (useful starting point for derived apps) - # import the app object and the Api class that you want to combine. - - run_api(App, AiidaApi, *sys.argv[1:], parse_aiida_profile=True, hookup=True, catch_internal_server=True) diff --git a/docs/source/verdi/verdi_user_guide.rst b/docs/source/verdi/verdi_user_guide.rst index db5e0b892c..6757486fba 100644 --- a/docs/source/verdi/verdi_user_guide.rst +++ b/docs/source/verdi/verdi_user_guide.rst @@ -687,13 +687,18 @@ Below is a list with all available subcommands. Example Usage: - verdi -p restapi --host 127.0.0.5 --port 6789 --config-dir + verdi -p restapi --hostname 127.0.0.5 --port 6789 --config-dir + --debug --wsgi-profile --hookup Options: - -H, --host TEXT the hostname to use - -p, --port INTEGER the port to use - -c, --config-dir PATH the path of the configuration directory - --help Show this message and exit. + -H, --hostname TEXT Hostname. + -P, --port INTEGER Port number. + -c, --config-dir PATH the path of the configuration directory + --debug run app in debug mode + --wsgi-profile to use WSGI profiler middleware for finding + bottlenecks in web application + --hookup / --no-hookup to hookup app + --help Show this message and exit. .. _verdi_run: