Add gifted subs API

master
Nikola Forró 6 years ago
parent 78a527dc61
commit 8a6b2bb602

@ -10,10 +10,24 @@ services:
ports:
- 127.0.0.1:8080:80
depends_on:
- gifted-subs-api
- quotes-api
- twitch-cache-api
- cms
# Gifted subs API service with /data/gifted-subs mounted as database storage
# SECRET_KEY is needed for API key validation
gifted-subs-api:
build:
context: ./gifted-subs-api
volumes:
- /data/gifted-subs:/gifted-subs
environment:
- SQLALCHEMY_DATABASE_URI=sqlite:////gifted-subs/gifted-subs.db
- SECRET_KEY=__SECRET_KEY__
expose:
- 5000
# Quotes API service with /data/quotes mounted as database storage
# SECRET_KEY is needed for API key validation
quotes-api:

@ -0,0 +1,96 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

@ -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,174 @@
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, GiftedSub
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)
gifted_sub_fields = {
'id': flask_restful.fields.Integer(),
'giver': flask_restful.fields.String(),
'receiver': flask_restful.fields.String(),
'time': flask_restful.fields.DateTime(dt_format='iso8601'),
'created_at': flask_restful.fields.DateTime(dt_format='iso8601'),
'updated_at': flask_restful.fields.DateTime(dt_format='iso8601'),
}
gifted_sub_parser = flask_restful.reqparse.RequestParser()
gifted_sub_parser.add_argument('id', type=int)
gifted_sub_parser.add_argument('giver', type=str, required=True)
gifted_sub_parser.add_argument('receiver', type=str, required=True)
gifted_sub_parser.add_argument('time', type=flask_restful.inputs.datetime_from_iso8601, 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)
@login_manager.request_loader
def load_user(request):
key = request.headers.get('X-Gifted-Subs-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
class GiftedSubResource(flask_restful.Resource):
@flask_restful.marshal_with(gifted_sub_fields)
def get(self, id):
q = db.session.query(GiftedSub).filter(GiftedSub.id == id)
gifted_sub = q.first()
if not gifted_sub:
flask_restful.abort(404, message='Gifted sub {0} does not exist'.format(id))
return gifted_sub, 200
@flask_login.login_required
@flask_restful.marshal_with(gifted_sub_fields)
def put(self, id):
args = gifted_sub_parser.parse_args()
now = sqlalchemy.func.now()
q = db.session.query(GiftedSub).filter(GiftedSub.id == id)
gifted_sub = q.first()
if not gifted_sub:
gifted_sub = GiftedSub(id=id, created_at=now)
gifted_sub.giver = args['giver']
gifted_sub.receiver = args['receiver']
gifted_sub.time = args['time']
gifted_sub.updated_at = now
db.session.add(gifted_sub)
db.session.commit()
return gifted_sub, 200
@flask_login.login_required
def delete(self, id):
q = db.session.query(GiftedSub).filter(GiftedSub.id == id)
gifted_sub = q.first()
if not gifted_sub:
flask_restful.abort(404, message='Gifted sub {0} does not exist'.format(id))
db.session.delete(gifted_sub)
db.session.commit()
return None, 204
class GiftedSubsResource(flask_restful.Resource):
@flask_restful.marshal_with(gifted_sub_fields)
def get(self):
args = filter_parser.parse_args()
q = db.session.query(GiftedSub)
if args['filter']:
q = q.filter(GiftedSub.giver.ilike('%{}%'.format(args['filter'])) |\
GiftedSub.receiver.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(GiftedSub, 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'])
gifted_subs = q.all()
return gifted_subs, 200, {'X-Total-Count': count}
@flask_login.login_required
@flask_restful.marshal_with(gifted_sub_fields)
def post(self):
args = gifted_sub_parser.parse_args()
if not args['giver']:
flask_restful.abort(400, message='Missing required parameter giver')
if not args['receiver']:
flask_restful.abort(400, message='Missing required parameter receiver')
if not args['time']:
flask_restful.abort(400, message='Missing required parameter time')
now = sqlalchemy.func.now()
q = db.session.query(GiftedSub).filter((GiftedSub.giver == args['giver']) &\
(GiftedSub.receiver == args['receiver']) &\
(sqlalchemy.func.DATE(GiftedSub.time) == args['time'].date()))
gifted_sub = q.first()
if not gifted_sub:
gifted_sub = GiftedSub(created_at=now)
gifted_sub.giver = args['giver']
gifted_sub.receiver = args['receiver']
gifted_sub.time = args['time']
gifted_sub.updated_at = now
db.session.add(gifted_sub)
db.session.commit()
url = api.url_for(GiftedSubResource, id=gifted_sub.id, _external=True, _scheme='https')
return gifted_sub, 201, {'Location': url}
api.add_resource(GiftedSubResource, '/gifted-subs/<int:id>')
api.add_resource(GiftedSubsResource, '/gifted-subs')
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 GiftedSub(db.Model):
__tablename__ = 'giftedsubs'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
giver = db.Column(db.String)
receiver = db.Column(db.String)
time = db.Column(db.DateTime)
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

@ -60,6 +60,15 @@ http {
root /twitch-logs;
}
location ^~ /gifted-subs/api/ {
rewrite ^/gifted-subs/api(/.*)$ $1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
tcp_nodelay on;
proxy_pass http://gifted-subs-api:5000/;
}
location ^~ /quotes/api/ {
rewrite ^/quotes/api(/.*)$ $1 break;
proxy_set_header Host $host;

Loading…
Cancel
Save