__init__.py 5.13 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Copyright 2018 Markus Scheidgen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an"AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

15
'''
16
17
18
This module comprises the nomad@FAIRDI APIs. Currently there is NOMAD's official api, and
we will soon at the optimade api. The app module also servers documentation, gui, and
alive.
19
'''
20
21
from flask import Flask, Blueprint, jsonify, url_for, abort, request, make_response
from flask_restplus import Api, representations
22
from flask_cors import CORS
23
from werkzeug.exceptions import HTTPException
Markus Scheidgen's avatar
Markus Scheidgen committed
24
from werkzeug.wsgi import DispatcherMiddleware  # pylint: disable=E0611
25
import os.path
26
import random
Markus Scheidgen's avatar
Markus Scheidgen committed
27
from structlog import BoundLogger
28
29
30
import orjson
import collections
from mongoengine.base.datastructures import BaseList
Markus Scheidgen's avatar
Markus Scheidgen committed
31
32

from nomad import config, utils as nomad_utils
33

34
35
36
from .api import blueprint as api_blueprint, api
from .optimade import blueprint as optimade_blueprint, api as optimade
from .docs import blueprint as docs_blueprint
37
from . import common
38

Markus Scheidgen's avatar
Markus Scheidgen committed
39

40
41
# replace the json implementation of flask_restplus
def output_json(data, code, headers=None):
42
    dumped = nomad_utils.dumps(data) + b'\n'
43
44
45
46
47
48
49
50
51
52

    resp = make_response(dumped, code)
    resp.headers.extend(headers or {})
    return resp


api.representation('application/json')(output_json)
optimade.representation('application/json')(output_json)


53
@property  # type: ignore
Markus Scheidgen's avatar
Markus Scheidgen committed
54
def specs_url(self):
55
    '''
Markus Scheidgen's avatar
Markus Scheidgen committed
56
57
58
    Fixes issue where swagger-ui makes a call to swagger.json over HTTP.
    This can ONLY be used on servers that actually use HTTPS.  On servers that use HTTP,
    this code should not be used at all.
59
    '''
60
    return url_for(self.endpoint('specs'), _external=True, _scheme='https')
Markus Scheidgen's avatar
Markus Scheidgen committed
61
62
63
64
65
66


if config.services.https:
    Api.specs_url = specs_url


Markus Scheidgen's avatar
Markus Scheidgen committed
67
app = Flask(__name__)
68
''' The Flask app that serves all APIs. '''
69

70
app.config.APPLICATION_ROOT = common.base_path  # type: ignore
71
72
73
74
app.config.RESTPLUS_MASK_HEADER = False  # type: ignore
app.config.RESTPLUS_MASK_SWAGGER = False  # type: ignore
app.config.SWAGGER_UI_OPERATION_ID = True  # type: ignore
app.config.SWAGGER_UI_REQUEST_DURATION = True  # type: ignore
75

Markus Scheidgen's avatar
Markus Scheidgen committed
76
77
app.config['SECRET_KEY'] = config.services.api_secret

78

Markus Scheidgen's avatar
Markus Scheidgen committed
79
def api_base_path_response(env, resp):
Markus Scheidgen's avatar
Markus Scheidgen committed
80
    resp('200 OK', [('Content-Type', 'text/plain')])
Markus Scheidgen's avatar
Markus Scheidgen committed
81
82
83
84
85
    return [
        ('Development nomad api server. Api is served under %s/.' %
            config.services.api_base_path).encode('utf-8')]


86
app.wsgi_app = DispatcherMiddleware(  # type: ignore
Markus Scheidgen's avatar
Markus Scheidgen committed
87
88
    api_base_path_response, {config.services.api_base_path: app.wsgi_app})

89
90
CORS(app)

91
92
93
app.register_blueprint(api_blueprint, url_prefix='/api')
app.register_blueprint(optimade_blueprint, url_prefix='/optimade')
app.register_blueprint(docs_blueprint, url_prefix='/docs')
94
95


96
97
@app.errorhandler(Exception)
def handle(error: Exception):
98
    status_code = getattr(error, 'code', 500)
Markus Scheidgen's avatar
Markus Scheidgen committed
99
100
    if not isinstance(status_code, int):
        status_code = 500
101
102
103
    if status_code < 100:
        status_code = 500

104
    name = getattr(error, 'name', 'Internal Server Error')
105
    description = getattr(error, 'description', 'No description available')
106
107
108
109
110
111
112
    data = dict(
        code=status_code,
        name=name,
        description=description)
    data.update(getattr(error, 'data', []))
    response = jsonify(data)
    response.status_code = status_code
113
    if status_code == 500:
114
        local_logger = common.logger
Markus Scheidgen's avatar
Markus Scheidgen committed
115
116
        # the logger is created in before_request, if the error was created before that
        # logger can be None
117
118
        if local_logger is None:
            local_logger = nomad_utils.get_logger(__name__)
Markus Scheidgen's avatar
Markus Scheidgen committed
119

Markus Scheidgen's avatar
Markus Scheidgen committed
120
121
122
        # TODO the error seems not to be the actual exception, therefore
        # there might be no stacktrace. Maybe there is a way to get the actual
        # exception/stacktrace
Markus Scheidgen's avatar
Markus Scheidgen committed
123
        local_logger.error('internal server error', error=str(error), exc_info=error)
Markus Scheidgen's avatar
Markus Scheidgen committed
124

125
    return response
126
127


128
@app.route('/alive')
129
def alive():
130
    ''' Simple endpoint to utilize kubernetes liveness/readiness probing. '''
131
132
133
    return "I am, alive!"


134
135
@app.before_request
def before_request():
Markus Scheidgen's avatar
Markus Scheidgen committed
136
    # api logger
137
138
139
140
141
142
143
144
    args = getattr(request, 'view_args')
    if args is None:
        args = {}
    else:
        args = dict(**args)

    args.update(
        name=__name__,
Markus Scheidgen's avatar
Markus Scheidgen committed
145
146
147
        blueprint=str(request.blueprint),
        endpoint=request.endpoint,
        method=request.method,
148
        url=request.url,
Markus Scheidgen's avatar
Markus Scheidgen committed
149
150
151
        json=request.json,
        args=request.args)

152
153
    common.logger = nomad_utils.get_logger(**args)

Markus Scheidgen's avatar
Markus Scheidgen committed
154
    # chaos monkey
155
156
157
    if config.services.api_chaos > 0:
        if random.randint(0, 100) <= config.services.api_chaos:
            abort(random.choice([400, 404, 500]), 'With best wishes from the chaos monkey.')
Markus Scheidgen's avatar
Markus Scheidgen committed
158
159
160
161
162
163
164
165


@app.before_first_request
def setup():
    from nomad import infrastructure

    if not app.config['TESTING']:
        infrastructure.setup()