import logging import os import flask import flask_login import flask_restful import flask_restful.fields import flask_restful.reqparse import itsdangerous import sqlalchemy import sqlalchemy.engine from db import db, Member app = flask.Flask(__name__) app.logger.setLevel(logging.INFO) app.config.update( ERROR_404_HELP=False, SQLALCHEMY_TRACK_MODIFICATIONS=False, SQLALCHEMY_DATABASE_URI=os.getenv('SQLALCHEMY_DATABASE_URI'), SECRET_KEY=os.getenv('SECRET_KEY')) if app.config.get('SQLALCHEMY_DATABASE_URI', '').startswith('sqlite://'): @sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, 'connect') def set_sqlite_pragma(dbapi_connection, connection_record): dbapi_connection.execute('PRAGMA journal_mode=WAL') dbapi_connection.execute('PRAGMA synchronous=NORMAL') db.init_app(app) db.create_all(app=app) login_manager = flask_login.LoginManager() login_manager.init_app(app) api = flask_restful.Api(app) member_fields = { 'id': flask_restful.fields.String(), 'nick': flask_restful.fields.String(), 'x': flask_restful.fields.Float(), 'y': flask_restful.fields.Float(), 'created_at': flask_restful.fields.DateTime(dt_format='iso8601'), 'updated_at': flask_restful.fields.DateTime(dt_format='iso8601'), } geometry_fields = { 'type': flask_restful.fields.String(default='Point'), 'coordinates': flask_restful.fields.List(flask_restful.fields.Float()), } properties_fields = { 'id': flask_restful.fields.String(), 'nick': flask_restful.fields.String(), } feature_fields = { 'type': flask_restful.fields.String(default='Feature'), 'geometry': flask_restful.fields.Nested(geometry_fields), 'properties': flask_restful.fields.Nested(properties_fields), } features_fields = { 'type': flask_restful.fields.String(default='FeatureCollection'), 'features': flask_restful.fields.List(flask_restful.fields.Nested(feature_fields)), } member_parser = flask_restful.reqparse.RequestParser() member_parser.add_argument('id', type=str, required=True) member_parser.add_argument('nick', type=str, required=True) member_parser.add_argument('x', type=float, required=True) member_parser.add_argument('y', type=float, required=True) filter_parser = flask_restful.reqparse.RequestParser() filter_parser.add_argument('filter', type=str) filter_parser.add_argument('sort_by', type=str) filter_parser.add_argument('sort_order', type=str) filter_parser.add_argument('page_number', type=int) filter_parser.add_argument('page_size', type=int) feature_parser = flask_restful.reqparse.RequestParser() feature_parser.add_argument('type', type=str, required=True) feature_parser.add_argument('geometry', type=dict, required=True) feature_parser.add_argument('properties', type=dict, required=True) geometry_parser = flask_restful.reqparse.RequestParser() geometry_parser.add_argument('type', type=str, location=('geometry',), required=True) geometry_parser.add_argument('coordinates', type=float, action='append', location=('geometry',), required=True) properties_parser = flask_restful.reqparse.RequestParser() properties_parser.add_argument('id', type=str, location=('properties',), required=True) properties_parser.add_argument('nick', type=str, location=('properties',), required=True) @login_manager.request_loader def load_user(request): key = request.headers.get('X-Members-API-Key') if not key: return None s = itsdangerous.TimedJSONWebSignatureSerializer(app.config['SECRET_KEY']) try: user = flask_login.UserMixin() user.id = s.loads(key) return user except (itsdangerous.SignatureExpired, itsdangerous.BadSignature): return None def member2feature(member): return { 'geometry': { 'coordinates': [ member.x, member.y, ], }, 'properties': { 'id': member.id, 'nick': member.nick, }, } def normalize(x, y): return x % 180 if x > 0 else x % -180, y class MemberResource(flask_restful.Resource): @flask_restful.marshal_with(member_fields) def get(self, id): q = db.session.query(Member).filter(Member.id == id) member = q.first() if not member: flask_restful.abort(404, message='Member {0} does not exist'.format(id)) return member, 200 @flask_login.login_required @flask_restful.marshal_with(member_fields) def put(self, id): args = member_parser.parse_args() now = sqlalchemy.func.now() q = db.session.query(Member).filter(Member.id == id) member = q.first() if not member: member = Member(id=id, created_at=now) member.nick = args['nick'] member.x, member.y = normalize(args['x'], args['y']) member.updated_at = now db.session.add(member) db.session.commit() return member, 200 @flask_login.login_required def delete(self, id): q = db.session.query(Member).filter(Member.id == id) member = q.first() if not member: flask_restful.abort(404, message='Member {0} does not exist'.format(id)) db.session.delete(member) db.session.commit() return None, 204 class MembersResource(flask_restful.Resource): @flask_restful.marshal_with(member_fields) def get(self): args = filter_parser.parse_args() q = db.session.query(Member) if args['filter']: q = q.filter(Member.nick.ilike('%{}%'.format(args['filter']))) count = q.count() if args['sort_order'] == 'random': q = q.order_by(sqlalchemy.func.random()) elif args['sort_by']: col = getattr(Member, args['sort_by'], None) if col: if args['sort_order']: order_by = getattr(col, args['sort_order'], None) if order_by: q = q.order_by(order_by()) else: q = q.order_by(col) if args['page_size']: q = q.limit(args['page_size']) if args['page_number'] and args['page_size']: q = q.offset(args['page_number'] * args['page_size']) members = q.all() return members, 200, {'X-Total-Count': count} @flask_login.login_required @flask_restful.marshal_with(member_fields) def post(self): args = member_parser.parse_args() if not args['id']: flask_restful.abort(400, message='Missing required parameter id') if not args['nick']: flask_restful.abort(400, message='Missing required parameter nick') if not args['x']: flask_restful.abort(400, message='Missing required parameter x') if not args['y']: flask_restful.abort(400, message='Missing required parameter y') now = sqlalchemy.func.now() q = db.session.query(Member).filter(Member.id == args['id']) member = q.first() if not member: member = Member(created_at=now) member.id = args['id'] member.nick = args['nick'] member.x, member.y = normalize(args['x'], args['y']) member.updated_at = now db.session.add(member) db.session.commit() url = api.url_for(MemberResource, id=member.id, _external=True, _scheme='https') return member, 201, {'Location': url} class FeatureResource(flask_restful.Resource): @flask_restful.marshal_with(feature_fields) def get(self, id): q = db.session.query(Member).filter(Member.id == id) member = q.first() if not member: flask_restful.abort(404, message='Member {0} does not exist'.format(id)) return member2feature(member), 200 class FeaturesResource(flask_restful.Resource): @flask_restful.marshal_with(features_fields) def get(self): q = db.session.query(Member) members = q.all() return {'features': [member2feature(m) for m in members]}, 200 @flask_login.login_required @flask_restful.marshal_with(feature_fields) def post(self): feature_args = feature_parser.parse_args() geometry_args = geometry_parser.parse_args(req=feature_args) properties_args = properties_parser.parse_args(req=feature_args) if feature_args['type'] != 'Feature': flask_restful.abort(400, message='Unexpected feature type') if geometry_args['type'] != 'Point': flask_restful.abort(400, message='Unexpected geometry type') now = sqlalchemy.func.now() q = db.session.query(Member).filter(Member.id == properties_args['id']) member = q.first() if not member: member = Member(created_at=now) member.id = properties_args['id'] member.nick = properties_args['nick'] member.x, member.y = normalize(*geometry_args['coordinates']) member.updated_at = now db.session.add(member) db.session.commit() url = api.url_for(FeatureResource, id=member.id, _external=True, _scheme='https') return member2feature(member), 201, {'Location': url} api.add_resource(MemberResource, '/members/') api.add_resource(MembersResource, '/members') api.add_resource(FeatureResource, '/features/') api.add_resource(FeaturesResource, '/features') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, debug=False)