Skip to content

Commit

Permalink
fix interface of run_api for running REST API
Browse files Browse the repository at this point in the history
The current `run_api` interface for running the AiiDA REST API had
unnecessarily many parameters, making it complicated to use in WSGI
scripts, e.g.:

```python
from aiida.restapi import api
from aiida.restapi.run_api import run_api
import aiida.restapi

CONFIG_DIR = os.path.join(os.path.split(
                  os.path.abspath(aiida.restapi.__file__))[0], 'common')

(app, api) = run_api(
    api.App,
    api.AiidaApi,
    hostname="localhost",
    port=5000,
    config=CONFIG_DIR,
    debug=False,
    wsgi_profile=False,
    hookup=False,
    catch_internal_server=False
)
```
While all but the first two parameters are keyword arguments, the code
would actually crash if they are not provided.

In reality, there is no reason to have to specify *any* parameters
whatsoever and one should simply be able to call `run_api()`.
This commit accomplishes this by defining the appropriate default
values.
  • Loading branch information
ltalirz authored and sphuber committed Mar 29, 2020
1 parent 58bace3 commit e8067f5
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 124 deletions.
42 changes: 18 additions & 24 deletions aiida/cmdline/commands/cmd_restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,54 @@
Main advantage of doing this by means of a verdi command is that different
profiles can be selected at hook-up (-p flag).
"""
import os

import click

import aiida.restapi
from aiida.cmdline.commands.cmd_verdi import verdi
from aiida.cmdline.params.options import HOSTNAME, PORT

CONFIG_DIR = os.path.join(os.path.split(os.path.abspath(aiida.restapi.__file__))[0], 'common')
from aiida.restapi.common import config


@verdi.command('restapi')
@HOSTNAME(default='127.0.0.1')
@PORT(default=5000)
@HOSTNAME(default=config.CLI_DEFAULTS['HOST_NAME'])
@PORT(default=config.CLI_DEFAULTS['PORT'])
@click.option(
'-c',
'--config-dir',
type=click.Path(exists=True),
default=CONFIG_DIR,
help='the path of the configuration directory'
default=config.CLI_DEFAULTS['CONFIG_DIR'],
help='Path to the configuration directory'
)
@click.option('--debug', 'debug', is_flag=True, default=False, help='run app in debug mode')
@click.option('--debug', 'debug', is_flag=True, default=config.APP_CONFIG['DEBUG'], help='Enable debugging')
@click.option(
'--wsgi-profile',
'wsgi_profile',
is_flag=True,
default=False,
help='to use WSGI profiler middleware for finding bottlenecks in web application'
default=config.CLI_DEFAULTS['WSGI_PROFILE'],
help='Whether to enable WSGI profiler middleware for finding bottlenecks'
)
@click.option(
'--hookup/--no-hookup',
'hookup',
is_flag=True,
default=config.CLI_DEFAULTS['HOOKUP_APP'],
help='Hookup app to flask server'
)
@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 --hostname 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
--debug --wsgi-profile --hookup
verdi -p <profile_name> restapi --hostname 127.0.0.5 --port 6789
"""
from aiida.restapi.api import App, AiidaApi
from aiida.restapi.run_api import run_api

# Construct parameter dictionary
kwargs = dict(
prog_name='verdi-restapi',
# Invoke the runner
run_api(
hostname=hostname,
port=port,
config=config_dir,
debug=debug,
wsgi_profile=wsgi_profile,
hookup=hookup,
catch_internal_server=True
)

# Invoke the runner
run_api(App, AiidaApi, **kwargs)
63 changes: 20 additions & 43 deletions aiida/restapi/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,26 @@
# For further information please visit http://www.aiida.net #
###########################################################################
"""
Constants used in rest api
Default configuration for the REST API
"""
import os

## Pagination defaults
LIMIT_DEFAULT = 400
PERPAGE_DEFAULT = 20

##Version prefix for all the URLs
PREFIX = '/api/v4'
VERSION = '4.0.1'
"""
Flask app configs.
DEBUG: True/False. enables debug mode N.B.
!!!For production run use ALWAYS False!!!
PROPAGATE_EXCEPTIONS: True/False serve REST exceptions to the client (and not a
generic 500: Internal Server Error exception)
API_CONFIG = {
'LIMIT_DEFAULT': 400, # default records total
'PERPAGE_DEFAULT': 20, # default records per page
'PREFIX': '/api/v4', # prefix for all URLs
'VERSION': '4.0.1',
}

"""
APP_CONFIG = {
'DEBUG': False,
'PROPAGATE_EXCEPTIONS': True,
'DEBUG': False, # use False for production
'PROPAGATE_EXCEPTIONS': True, # serve REST exceptions to client instead of generic 500 internal server error
}
"""
JSON serialization config. Leave this dictionary empty if default Flask
serializer is desired.
Here is a list a all supported fields. If a field is not present in the
dictionary its value is assumed to be 'default'.

DATETIME_FORMAT: allowed values are 'asinput' and 'default'.
SERIALIZER_CONFIG = {'datetime_format': 'default'} # use 'asinput' or 'default'

"""
SERIALIZER_CONFIG = {'datetime_format': 'default'}
"""
Caching configuration
memcached: backend caching system
"""
CACHE_CONFIG = {'CACHE_TYPE': 'memcached'}
CACHING_TIMEOUTS = { #Caching TIMEOUTS (in seconds)
CACHING_TIMEOUTS = { # Caching timeouts in seconds
'nodes': 10,
'users': 10,
'calculations': 10,
Expand All @@ -61,13 +39,12 @@

# IO tree
MAX_TREE_DEPTH = 5
"""
Aiida profile used by the REST api when no profile is specified (ex. by
--aiida-profile flag).
This has to be one of the profiles registered in .aiida/config.json
In case you want to use the default stored in
.aiida/config.json, set this varibale to "default"

"""
DEFAULT_AIIDA_PROFILE = None
CLI_DEFAULTS = {
'HOST_NAME': '127.0.0.1',
'PORT': 5000,
'CONFIG_DIR': os.path.dirname(os.path.abspath(__file__)),
'WSGI_PROFILE': False,
'HOOKUP_APP': True,
'CATCH_INTERNAL_SERVER': False,
}
6 changes: 3 additions & 3 deletions aiida/restapi/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ def get(self):

response = {}

import aiida.restapi.common.config as conf
from aiida.restapi.common.config import API_CONFIG
from aiida import __version__

if resource_type == 'info':
response = {}

# Add Rest API version
api_version = conf.VERSION.split('.')
api_version = API_CONFIG['VERSION'].split('.')
response['API_major_version'] = api_version[0]
response['API_minor_version'] = api_version[1]
response['API_revision_version'] = api_version[2]

# Add Rest API prefix
response['API_prefix'] = conf.PREFIX
response['API_prefix'] = API_CONFIG['PREFIX']

# Add AiiDA version
response['AiiDA_version'] = __version__
Expand Down
90 changes: 43 additions & 47 deletions aiida/restapi/run_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,83 +16,79 @@
import os

from flask_cors import CORS
from .common.config import CLI_DEFAULTS, APP_CONFIG, API_CONFIG
from . import api as api_classes


def run_api(flask_app, flask_api, **kwargs):
def run_api(flask_app=api_classes.App, flask_api=api_classes.AiidaApi, **kwargs):
"""
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
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
hostname: self-explainatory
port: self-explainatory
config: directory containing the config.py file used to
configure the RESTapi
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.
:param flask_app: Class inheriting from flask app class
:type flask_app: :py:class:`flask.Flask`
:param flask_api: flask_restful API class to be used to wrap the app
:type flask_api: :py:class:`flask_restful.Api`
List of valid keyword arguments:
:param hostname: hostname to run app on (only when using built-in server)
:param port: port to run app on (only when using built-in server)
:param config: directory containing the config.py file used to configure the RESTapi
:param catch_internal_server: If true, catch and print all inter server errors
:param debug: enable debugging
:param wsgi_profile: use WSGI profiler middleware for finding bottlenecks in web application
:param hookup: If true, hook up application to built-in server - else just return it
"""
# pylint: disable=too-many-locals

# Unpack parameters
hostname = kwargs['hostname']
port = kwargs['port']
config = kwargs['config']
hostname = kwargs.pop('hostname', CLI_DEFAULTS['HOST_NAME'])
port = kwargs.pop('port', CLI_DEFAULTS['PORT'])
config = kwargs.pop('config', CLI_DEFAULTS['CONFIG_DIR'])

catch_internal_server = kwargs.pop('catch_internal_server', False)
debug = kwargs['debug']
wsgi_profile = kwargs['wsgi_profile']
hookup = kwargs['hookup']
catch_internal_server = kwargs.pop('catch_internal_server', CLI_DEFAULTS['CATCH_INTERNAL_SERVER'])
debug = kwargs.pop('debug', APP_CONFIG['DEBUG'])
wsgi_profile = kwargs.pop('wsgi_profile', CLI_DEFAULTS['WSGI_PROFILE'])
hookup = kwargs.pop('hookup', CLI_DEFAULTS['HOOKUP_APP'])

# Import the right configuration file
if kwargs:
raise ValueError('Unknown keyword arguments: {}'.format(kwargs))

# Import the configuration file
spec = importlib.util.spec_from_file_location(os.path.join(config, 'config'), os.path.join(config, 'config.py'))
confs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(confs)
config_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(config_module)

# Instantiate an app
app_kwargs = dict(catch_internal_server=catch_internal_server)
app = flask_app(__name__, **app_kwargs)
app = flask_app(__name__, catch_internal_server=catch_internal_server)

# Config the app
app.config.update(**confs.APP_CONFIG)
# Apply default configuration
app.config.update(**config_module.APP_CONFIG)

# cors
cors_prefix = os.path.join(confs.PREFIX, '*')
CORS(app, resources={r'' + cors_prefix: {'origins': '*'}})
# Allow cross-origin resource sharing
cors_prefix = r'{}/*'.format(config_module)
CORS(app, resources={cors_prefix: {'origins': '*'}})

# Config the serializer used by the app
if confs.SERIALIZER_CONFIG:
# Configure the serializer
if config_module.SERIALIZER_CONFIG:
from aiida.restapi.common.utils import CustomJSONEncoder
app.json_encoder = CustomJSONEncoder

# If the user selects the profiling option, then we need
# to do a little extra setup
# Set up WSGI profile if requested
if wsgi_profile:
from werkzeug.middleware.profiler import ProfilerMiddleware

app.config['PROFILE'] = True
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])

# Instantiate an Api by associating its app
api_kwargs = dict(PREFIX=confs.PREFIX, PERPAGE_DEFAULT=confs.PERPAGE_DEFAULT, LIMIT_DEFAULT=confs.LIMIT_DEFAULT)
api = flask_api(app, **api_kwargs)
api = flask_api(app, **API_CONFIG)

# Check if the app has to be hooked-up or just returned
if hookup:
print(' * REST API running on http://{}:{}{}'.format(hostname, port, confs.PREFIX))
# Run app through built-in werkzeug server
print(' * REST API running on http://{}:{}{}'.format(hostname, port, API_CONFIG['PREFIX']))
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
# mode, port and host. This can be handled by an external server,
# e.g. apache2, which will set the host and port. This implies that
# the user-defined configuration of the app is ineffective (it only
# affects the internal werkzeug server used by Flask).
# Return the app & api without specifying port/host to be handled by an external server (e.g. apache).
# Some of the user-defined configuration of the app is ineffective (only affects built-in server).
return (app, api)
13 changes: 6 additions & 7 deletions docs/source/verdi/verdi_user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -677,17 +677,16 @@ Below is a list with all available subcommands.

Example Usage:

verdi -p <profile_name> restapi --hostname 127.0.0.5 --port 6789 --config-dir <location of the config.py file>
--debug --wsgi-profile --hookup
verdi -p <profile_name> restapi --hostname 127.0.0.5 --port 6789

Options:
-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
-c, --config-dir PATH Path to the configuration directory
--debug Enable debugging
--wsgi-profile Whether to enable WSGI profiler middleware for
finding bottlenecks
--hookup / --no-hookup Hookup app to flask server
--help Show this message and exit.


Expand Down

0 comments on commit e8067f5

Please sign in to comment.