Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

Commit

Permalink
feat: Add socket.io
Browse files Browse the repository at this point in the history
  • Loading branch information
TomBursch committed Aug 17, 2023
1 parent 542bfea commit c14b81c
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 15 deletions.
5 changes: 2 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
gcc g++ libffi-dev libpcre3-dev build-essential cargo \
libxml2-dev libxslt-dev cmake gfortran libopenblas-dev liblapack-dev pkg-config ninja-build \
autoconf automake zlib1g-dev libjpeg62-turbo-dev
autoconf automake zlib1g-dev libjpeg62-turbo-dev libssl-dev

# Create virtual enviroment
RUN python -m venv /opt/venv && /opt/venv/bin/pip install --no-cache-dir -U pip setuptools wheel
Expand Down Expand Up @@ -45,9 +45,8 @@ HEALTHCHECK --interval=60s --timeout=3s CMD curl -f http://localhost/api/health/
ENV STORAGE_PATH='/data'
ENV JWT_SECRET_KEY='PLEASE_CHANGE_ME'
ENV DEBUG='False'
ENV HTTP_PORT=80

RUN chmod u+x ./entrypoint.sh

CMD ["wsgi.ini"]
CMD ["wsgi.ini -gevent 100"]
ENTRYPOINT ["./entrypoint.sh"]
3 changes: 2 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from app.config import app, jwt
from app.config import app, jwt, socketio
from app.config import db
from app.config import scheduler
from app.controller import *
from app.sockets import *
from app.jobs import *
from app.api import *
7 changes: 7 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import timedelta
from flask_socketio import SocketIO
from sqlalchemy import MetaData
from sqlalchemy.engine import URL
from werkzeug.exceptions import MethodNotAllowed
Expand Down Expand Up @@ -64,6 +65,7 @@

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 32 * 1000 * 1000 # 32MB max upload
app.config['SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'super-secret')
# SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
Expand All @@ -87,6 +89,7 @@
migrate = Migrate(app, db, render_as_batch=True)
bcrypt = Bcrypt(app)
jwt = JWTManager(app)
socketio = SocketIO(app, json=app.json, logger=True, cors_allowed_origins=os.getenv('FRONT_URL'))

scheduler = APScheduler()
# enable for debugging jobs: ../scheduler/jobs to see scheduled jobs
Expand Down Expand Up @@ -138,3 +141,7 @@ def unhandled_exception(e: Exception):
@app.errorhandler(404)
def not_found(error):
return "Requested resource not found", 404

@socketio.on_error_default
def default_socket_error_handler(e):
app.logger.error(e)
26 changes: 20 additions & 6 deletions app/controller/shoppinglist/shoppinglist_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.errors import NotFoundRequest, InvalidUsage
from datetime import datetime, timedelta, timezone
import app.util.description_merger as description_merger
from app import socketio


shoppinglist = Blueprint('shoppinglist', __name__)
Expand Down Expand Up @@ -193,6 +194,11 @@ def addShoppinglistItemByName(args, id):

History.create_added(shoppinglist, item, description)

socketio.emit("shoppinglist_item:add", {
"item": con.obj_to_item_dict(),
"shoppinglist": shoppinglist.obj_to_dict()
}, to=shoppinglist.household_id)

return jsonify(item.obj_to_dict())


Expand All @@ -205,8 +211,12 @@ def removeShoppinglistItem(args, id):
raise NotFoundRequest()
shoppinglist.checkAuthorized()

removeShoppinglistItem(
con = removeShoppinglistItem(
shoppinglist, args['item_id'], args['removed_at'] if 'removed_at' in args else None)
if con: socketio.emit("shoppinglist_item:remove", {
"item": con.obj_to_item_dict(),
"shoppinglist": shoppinglist.obj_to_dict()
}, to=shoppinglist.household_id)

return jsonify({'msg': "DONE"})

Expand All @@ -221,19 +231,23 @@ def removeShoppinglistItems(args, id):
shoppinglist.checkAuthorized()

for arg in args['items']:
removeShoppinglistItem(
con = removeShoppinglistItem(
shoppinglist, arg['item_id'], arg['removed_at'] if 'removed_at' in arg else None)
if con: socketio.emit("shoppinglist_item:remove", {
"item": con.obj_to_item_dict(),
"shoppinglist": shoppinglist.obj_to_dict()
}, to=shoppinglist.household_id)

return jsonify({'msg': "DONE"})


def removeShoppinglistItem(shoppinglist: Shoppinglist, item_id: int, removed_at: int = None) -> bool:
def removeShoppinglistItem(shoppinglist: Shoppinglist, item_id: int, removed_at: int = None) -> ShoppinglistItems:
item = Item.find_by_id(item_id)
if not item:
return False
return None
con = ShoppinglistItems.find_by_ids(shoppinglist.id, item.id)
if not con:
return False
return None
description = con.description
con.delete()

Expand All @@ -244,7 +258,7 @@ def removeShoppinglistItem(shoppinglist: Shoppinglist, item_id: int, removed_at:

History.create_dropped(
shoppinglist, item, description, removed_at_datetime)
return True
return con


@shoppinglist.route('/<int:id>/recipeitems', methods=['POST'])
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
from .db_model_authorize_mixin import DbModelAuthorizeMixin
from .timestamp_mixin import TimestampMixin
from .validate_args import validate_args
from .validate_socket_args import validate_socket_args
from .server_admin_required import server_admin_required
from .authorize_household import authorize_household, RequiredRights
from .socket_jwt_required import socket_jwt_required
25 changes: 25 additions & 0 deletions app/helpers/socket_jwt_required.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from functools import wraps
from flask import request

from flask_jwt_extended import verify_jwt_in_request
from flask_socketio import disconnect


def socket_jwt_required(
optional: bool = False,
fresh: bool = False,
refresh: bool = False,
):
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
try:
verify_jwt_in_request(optional, fresh, refresh)
except:
disconnect()
return
return fn(*args, **kwargs)

return decorator

return wrapper
23 changes: 23 additions & 0 deletions app/helpers/validate_socket_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from marshmallow.exceptions import ValidationError
from app.errors import InvalidUsage
from flask import request
from functools import wraps


def validate_socket_args(schema_cls):
def validate(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
if not schema_cls:
raise Exception("Invalid usage. Schema class missing")

try:
arguments = schema_cls().load(args[0])
except ValidationError as exc:
raise InvalidUsage('{}'.format(exc))

return func(arguments, **kwargs)

return func_wrapper

return validate
2 changes: 1 addition & 1 deletion app/models/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def create_dropped(cls, shoppinglist, item, description='', created_at=None) ->
item_id=item.id,
status=Status.DROPPED,
description=description,
created_at=created_at or datetime.utcnow
created_at=created_at
).save()

def obj_to_item_dict(self) -> dict:
Expand Down
2 changes: 2 additions & 0 deletions app/sockets/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .shoppinglist_socket import *
from .connection_socket import *
18 changes: 18 additions & 0 deletions app/sockets/connection_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flask_jwt_extended import current_user
from flask_socketio import join_room

from app.helpers import socket_jwt_required
from app import socketio


@socketio.on('connect')
@socket_jwt_required()
def on_connect():
for household in current_user.households:
join_room(household.household_id)


@socketio.on('reconnect')
@socket_jwt_required()
def on_reconnect():
pass
13 changes: 13 additions & 0 deletions app/sockets/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from marshmallow import Schema, fields


class shoppinglist_item_add(Schema):
shoppinglist_id = fields.Integer(required=True)
name = fields.String(
required=True
)
description = fields.String()

class shoppinglist_item_remove(Schema):
shoppinglist_id = fields.Integer(required=True)
item_id = fields.Integer(required=True)
56 changes: 56 additions & 0 deletions app/sockets/shoppinglist_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from flask_jwt_extended import current_user
from flask_socketio import emit
from app.controller.shoppinglist.shoppinglist_controller import removeShoppinglistItem
from app.errors import NotFoundRequest

from app.helpers import socket_jwt_required, validate_socket_args
from app.models import Shoppinglist, Item, ShoppinglistItems, History
from app import socketio
from .schemas import shoppinglist_item_add, shoppinglist_item_remove


@socketio.on('shoppinglist_item:add')
@socket_jwt_required()
@validate_socket_args(shoppinglist_item_add)
def on_add(args):
shoppinglist = Shoppinglist.find_by_id(args['shoppinglist_id'])
if not shoppinglist:
raise NotFoundRequest()
shoppinglist.checkAuthorized()

item = Item.find_by_name(shoppinglist.household_id, args['name'])
if not item:
item = Item.create_by_name(shoppinglist.household_id, args['name'])

con = ShoppinglistItems.find_by_ids(shoppinglist.id, item.id)
if not con:
description = args['description'] if 'description' in args else ''
con = ShoppinglistItems(description=description)
con.created_by = current_user.id
con.item = item
con.shoppinglist = shoppinglist
con.save()

History.create_added(shoppinglist, item, description)

emit("shoppinglist_item:add", {
"item": con.obj_to_item_dict(),
"shoppinglist": shoppinglist.obj_to_dict()
}, to=shoppinglist.household_id)


@socketio.on('shoppinglist_item:remove')
@socket_jwt_required()
@validate_socket_args(shoppinglist_item_remove)
def on_remove(args):
shoppinglist = Shoppinglist.find_by_id(args['shoppinglist_id'])
if not shoppinglist:
raise NotFoundRequest()
shoppinglist.checkAuthorized()

con = removeShoppinglistItem(shoppinglist, args['item_id'])
if con:
emit('shoppinglist_item:remove', {
"item": con.obj_to_item_dict(),
"shoppinglist": shoppinglist.obj_to_dict()
}, to=shoppinglist.household_id)
10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ attrs==23.1.0
autopep8==2.0.2
bcrypt==4.0.1
beautifulsoup4==4.12.2
bidict==0.22.1
black==23.1a1
blinker==1.6.2
certifi==2023.7.22
Expand All @@ -21,9 +22,12 @@ Flask-APScheduler==1.12.4
Flask-Bcrypt==1.0.1
Flask-JWT-Extended==4.5.2
Flask-Migrate==4.0.4
Flask-SocketIO==5.3.5
Flask-SQLAlchemy==3.0.5
fonttools==4.42.0
gevent==23.7.0
greenlet==2.0.2
h11==0.14.0
html-text==0.5.2
html5lib==1.1
idna==3.4
Expand Down Expand Up @@ -67,6 +71,8 @@ pytest==7.4.0
python-crfsuite==0.9.9
python-dateutil==2.8.2
python-editor==1.0.4
python-engineio==4.5.1
python-socketio==5.8.0
pytz==2023.3
pytz-deprecation-shim==0.1.0.post0
rdflib==7.0.0
Expand All @@ -77,6 +83,7 @@ requests==2.31.0
scikit-learn==1.3.0
scipy==1.11.1
setuptools-scm==7.1.0
simple-websocket==0.10.1
six==1.16.0
soupsieve==2.4.1
SQLAlchemy==2.0.19
Expand All @@ -97,3 +104,6 @@ uWSGI==2.0.22
w3lib==2.1.2
webencodings==0.5.1
Werkzeug==2.3.6
wsproto==1.2.0
zope.event==5.0
zope.interface==6.0
3 changes: 1 addition & 2 deletions wsgi.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
strict = true
master = true
enable-threads = true
http-websockets = true
lazy-apps=true
vacuum = true
single-interpreter = true
Expand All @@ -11,7 +12,5 @@ chmod-socket = 664

wsgi-file = wsgi.py
callable = app
http = 0.0.0.0:$(HTTP_PORT)
http-keepalive = true
socket = 0.0.0.0:5000
procname-prefix-spaced = kitchenowl
7 changes: 5 additions & 2 deletions wsgi.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from app import app
import gevent.monkey
gevent.monkey.patch_all()

from app import app, socketio
import os

from app.config import UPLOAD_FOLDER

if __name__ == "__main__":
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(debug=True)
socketio.run(app, debug=True)

0 comments on commit c14b81c

Please sign in to comment.