diff options
| author | 2018-02-13 13:11:57 +0100 | |
|---|---|---|
| committer | 2018-02-13 12:11:57 +0000 | |
| commit | f92946e12f9acfa394c04a140a2a4d025a5fe3ca (patch) | |
| tree | a74a0874c842952899f3d10ff2df8397fb89560d /pysite | |
| parent | Websocket echo test (diff) | |
RethinkDB API Views #yqhg
* Refactoring the database implementation into a class of its own.
* Refactoring the database implementation into a class of its own.
* healthcheck should belong to the API.
* dynamic subdomain loading, setting up basic handling for staff.pythondiscord.com, and started on a TagView for a bot tag feature.
* Oops, forgot to fix some merges.
* Some quality of life updates - default values for env variables that aren't secret, and starting to get through the tag view.
* Refactoring the database implementation into a class of its own.
* healthcheck should belong to the API.
* dynamic subdomain loading, setting up basic handling for staff.pythondiscord.com, and started on a TagView for a bot tag feature.
* Oops, forgot to fix some merges.
* Some quality of life updates - default values for env variables that aren't secret, and starting to get through the tag view.
* API validation added to the APIView class, TagView should be finished as well.
* super important commit you guys
* fixed a bug with the RethinkDB class where host and port attributes were accessed before being created
* Fixed my editor now you guys
* Fixing up some of the problems brought up in gdude's review
* Handling GET requests with param indata and POST with JSON. Fixed error handling to use the constants and the baseclass self.error().
* Get API-key from headers, context manage the db assignment, and default env var values
* Changed API-KEY header to X_API_Key. Added a default for secret key. it should no longer be necessary with environment variables to run this system locally.
* Changing back the nav to have relative paths
* Why am I like this
Diffstat (limited to '')
| -rw-r--r-- | pysite/base_route.py | 21 | ||||
| -rw-r--r-- | pysite/constants.py | 3 | ||||
| -rw-r--r-- | pysite/database.py | 43 | ||||
| -rw-r--r-- | pysite/route_manager.py | 76 | ||||
| -rw-r--r-- | pysite/views/__init__.py | 0 | ||||
| -rw-r--r-- | pysite/views/api/bot/__init__.py | 0 | ||||
| -rw-r--r-- | pysite/views/api/bot/tag.py | 66 | ||||
| -rw-r--r-- | pysite/views/api/healthcheck.py | 4 | ||||
| -rw-r--r-- | pysite/views/staff/__init__.py | 0 | ||||
| -rw-r--r-- | pysite/views/staff/index.py | 10 | 
10 files changed, 167 insertions, 56 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py index a3e8615b..c705a350 100644 --- a/pysite/base_route.py +++ b/pysite/base_route.py @@ -1,4 +1,8 @@  # coding=utf-8 +import os +import random +import string +  from flask import Blueprint, jsonify, render_template  from flask.views import MethodView @@ -27,7 +31,17 @@ class RouteView(BaseView):  class APIView(RouteView): +    def validate_key(self, api_key: str): +        """ Placeholder! """ +        return api_key == os.environ("API_KEY") + +    def generate_api_key(self): +        """ Generate a random string of n characters. """ +        pool = random.choices(string.ascii_letters + string.digits, k=32) +        return "".join(pool) +      def error(self, error_code: ErrorCodes): +          data = {              "error_code": error_code.value,              "error_message": "Unknown error" @@ -40,7 +54,12 @@ class APIView(RouteView):              http_code = 404          elif error_code is ErrorCodes.unauthorized:              data["error_message"] = "Unauthorized" -            http_code = 403 +            http_code = 401 +        elif error_code is ErrorCodes.invalid_api_key: +            data["error_message"] = "Invalid API-key" +            http_code = 401 +        elif error_code is ErrorCodes.missing_parameters: +            data["error_message"] = "Not all required parameters were provided"          response = jsonify(data)          response.status_code = http_code diff --git a/pysite/constants.py b/pysite/constants.py index e5a283a7..32d7fd14 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -1,5 +1,4 @@  # coding=utf-8 -__author__ = "Gareth Coles"  from enum import IntEnum @@ -7,3 +6,5 @@ from enum import IntEnum  class ErrorCodes(IntEnum):      unknown_route = 0      unauthorized = 1 +    invalid_api_key = 2 +    missing_parameters = 3 diff --git a/pysite/database.py b/pysite/database.py new file mode 100644 index 00000000..75f01378 --- /dev/null +++ b/pysite/database.py @@ -0,0 +1,43 @@ +# coding=utf-8 + +import os + +from flask import abort + +import rethinkdb + + +class RethinkDB: + +    def __init__(self, loop_type: str = "gevent"): +        self.host = os.environ.get("RETHINKDB_HOST", "127.0.0.1") +        self.port = os.environ.get("RETHINKDB_PORT", "28016") +        self.database = os.environ.get("RETHINKDB_DATABASE", "pythondiscord") +        self.conn = None + +        rethinkdb.set_loop_type(loop_type) + +        with self.get_connection(connect_database=False) as conn: +            try: +                rethinkdb.db_create(self.database).run(conn) +                print(f"Database created: {self.database}") +            except rethinkdb.RqlRuntimeError: +                print(f"Database found: {self.database}") + +    def get_connection(self, connect_database: bool = True): +        if connect_database: +            return rethinkdb.connect(host=self.host, port=self.port, db=self.database) +        else: +            return rethinkdb.connect(host=self.host, port=self.port) + +    def before_request(self): +        try: +            self.conn = self.get_connection() +        except rethinkdb.RqlDriverError: +            abort(503, "Database connection could not be established.") + +    def teardown_request(self, _): +        try: +            self.conn.close() +        except AttributeError: +            pass diff --git a/pysite/route_manager.py b/pysite/route_manager.py index d7cf0fa1..6f973767 100644 --- a/pysite/route_manager.py +++ b/pysite/route_manager.py @@ -3,16 +3,10 @@ import importlib  import inspect  import os -from flask import Blueprint, Flask, abort, g - -import rethinkdb +from flask import Blueprint, Flask, g  from pysite.base_route import APIView, BaseView, ErrorView, RouteView - -DB_HOST = os.environ.get("RETHINKDB_HOST") -DB_PORT = os.environ.get("RETHINKDB_PORT") -DB_DATABASE = os.environ.get("RETHINKDB_DATABASE") -DB_TABLE = os.environ.get("RETHINKDB_TABLE") +from pysite.database import RethinkDB  TEMPLATES_PATH = "../templates"  STATIC_PATH = "../static" @@ -20,29 +14,42 @@ STATIC_PATH = "../static"  class RouteManager:      def __init__(self): + +        # Set up the app and the database          self.app = Flask(              __name__, template_folder=TEMPLATES_PATH, static_folder=STATIC_PATH, static_url_path="/static",          ) -        self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY") -        self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "localhost") +        self.db = RethinkDB() +        self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY", "super_secret") +        self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.com:8080") +        self.app.before_request(self.db.before_request) +        self.app.teardown_request(self.db.teardown_request) -        self.main_blueprint = Blueprint("main", __name__) +        # Store the database in the Flask global context +        with self.app.app_context(): +            g.db = self.db  # type: RethinkDB +        # Load the main blueprint +        self.main_blueprint = Blueprint("main", __name__)          print(f"Loading Blueprint: {self.main_blueprint.name}")          self.load_views(self.main_blueprint, "pysite/views/main")          self.app.register_blueprint(self.main_blueprint)          print("") -        self.api_blueprint = Blueprint("api", __name__, subdomain="api") +        # Load the subdomains +        self.subdomains = ['api', 'staff'] -        print(f"Loading Blueprint: {self.api_blueprint.name}") -        self.load_views(self.api_blueprint, "pysite/views/api") -        self.app.register_blueprint(self.api_blueprint) -        print("") +        for sub in self.subdomains: +            self.sub_blueprint = Blueprint(sub, __name__, subdomain=sub) + +            print(f"Loading Blueprint: {self.sub_blueprint.name}") +            self.load_views(self.sub_blueprint, f"pysite/views/{sub}") +            self.app.register_blueprint(self.sub_blueprint) +            print("")      def run(self):          self.app.run( -            port=int(os.environ.get("WEBPAGE_PORT")), debug="FLASK_DEBUG" in os.environ +            port=int(os.environ.get("WEBPAGE_PORT", 8080)), debug="FLASK_DEBUG" in os.environ          )      def load_views(self, blueprint, location="pysite/views"): @@ -66,38 +73,3 @@ class RouteManager:                      ):                          cls.setup(blueprint)                          print(f">> View loaded: {cls.name: <15} ({module.__name__}.{cls_name})") - -    def setup_db(self): -        connection = self.get_db_connection(connect_database=False) - -        try: -            rethinkdb.db_create(DB_DATABASE).run(connection) -            rethinkdb.db(DB_DATABASE).table_create(DB_TABLE).run(connection) -            print("Database created") -        except rethinkdb.RqlRuntimeError: -            print("Database found") -        finally: -            connection.close() - -        self.app.before_request(self.db_before_request) -        self.app.teardown_request(self.db_teardown_request) - -    def get_db_connection(self, connect_database=True): -        if connect_database: -            return rethinkdb.connect(host=DB_HOST, port=DB_PORT, db=DB_DATABASE) -        else: -            return rethinkdb.connect(host=DB_HOST, port=DB_PORT) - -    def db_before_request(self): -        try: -            # g is the Flask global context object -            g.rdb_conn = self.get_db_connection() -        except rethinkdb.RqlDriverError: -            abort(503, "Database connection could be established.") - -    def db_teardown_request(self, _): -        try: -            # g is the Flask global context object -            g.rdb_conn.close() -        except AttributeError: -            pass diff --git a/pysite/views/__init__.py b/pysite/views/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/views/__init__.py diff --git a/pysite/views/api/bot/__init__.py b/pysite/views/api/bot/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/views/api/bot/__init__.py diff --git a/pysite/views/api/bot/tag.py b/pysite/views/api/bot/tag.py new file mode 100644 index 00000000..acbdbc0c --- /dev/null +++ b/pysite/views/api/bot/tag.py @@ -0,0 +1,66 @@ +# coding=utf-8 + +from flask import g, jsonify, request + +import rethinkdb + +from pysite.base_route import APIView +from pysite.constants import ErrorCodes + + +class TagView(APIView): +    path = '/tag' +    name = 'tag' +    table = 'tag' + +    def __init__(self): +        # make sure the table exists +        with g.db.get_connection() as conn: +            try: +                rethinkdb.db(g.db.database).table_create(self.table, {'primary_key': 'tag_name'}).run(conn) +            except rethinkdb.RqlRuntimeError: +                print(f'Table {self.table} exists') + +    def get(self): +        """ +        Indata must be provided as params, +        API key must be provided as header +        """ +        rdb = rethinkdb.table(self.table) +        api_key = request.headers.get('X-API-Key') +        tag_name = request.args.get('tag_name') + +        if self.validate_key(api_key): +            if tag_name: +                data = rdb.get(tag_name).run(g.db.conn) +                data = dict(data) if data else {} +            else: +                data = rdb.pluck('tag_name').run(g.db.conn) +                data = list(data) if data else [] +        else: +            self.error(ErrorCodes.invalid_api_key) + +        return jsonify(data) + +    def post(self): +        """ Indata must be provided as JSON. """ +        rdb = rethinkdb.table(self.table) +        indata = request.get_json() +        tag_name = indata.get('tag_name') +        tag_content = indata.get('tag_content') +        tag_category = indata.get('tag_category') +        api_key = indata.get('api_key') + +        if self.validate_key(api_key): +            if tag_name and tag_content: +                rdb.insert({ +                    'tag_name': tag_name, +                    'tag_content': tag_content, +                    'tag_category': tag_category +                }).run(g.db.conn) +            else: +                self.error(ErrorCodes.missing_parameters) +        else: +            self.error(ErrorCodes.invalid_api_key) + +        return jsonify({'success': True}) diff --git a/pysite/views/api/healthcheck.py b/pysite/views/api/healthcheck.py index 9d1f681a..2ff5dfb0 100644 --- a/pysite/views/api/healthcheck.py +++ b/pysite/views/api/healthcheck.py @@ -1,10 +1,10 @@  # coding=utf-8  from flask import jsonify -from pysite.base_route import RouteView +from pysite.base_route import APIView -class IndexView(RouteView): +class HealthCheckView(APIView):      path = "/healthcheck"      name = "healthcheck" diff --git a/pysite/views/staff/__init__.py b/pysite/views/staff/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/views/staff/__init__.py diff --git a/pysite/views/staff/index.py b/pysite/views/staff/index.py new file mode 100644 index 00000000..c23af392 --- /dev/null +++ b/pysite/views/staff/index.py @@ -0,0 +1,10 @@ +# coding=utf-8 +from pysite.base_route import RouteView + + +class StaffView(RouteView): +    path = "/" +    name = "staff" + +    def get(self): +        return self.render("staff.html")  |