diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-05-15 13:09:06 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-05-18 15:46:40 +0200 |
| commit | 2281d3265423d01e60f8cc088de5a5730bb8a910 (patch) | |
| tree | 8dc81338cfd30845717f1b9025176d26c82fe930 /opendc-web/opendc-web-api/app.py | |
| parent | 05d2318538eba71ac0555dc5ec146499d9cb0592 (diff) | |
api: Migrate to Flask Restful
This change updates the API to use Flask Restful instead of our own
in-house REST library. This change reduces the maintenance effort and
allows us to drastically simplify the API implementation needed for the
OpenDC v2 API.
Diffstat (limited to 'opendc-web/opendc-web-api/app.py')
| -rwxr-xr-x | opendc-web/opendc-web-api/app.py | 179 |
1 files changed, 67 insertions, 112 deletions
diff --git a/opendc-web/opendc-web-api/app.py b/opendc-web/opendc-web-api/app.py index ee4b3d32..5041457f 100755 --- a/opendc-web/opendc-web-api/app.py +++ b/opendc-web/opendc-web-api/app.py @@ -1,25 +1,35 @@ #!/usr/bin/env python3 -import json import os -import sys -import traceback from dotenv import load_dotenv -from flask import Flask, request, jsonify +from flask import Flask, jsonify from flask_compress import Compress from flask_cors import CORS +from flask_restful import Api +from marshmallow import ValidationError -from opendc.util import rest, path_parser, database -from opendc.util.auth import AuthError, AuthManager, AsymmetricJwtAlgorithm -from opendc.util.exceptions import AuthorizationTokenError, RequestInitializationError -from opendc.util.json import JSONEncoder +from opendc.api.portfolios import Portfolio, PortfolioScenarios +from opendc.api.prefabs import Prefab, PrefabList +from opendc.api.projects import ProjectList, Project, ProjectTopologies, ProjectPortfolios +from opendc.api.scenarios import Scenario +from opendc.api.schedulers import SchedulerList +from opendc.api.topologies import Topology +from opendc.api.traces import TraceList, Trace +from opendc.auth import AuthError +from opendc.util import JSONEncoder + +# Load environmental variables from dotenv file load_dotenv() -TEST_MODE = "OPENDC_FLASK_TESTING" in os.environ -# Setup Sentry if DSN is specified -if 'SENTRY_DSN' in os.environ: +def setup_sentry(): + """ + Setup the Sentry integration for Flask if a DSN is supplied via the environmental variables. + """ + if 'SENTRY_DSN' not in os.environ: + return + import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration @@ -28,119 +38,64 @@ if 'SENTRY_DSN' in os.environ: traces_sample_rate=0.1 ) -# Set up database if not testing -if not TEST_MODE: - database.DB.initialize_database( - user=os.environ['OPENDC_DB_USERNAME'], - password=os.environ['OPENDC_DB_PASSWORD'], - database=os.environ['OPENDC_DB'], - host=os.environ.get('OPENDC_DB_HOST', 'localhost')) - -# Set up the core app -app = Flask("opendc") -app.testing = TEST_MODE -app.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] -app.json_encoder = JSONEncoder - -# Set up CORS support -CORS(app) - -compress = Compress() -compress.init_app(app) - -auth = AuthManager(AsymmetricJwtAlgorithm(jwks_url=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json"), - issuer=f"https://{os.environ['AUTH0_DOMAIN']}/", audience=os.environ['AUTH0_AUDIENCE']) - -API_VERSIONS = {'v2'} - - -@app.errorhandler(AuthError) -def handle_auth_error(ex): - response = jsonify(ex.error) - response.status_code = ex.status_code - return response - - -@app.route('/<string:version>/<path:endpoint_path>', methods=['GET', 'POST', 'PUT', 'DELETE']) -@auth.require -def api_call(version, endpoint_path): - """Call an API endpoint directly over HTTP.""" - - # Check whether given version is valid - if version not in API_VERSIONS: - return jsonify(error='API version not found'), 404 - - # Get path and parameters - (path, path_parameters) = path_parser.parse(version, endpoint_path) - - query_parameters = request.args.to_dict() - for param in query_parameters: - try: - query_parameters[param] = int(query_parameters[param]) - except: - pass - - try: - body_parameters = json.loads(request.get_data()) - except: - body_parameters = {} - - # Create and call request - (req, response) = _process_message({ - 'id': 0, - 'method': request.method, - 'parameters': { - 'body': body_parameters, - 'path': path_parameters, - 'query': query_parameters - }, - 'path': path, - 'token': request.headers.get('auth-token') - }) - print( - f'HTTP:\t{req.method} to `/{req.path}` resulted in {response.status["code"]}: {response.status["description"]}') - sys.stdout.flush() +def setup_api(app): + """ + Setup the API interface. + """ + api = Api(app) + # Map to ('string', 'ObjectId') passing type and format + api.add_resource(ProjectList, '/projects/') + api.add_resource(Project, '/projects/<string:project_id>') + api.add_resource(ProjectTopologies, '/projects/<string:project_id>/topologies') + api.add_resource(ProjectPortfolios, '/projects/<string:project_id>/portfolios') + api.add_resource(Topology, '/topologies/<string:topology_id>') + api.add_resource(PrefabList, '/prefabs/') + api.add_resource(Prefab, '/prefabs/<string:prefab_id>') + api.add_resource(Portfolio, '/portfolios/<string:portfolio_id>') + api.add_resource(PortfolioScenarios, '/portfolios/<string:portfolio_id>/scenarios') + api.add_resource(Scenario, '/scenarios/<string:scenario_id>') + api.add_resource(TraceList, '/traces/') + api.add_resource(Trace, '/traces/<string:trace_id>') + api.add_resource(SchedulerList, '/schedulers/') - flask_response = jsonify(json.loads(response.to_JSON())) - flask_response.status_code = response.status['code'] - return flask_response + @app.errorhandler(AuthError) + def handle_auth_error(ex): + response = jsonify(ex.error) + response.status_code = ex.status_code + return response + @app.errorhandler(ValidationError) + def handle_validation_error(ex): + return {'message': 'Input validation failed', 'errors': ex.messages}, 400 -def _process_message(message): - """Process a request message and return the response.""" + return api - try: - req = rest.Request(message) - res = req.process() - return req, res +def create_app(testing=False): + app = Flask(__name__) + app.config['TESTING'] = testing + app.config['SECRET_KEY'] = os.environ['OPENDC_FLASK_SECRET'] + app.config['RESTFUL_JSON'] = {'cls': JSONEncoder} + app.json_encoder = JSONEncoder - except AuthorizationTokenError: - res = rest.Response(401, 'Authorization error') - res.id = message['id'] + # Setup Sentry if DSN is specified + setup_sentry() - except RequestInitializationError as e: - res = rest.Response(400, str(e)) - res.id = message['id'] + # Set up CORS support + CORS(app) - if not 'method' in message: - message['method'] = 'UNSPECIFIED' - if not 'path' in message: - message['path'] = 'UNSPECIFIED' + # Setup compression + compress = Compress() + compress.init_app(app) - except Exception: - res = rest.Response(500, 'Internal server error') - if 'id' in message: - res.id = message['id'] - traceback.print_exc() + # Setup API + setup_api(app) - req = rest.Request() - req.method = message['method'] - req.path = message['path'] + return app - return req, res +application = create_app(testing="OPENDC_FLASK_TESTING" in os.environ) if __name__ == '__main__': - app.run() + application.run() |
