From bb27efebb9ebb30e85e780758d259bcb72a699c4 Mon Sep 17 00:00:00 2001 From: romangraef Date: Sat, 9 Jun 2018 12:28:18 +0200 Subject: Initial commit --- api/__init__.py | 3 +++ api/v1/db.py | 35 ++++++++++++++++++++++++++++++++ api/v1/server.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ api/v1/util.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app.py | 16 +++++++++++++++ requirements.txt | 3 +++ 6 files changed, 169 insertions(+) create mode 100644 api/__init__.py create mode 100644 api/v1/db.py create mode 100644 api/v1/server.py create mode 100644 api/v1/util.py create mode 100644 app.py create mode 100644 requirements.txt diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..685c125 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,3 @@ +from .v1.server import bp as v1_blueprint + +bp = v1_blueprint diff --git a/api/v1/db.py b/api/v1/db.py new file mode 100644 index 0000000..3d226c5 --- /dev/null +++ b/api/v1/db.py @@ -0,0 +1,35 @@ +from peewee import SqliteDatabase, ForeignKeyField, CharField, Model, IntegerField + +db = SqliteDatabase('datapackmanager.db') + + +class BaseModel(Model): + class Meta: + database = db + + +class User(BaseModel): + name = CharField() + id = IntegerField(primary_key=True) + + +class Category(BaseModel): + name = CharField() + id = IntegerField(primary_key=True) + + +class DataPack(BaseModel): + id = IntegerField(primary_key=True) + name = CharField() + description = CharField(max_length=10000) + category = ForeignKeyField(Category) + author = ForeignKeyField(User) + + +class Version(BaseModel): + name = CharField() + datapack = ForeignKeyField(DataPack) + + +db.create_tables([DataPack, Category, Version, User]) +__all__ = ('DataPack', 'Category', 'Version', 'db') diff --git a/api/v1/server.py b/api/v1/server.py new file mode 100644 index 0000000..96ff8da --- /dev/null +++ b/api/v1/server.py @@ -0,0 +1,51 @@ +from flask import Blueprint, jsonify, abort, make_response, request + +from .db import Category, DataPack +from .util import model_paginator, query_paginator, ConstrainFailed + +bp = Blueprint("api v1", __name__) + + +@bp.errorhandler(ConstrainFailed) +def handler(e: ConstrainFailed): + return make_response(jsonify({ + 'error': { + 'description': e.description, + 'parameter': e.parameter, + 'reason': e.reason, + } + }), 404) + + +@bp.route('/version/') +def version(): + return jsonify({ + 'version': '1.0.0', + 'deprecated': False, + }) + + +@bp.route('/list/categories/') +def list_categories(): + return model_paginator(Category, lambda cat: { + 'name': cat.name, + 'id': cat.id, + }) + + +@bp.route('/list/datapacks') +def list_datapacks(): + category = request.args.get('category', '') + query = DataPack.select() + if category != '': + query = query.join(Category).where(DataPack.category.id == category) + return query_paginator(query, lambda dp: { + 'name': dp.name, + 'id': dp.id, + 'category': dp.category.id, + }) + + +@bp.route('/') +def invalid_path(invalid_path): + abort(404) diff --git a/api/v1/util.py b/api/v1/util.py new file mode 100644 index 0000000..f2dddad --- /dev/null +++ b/api/v1/util.py @@ -0,0 +1,61 @@ +import typing + +from flask import jsonify, request +from peewee import Model, ModelSelect + +_V = typing.TypeVar('_V', bound=Model) + + +def model_paginator(model: typing.Type[_V], mapping: typing.Callable[[_V], dict]): + return query_paginator(model.select(), mapping) + + +class ConstrainFailed(Exception): + def __init__(self, description: str, parameter: str, reason: str): + self.reason = reason + self.parameter = parameter + self.description = description + + +def try_or_fail_contrain(func: typing.Callable, parameter: str, value=None, default=None): + if value is None: + value = request.args.get(parameter, default) + try: + return func(value) + except BaseException as e: + func = func.__name__ + raise ConstrainFailed(f"{func}({parameter}) failed with an exception", parameter, + f"{func} raised {type(e).__name__}") + + +def max_constrain(max: int, parameter: str, value=None, default=None): + value = try_or_fail_contrain(int, parameter, value, default) + if value > max: + raise ConstrainFailed(f"requested {parameter} too big", parameter, f'> {max}') + return value + + +def min_constrain(min: int, parameter: str, value=None, default=None): + value = try_or_fail_contrain(int, parameter, value, default) + if value < min: + raise ConstrainFailed(f"requested {parameter} too big", parameter, f'< {min}') + return value + + +def query_paginator(query: ModelSelect, mapping: typing.Callable[[_V], dict]): + size = try_or_fail_contrain(int, 'size', default='50') + max_constrain(50, 'size', size) + min_constrain(2, 'size', size) + offset = try_or_fail_contrain(int, 'offset', default='0') + min_constrain(0, 'offset', offset) + res = [] + for obj in query.limit(size + 1).offset(offset): + res.append(mapping(obj)) + more = len(res) > size + if more: + res[-1:] = [] + return jsonify({ + 'more': more, + 'next': offset + size if more else None, + 'results': res, + }) diff --git a/app.py b/app.py new file mode 100644 index 0000000..75f2211 --- /dev/null +++ b/app.py @@ -0,0 +1,16 @@ +from flask import Flask, abort + +from api import bp as api_blueprint + +app = Flask(__name__) + + +@app.route('/') +def index(): + abort(404) + + +app.register_blueprint(api_blueprint, url_prefix='/api') + +if __name__ == '__main__': + app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55a911d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +peewee + -- cgit