You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

259 lines
9.0 KiB

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.Integer(),
'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 = {
'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=int)
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('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': {
'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['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.user == args['nick'])
member = q.first()
if not member:
member = Member(created_at=now)
member.user = 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.nick == properties_args['nick'])
member = q.first()
if not member:
member = Member(created_at=now)
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/<int:id>')
api.add_resource(MembersResource, '/members')
api.add_resource(FeatureResource, '/features/<int:id>')
api.add_resource(FeaturesResource, '/features')
if __name__ == '__main__':
app.run(host='0.0.0.0', threaded=True, debug=False)