parent
2770dbab49
commit
2e6ba0bb70
@ -0,0 +1,14 @@
|
|||||||
|
FROM python:alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --requirement requirements.txt
|
||||||
|
|
||||||
|
RUN addgroup -g 9999 lilia
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
USER nobody:lilia
|
||||||
|
|
||||||
|
ENTRYPOINT ["python", "app.py"]
|
@ -0,0 +1,256 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 = args['x']
|
||||||
|
member.y = 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 = args['x']
|
||||||
|
member.y = 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 = 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)
|
@ -0,0 +1,15 @@
|
|||||||
|
import flask_sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
|
db = flask_sqlalchemy.SQLAlchemy(session_options=dict(autoflush=False))
|
||||||
|
|
||||||
|
|
||||||
|
class Member(db.Model):
|
||||||
|
__tablename__ = 'members'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
nick = db.Column(db.String)
|
||||||
|
x = db.Column(db.Float)
|
||||||
|
y = db.Column(db.Float)
|
||||||
|
created_at = db.Column(db.DateTime)
|
||||||
|
updated_at = db.Column(db.DateTime)
|
@ -0,0 +1,24 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import itsdangerous
|
||||||
|
|
||||||
|
|
||||||
|
def generate(secret_key, username, expiration):
|
||||||
|
s = itsdangerous.TimedJSONWebSignatureSerializer(secret_key, expires_in=expiration)
|
||||||
|
return s.dumps(username)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('username', metavar='USERNAME', help='user name')
|
||||||
|
parser.add_argument('expiration', metavar='EXPIRATION', type=int,
|
||||||
|
help='expiration time in seconds')
|
||||||
|
args = parser.parse_args()
|
||||||
|
secret_key = os.getenv('SECRET_KEY')
|
||||||
|
api_key = generate(secret_key, args.username, args.expiration)
|
||||||
|
print(api_key.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,5 @@
|
|||||||
|
Flask
|
||||||
|
Flask-Login
|
||||||
|
Flask-RESTful
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
itsdangerous
|
Loading…
Reference in new issue