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

changes in verdi restapi command (#2384) #2853

Merged
merged 1 commit into from
May 9, 2019
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
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same goes for the default_config no? As in, this should just be config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected.

config=config_dir,
debug=debug,
wsgi_profile=wsgi_profile,
hookup=hookup,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that parse_aiida_profile is still being passed, but I don't think it is being used in the run_api function. If true, please remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

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