|
| 1 | +#! /usr/bin/env python2 |
| 2 | +"""SQL as a Service""" |
| 3 | +from ast import literal_eval |
| 4 | +from functools import partial |
| 5 | +from json import loads |
| 6 | +from os import environ as env |
| 7 | +from traceback import format_exc |
| 8 | + |
| 9 | +from flask import Flask, request, jsonify |
| 10 | +from sqlalchemy.exc import ProgrammingError, OperationalError |
| 11 | +from pynamodb.exceptions import ( |
| 12 | + PynamoDBConnectionError, |
| 13 | + QueryError, |
| 14 | + GetError, |
| 15 | + DeleteError, |
| 16 | + DoesNotExist |
| 17 | +) |
| 18 | + |
| 19 | +from engine import SqlClient |
| 20 | +from jinja2 import Environment |
| 21 | +from models import Backend |
| 22 | +from middleware import list_routes |
| 23 | +from utils import decrypt |
| 24 | + |
| 25 | +api = Flask(__name__) |
| 26 | + |
| 27 | +def raise_if_not_exists(model): |
| 28 | + if not model.exists(): |
| 29 | + raise DoesNotExist() |
| 30 | + |
| 31 | +@api.errorhandler(Exception) |
| 32 | +def exception_handler(error): |
| 33 | + # type: (Exception) -> Exception |
| 34 | + """Show uncaught exceptions. |
| 35 | +
|
| 36 | + Args: |
| 37 | + error |
| 38 | +
|
| 39 | + Raises: |
| 40 | + Exception |
| 41 | + """ |
| 42 | + raise Exception(format_exc()) |
| 43 | + |
| 44 | +@api.route('/') |
| 45 | +def list_api_routes(): |
| 46 | + """List all endpoints""" |
| 47 | + return jsonify(list_routes(api)) |
| 48 | + |
| 49 | +@api.route('/backend/list') |
| 50 | +def get_backends(): |
| 51 | + raise_if_not_exists(Backend) |
| 52 | + return ''.join({item.name: |
| 53 | + item.description |
| 54 | + for item in Backend.scan()}) |
| 55 | + |
| 56 | +@api.route('/register/<backend>', methods=['POST']) |
| 57 | +def register_backend(backend): |
| 58 | + """See client.py to generate new backends.""" |
| 59 | + |
| 60 | + if not Backend.exists(): |
| 61 | + print('Creating table Backend for item: %s' % backend) |
| 62 | + Backend.create_table(wait=True) |
| 63 | + |
| 64 | + json = request.json |
| 65 | + credentials = json['credentials'] |
| 66 | + description = json['description'] |
| 67 | + |
| 68 | + new_backend = Backend( |
| 69 | + name=backend, |
| 70 | + description=description, |
| 71 | + credentials=credentials, |
| 72 | + ) |
| 73 | + new_backend.save() |
| 74 | + return jsonify("""{backend}: {description} has been registered. |
| 75 | + Run ./scripts/sqlcmd to run SQL |
| 76 | + """.format(backend=backend, description=description) |
| 77 | + ) |
| 78 | + |
| 79 | + |
| 80 | +@api.route('/backend/<backend>', methods=['DELETE']) |
| 81 | +def delete_backend(backend): |
| 82 | + raise_if_not_exists(Backend) |
| 83 | + items = Backend.query('name', name__eq=backend) |
| 84 | + for item in items: |
| 85 | + try: |
| 86 | + item.delete() |
| 87 | + except DeleteError as err: |
| 88 | + return err |
| 89 | + return jsonify("%s has been deleted" % backend) |
| 90 | + |
| 91 | + |
| 92 | +@api.route('/view/<backend>') |
| 93 | +def view_sql(backend): |
| 94 | + """View data from database. |
| 95 | +
|
| 96 | + By default, thise route runs in autocommit false. |
| 97 | +
|
| 98 | + Args: |
| 99 | + sql: Valid sql for your backend |
| 100 | + sql_params: Optional jinja2 params |
| 101 | +
|
| 102 | + Returns: |
| 103 | + json |
| 104 | + """ |
| 105 | + key = request.headers['x-api-key'] |
| 106 | + args = request.args |
| 107 | + sql = args['sql'] |
| 108 | + sql_params = args.get('sql_params', '{}') |
| 109 | + rendered_sql = Environment().from_string( |
| 110 | + sql).render(dict(**literal_eval(sql_params))) |
| 111 | + raise_if_not_exists(Backend) |
| 112 | + backend_results = {item.name: item.credentials |
| 113 | + for item in Backend.query('name', name__eq=backend)} |
| 114 | + credentials = backend_results[backend] |
| 115 | + |
| 116 | + backend_client = SqlClient(decrypt(credentials, key=key), autocommit=False) |
| 117 | + viewer = backend_client.sql_viewer() |
| 118 | + try: |
| 119 | + results = viewer(rendered_sql) |
| 120 | + except ProgrammingError as err: |
| 121 | + return str(err), 400 |
| 122 | + except OperationalError as err: |
| 123 | + return str(err), 400 |
| 124 | + except Exception as err: |
| 125 | + return str(err), 400 |
| 126 | + return jsonify(results.to_json(orient='records')) |
| 127 | + |
| 128 | + |
| 129 | +@api.route('/execute/<backend>', methods=['POST']) |
| 130 | +def execute_sql(backend): |
| 131 | + """Modify data from the database. |
| 132 | +
|
| 133 | + Args: |
| 134 | + sql: valid sql for your backend |
| 135 | + sql_params: Optional jinja2 params |
| 136 | +
|
| 137 | + Returns: |
| 138 | + json |
| 139 | + """ |
| 140 | + key = request.headers['x-api-key'] |
| 141 | + args = request.json |
| 142 | + sql = args['sql'] |
| 143 | + sql_params = args.get('sql_params', '{}') |
| 144 | + rendered_sql = Environment().from_string( |
| 145 | + sql).render(dict(**literal_eval(sql_params))) |
| 146 | + autocommit = args.get('autocommit', False) |
| 147 | + raise_if_not_exists(Backend) |
| 148 | + backend_results = {item.name: item.credentials |
| 149 | + for item in Backend.query('name', name__eq=backend)} |
| 150 | + credentials = backend_results[backend] |
| 151 | + backend_client = SqlClient(decrypt(credentials, key=key), autocommit) |
| 152 | + doer = backend_client.sql_doer() |
| 153 | + try: |
| 154 | + results = doer(rendered_sql) |
| 155 | + except OperationalError as err: |
| 156 | + return str(err), 400 |
| 157 | + except ProgrammingError as err: |
| 158 | + return str(err), 400 |
| 159 | + except Exception as err: |
| 160 | + return str(err), 400 |
| 161 | + return jsonify(str("%s row(s) affected." % results.rowcount)) |
| 162 | + |
| 163 | + |
| 164 | +if __name__ == '__main__': |
| 165 | + DEBUG = False if env['STAGE'] == 'prod' else True |
| 166 | + api.run(debug=DEBUG, port=5001) |
0 commit comments