Skip to content

Commit

Permalink
Changes in the verdi restapi command (#2853)
Browse files Browse the repository at this point in the history
- Updated `verdi restapi` command and its click parameters
- Removed `load_profile` functionality from restapi code as it is
  already handled by the `verdi` command itself
- Removed `__main__` from the restapi code as it is duplicating the
  functionality provided by `verdi restapi`
  • Loading branch information
Snehal Kumbhar authored and sphuber committed May 9, 2019
1 parent 8836a8f commit e9650cc
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 141 deletions.
1 change: 1 addition & 0 deletions aiida/backends/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
35 changes: 35 additions & 0 deletions aiida/backends/tests/cmdline/commands/test_restapi.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion aiida/backends/tests/test_restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions aiida/cmdline/commands/cmd_restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <profile_name> restapi --host 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
verdi -p <profile_name> restapi --hostname 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
--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
Expand Down
147 changes: 23 additions & 124 deletions aiida/restapi/run_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# <profile> --> corresponding profile in .aiida/config.json
# None --> default restapi profile set in <config_dir>/config,py
#
# if aiida_profile is not parsed we assume
#
# default restapi profile set in <config_dir>/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)
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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_path>/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)
15 changes: 10 additions & 5 deletions docs/source/verdi/verdi_user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -687,13 +687,18 @@ Below is a list with all available subcommands.

Example Usage:

verdi -p <profile_name> restapi --host 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
verdi -p <profile_name> restapi --hostname 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
--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:
Expand Down

0 comments on commit e9650cc

Please sign in to comment.