diff --git a/bin/fdbk-server b/bin/fdbk-server index 502bae9..6254398 100644 --- a/bin/fdbk-server +++ b/bin/fdbk-server @@ -1,19 +1,23 @@ #!/usr/bin/env python3 -import argparse -import json import logging import sys +try: + from flask_cors import CORS + cors_supported = True +except ImportError: + cors_supported = False + from fdbk.server import generate_app +from fdbk.utils import get_connection_argparser from fdbk import __version__ def _generate_parser(): - parser = argparse.ArgumentParser() - parser.add_argument("-c","--config-file", - help="configuration file path", - default="", - type=str) + parser = get_connection_argparser() + parser.add_argument("--cors", + help="allow cors requests", + action="store_true") parser.add_argument("--host", help="hosts to serve to (default = 0.0.0.0)", default="0.0.0.0", @@ -22,26 +26,24 @@ def _generate_parser(): help="port to serve from (default = 8080)", default=8080, type=int) - parser.add_argument("--no-front", - help="Disable serving CWD", - action="store_true") parser.add_argument("-v", "--version", help="Print package version", action="store_true") return parser -_args = _generate_parser().parse_args() +args = _generate_parser().parse_args() -if _args.version: +if args.version: print(f"fdbk {__version__}") sys.exit() -_serve_cwd = not _args.no_front -if not _args.config_file: - _config = None -else: - with open(_args.config_file, "r") as f: - _config = json.load(f) - _config["ServeCWD"] = _serve_cwd +if args.cors and not cors_supported: + print("To enable CORS, install flask_cors.") + sys.exit() + +app = generate_app(args.db_connection, args.db_parameters, log_level=logging.INFO) + +if args.cors and cors_supported: + CORS(app) -generate_app(config=_config, serve_cwd=_serve_cwd, log_level=logging.INFO).run(use_reloader=True, host=_args.host, port=_args.port, threaded=True) +app.run(use_reloader=True, host=args.host, port=args.port, threaded=True) diff --git a/fdbk/server/_server.py b/fdbk/server/_server.py index 5eb21c1..5861487 100644 --- a/fdbk/server/_server.py +++ b/fdbk/server/_server.py @@ -1,72 +1,46 @@ -import json import logging -import os from dateutil.parser import isoparse -from flask import Flask, request, send_from_directory +from flask import Flask, jsonify, request -from fdbk import utils +from fdbk.utils import create_db_connection from ._server_handlers import ServerHandlers -def generate_app(config=None, serve_cwd=True, log_level=logging.WARN): - default_config = { - "DBConnection": "DictConnection", - "DBParameters": [], - "AllowedActions": [ - "add_data", - "add_topic", - "get_comparison", - "get_data", - "get_latest", - "get_overview", - "get_summary", - "get_topics", - "get_topic" - ], - "ServeCWD": serve_cwd - } - - if not config: - config = default_config - elif isinstance(config, str): - with open(config, "r") as f: - config = json.load(f) - elif not isinstance(config, dict): - raise ValueError("Input configuration not recognized.") - - static_folder = os.path.join( - os.getcwd(), "static") if config["ServeCWD"] else None - app = Flask(__name__, static_folder=static_folder) - - db_connection = utils.create_db_connection( - config["DBConnection"], config["DBParameters"]) - - handlers = ServerHandlers(db_connection, config) +def generate_app(db_plugin, db_parameters, log_level=logging.WARN): + app = Flask(__name__) + + db_connection = create_db_connection( + db_plugin, db_parameters) + + handlers = ServerHandlers(db_connection) app.logger.setLevel(log_level) # pylint: disable=no-member app.logger.info('Created "' + # pylint: disable=no-member - config["DBConnection"] + + db_plugin + '" with parameters: ' + - str(config["DBParameters"])) - - # API + str(db_parameters)) - if config["ServeCWD"]: - @app.route('/') - def index(): - return send_from_directory(os.getcwd(), 'index.html') + def _jsonify(response): + data, code = response + return jsonify(data), code @app.route('/topics', methods=['GET', 'POST']) def topics(): if request.method == 'GET': - return handlers.get_topics(request.args.get('type')) + return _jsonify(handlers.get_topics(request.args.get('type'))) if request.method == 'POST': - return handlers.add_topic() + try: + json_in = request.get_json() + except BaseException: + return jsonify({ + "error": "No topic data provided in request" + }), 404 + return _jsonify(handlers.add_topic(json_in)) @app.route('/topics/', methods=['GET']) def topics_get(topic_id): - return handlers.get_topic(topic_id) + return _jsonify(handlers.get_topic(topic_id)) def _parse_param(param, parser): try: @@ -77,32 +51,38 @@ def _parse_param(param, parser): @app.route('/topics//data', methods=['GET', 'POST']) def data(topic_id): if request.method == 'GET': - return handlers.get_data( + return _jsonify(handlers.get_data( topic_id, _parse_param(request.args.get('since'), isoparse), _parse_param(request.args.get('until'), isoparse), - _parse_param(request.args.get('limit'), int)) + _parse_param(request.args.get('limit'), int))) if request.method == 'POST': - return handlers.add_data(topic_id) + try: + json_in = request.get_json() + except BaseException: + return jsonify({ + "error": "No topic data provided in request" + }), 404 + return _jsonify(handlers.add_data(topic_id, json_in)) @app.route('/topics//data/latest', methods=['GET', 'POST']) def latest(topic_id): - return handlers.get_latest(topic_id) + return _jsonify(handlers.get_latest(topic_id)) @app.route('/topics//summary', methods=['GET']) def summary(topic_id): - return handlers.get_summary(topic_id) + return _jsonify(handlers.get_summary(topic_id)) @app.route('/comparison/', methods=['GET']) def comparison(topic_ids): - return handlers.get_comparison(topic_ids) + return _jsonify(handlers.get_comparison(topic_ids)) @app.route('/comparison', methods=['GET']) def comparison_all(): - return handlers.get_comparison() + return _jsonify(handlers.get_comparison()) @app.route('/overview', methods=['GET']) def overview(): - return handlers.get_overview() + return _jsonify(handlers.get_overview()) return app diff --git a/fdbk/server/_server_handlers.py b/fdbk/server/_server_handlers.py index f245268..3424a34 100644 --- a/fdbk/server/_server_handlers.py +++ b/fdbk/server/_server_handlers.py @@ -1,157 +1,104 @@ -from flask import jsonify, request - - class ServerHandlers: - def __init__(self, db_connection, config): - self.__db_connection = db_connection - self.__config = config - - self.__invalid_token_json = { - "error": "Token not recognized" - } - - self.__action_not_allowed_json = { - "error": "Action not allowed" - } - - def add_topic(self): - if "add_topic" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 - if "AddTokens" in self.__config and self.__config["AddTokens"] and ( - "token" not in request.args or - request.args["token"] not in self.__config["AddTokens"]): - return jsonify(self.__invalid_token_json), 403 - try: - json_in = request.get_json() - except BaseException: - return jsonify({ - "error": "No topic data provided in request" - }), 404 + def __init__(self, db_connection): + self._db_connection = db_connection + def add_topic(self, json_in): topic = json_in.pop("name", None) type_str = json_in.pop("type", None) if not topic: - return jsonify({ + return { "error": "No 'topic' field in input data" - }), 404 + }, 404 try: - topic_id = self.__db_connection.add_topic( + topic_id = self._db_connection.add_topic( topic, type_str=type_str, **json_in) except KeyError as error: # Field not available in input data - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 except TypeError as error: - return jsonify({ + return { "error": str(error) - }), 400 - return jsonify({ + }, 400 + return { "topic_id": topic_id, "success": "Topic successfully added to DB" - }) - - def add_data(self, topic_id): - if "add_data" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 - if "AddTokens" in self.__config and self.__config["AddTokens"] and ( - "token" not in request.args or - request.args["token"] not in self.__config["AddTokens"]): - return jsonify(self.__invalid_token_json), 403 + }, 200 + def add_data(self, topic_id, json_in): try: - json_in = request.get_json() - except BaseException: - return jsonify({ - "error": "No topic data provided in request" - }), 404 - try: - self.__db_connection.add_data(topic_id, json_in) + self._db_connection.add_data(topic_id, json_in) except KeyError as error: # Topic not defined - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 except ValueError as error: # Fields do not match with topic - return jsonify({ + return { "error": str(error) - }), 400 - return jsonify({ + }, 400 + return { "success": "Data successfully added to DB" - }) + }, 200 def get_topics(self, type_=None): - if "get_topics" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 - return jsonify(self.__db_connection.get_topics(type_)) + return self._db_connection.get_topics(type_), 200 def get_topic(self, topic_id): - if "get_topic" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 try: - topic_json = self.__db_connection.get_topic(topic_id) - return jsonify(topic_json) + topic_json = self._db_connection.get_topic(topic_id) + return topic_json, 200 except KeyError as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 def get_data(self, topic_id, since=None, until=None, limit=None): - if "get_data" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 try: - data = self.__db_connection.get_data(topic_id, since, until, limit) - return jsonify(data) + data = self._db_connection.get_data(topic_id, since, until, limit) + return data, 200 except KeyError as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 def get_latest(self, topic_id): - if "get_latest" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 try: - data = self.__db_connection.get_latest(topic_id) - return jsonify(data) + data = self._db_connection.get_latest(topic_id) + return data, 200 except Exception as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 def get_summary(self, topic_id): - if "get_summary" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 try: - data = self.__db_connection.get_summary(topic_id) - return jsonify(data) + data = self._db_connection.get_summary(topic_id) + return data, 200 except KeyError as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 def get_comparison(self, topic_ids=None): - if "get_comparison" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 - topic_ids_a = topic_ids.split(',') if topic_ids else None try: - data = self.__db_connection.get_comparison(topic_ids_a) - return jsonify(data) + data = self._db_connection.get_comparison(topic_ids_a) + return data, 200 except KeyError as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 def get_overview(self): - if "get_overview" not in self.__config["AllowedActions"]: - return jsonify(self.__action_not_allowed_json), 403 try: - data = self.__db_connection.get_overview() - return jsonify(data) + data = self._db_connection.get_overview() + return data, 200 except KeyError as error: - return jsonify({ + return { "error": str(error) - }), 404 + }, 404 diff --git a/fdbk/utils/_connection.py b/fdbk/utils/_connection.py index b0c25bf..c476b25 100644 --- a/fdbk/utils/_connection.py +++ b/fdbk/utils/_connection.py @@ -1,3 +1,4 @@ +from argparse import ArgumentParser from importlib import import_module BUILT_IN = dict( @@ -18,3 +19,21 @@ def create_db_connection(db_plugin, db_parameters): except Exception as e: raise RuntimeError( "Loading or creating fdbk DB connection failed: " + str(e)) + + +def get_connection_argparser(parser=None): + if not parser: + parser = ArgumentParser() + + parser.add_argument( + "db_parameters", + nargs="*", + type=str, + help="Parameters for fdbk DB connection.") + parser.add_argument( + "--db-connection", + type=str, + default="ClientConnection", + help="fdbk DB connection to use (default=ClientConnection)") + + return parser diff --git a/fdbk/utils/_reporter.py b/fdbk/utils/_reporter.py index 0fa4e1e..c2b5840 100644 --- a/fdbk/utils/_reporter.py +++ b/fdbk/utils/_reporter.py @@ -1,20 +1,13 @@ from argparse import ArgumentParser +from ._connection import get_connection_argparser + def get_reporter_argparser(parser=None): if not parser: parser = ArgumentParser() - parser.add_argument( - "db_parameters", - nargs="*", - type=str, - help="Parameters for fdbk DB connection.") - parser.add_argument( - "--db-connection", - type=str, - default="ClientConnection", - help="fdbk DB connection to use (default=ClientConnection)") + get_connection_argparser(parser) parser.add_argument( "--interval", "-i", diff --git a/tst/test_client_connection.py b/tst/test_client_connection.py index 7ae4589..600e1bf 100644 --- a/tst/test_client_connection.py +++ b/tst/test_client_connection.py @@ -22,7 +22,7 @@ def ok(self): class ClientConnectionTest(TestCase): def setUp(self): - self.__server = generate_app().test_client() + self.__server = generate_app("DictConnection", []).test_client() def mock_requests_get(self, *args, **kwargs): response = self.__server.get(*args, **kwargs) diff --git a/tst/test_utils.py b/tst/test_utils.py index 7487eb8..abdd14d 100644 --- a/tst/test_utils.py +++ b/tst/test_utils.py @@ -1,7 +1,7 @@ from unittest import TestCase from unittest.mock import Mock, patch -from fdbk.utils import get_reporter_argparser +from fdbk.utils import get_connection_argparser, get_reporter_argparser class ReporterArgparserTest(TestCase): def test_interval_and_num_samples_default_to_none(self): @@ -9,4 +9,15 @@ def test_interval_and_num_samples_default_to_none(self): args = parser.parse_args([]) self.assertIsNone(args.interval) - self.assertIsNone(args.num_samples) \ No newline at end of file + self.assertIsNone(args.num_samples) + + def test_parsers_db_connection_details(self): + for parser in (get_connection_argparser(), get_reporter_argparser(), ): + in_ = ["http://localhost:8080"] + args = parser.parse_args(in_) + self.assertEqual(args.db_parameters, in_) + + in_ = ["--db-connection", "fdbk_dynamodb_plugin"] + args = parser.parse_args(in_) + self.assertEqual(args.db_parameters, []) + self.assertEqual(args.db_connection, "fdbk_dynamodb_plugin")