Skip to content

Commit

Permalink
Migrate from flask_restful to marshmallow (#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
pajlada committed Apr 4, 2022
1 parent 3f8f73e commit a61f24b
Show file tree
Hide file tree
Showing 29 changed files with 458 additions and 829 deletions.
3 changes: 0 additions & 3 deletions .mypy.ini
Expand Up @@ -13,9 +13,6 @@ ignore_missing_imports = True
[mypy-flask_assets.*]
ignore_missing_imports = True

[mypy-flask_restful.*]
ignore_missing_imports = True

[mypy-unidecode.*]
ignore_missing_imports = True

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,10 @@

Remember to bring your dependencies up to date with `./scripts/venvinstall.sh` when updating to this version!

- Major: Remove pleblist API endpoints. (#1809)
- Major: Remove pleblist pages. (#1809)
- Dev: Migrate from `flask_restful` to `marshmallow` for handling request parameter parsing. (#1809)

## v1.60

Remember to bring your dependencies up to date with `./scripts/venvinstall.sh` when updating to this version!
Expand Down
2 changes: 1 addition & 1 deletion pajbot/web/__init__.py
Expand Up @@ -143,7 +143,7 @@ def init(args):
pajbot.web.routes.base.init(app)

# Make a CSRF exemption for the /api/v1/banphrases/test endpoint
csrf.exempt("pajbot.web.routes.api.banphrases.apibanphrasetest")
csrf.exempt("pajbot.web.routes.api.banphrases.banphrases_test")

pajbot.web.common.filters.init(app)
pajbot.web.common.assets.init(app)
Expand Down
4 changes: 3 additions & 1 deletion pajbot/web/models/errors.py
@@ -1,13 +1,15 @@
import logging

from flask import render_template
from flask import jsonify, render_template, request

log = logging.getLogger(__name__)


def init(app, config):
@app.errorhandler(404)
def page_not_found(e):
if request.path.startswith("/api/"):
return jsonify({"error": "No API endpoint here!"})
return render_template("errors/404.html"), 404

@app.errorhandler(500)
Expand Down
30 changes: 14 additions & 16 deletions pajbot/web/routes/api/__init__.py
Expand Up @@ -3,47 +3,45 @@
import pajbot.web.routes.api.common
import pajbot.web.routes.api.modules
import pajbot.web.routes.api.playsound
import pajbot.web.routes.api.pleblist
import pajbot.web.routes.api.social
import pajbot.web.routes.api.timers
import pajbot.web.routes.api.twitter
import pajbot.web.routes.api.users

from flask_restful import Api
from flask import Blueprint


def init(app):
def init(app) -> None:
# Initialize the v1 api
# /api/v1
api = Api(app, prefix="/api/v1", catch_all_404s=False)
bp = Blueprint("api", __name__, url_prefix="/api/v1")

# Initialize any common settings and routes
pajbot.web.routes.api.common.init(api)
pajbot.web.routes.api.common.init(bp)

# /users
pajbot.web.routes.api.users.init(api)
pajbot.web.routes.api.users.init(bp)

# /twitter
pajbot.web.routes.api.twitter.init(api)
pajbot.web.routes.api.twitter.init(bp)

# /commands
pajbot.web.routes.api.commands.init(api)

# /pleblist
pajbot.web.routes.api.pleblist.init(api)
pajbot.web.routes.api.commands.init(bp)

# /social
pajbot.web.routes.api.social.init(api)
pajbot.web.routes.api.social.init(bp)

# /timers
pajbot.web.routes.api.timers.init(api)
pajbot.web.routes.api.timers.init(bp)

# /banphrases
pajbot.web.routes.api.banphrases.init(api)
pajbot.web.routes.api.banphrases.init(bp)

# /modules
pajbot.web.routes.api.modules.init(api)
pajbot.web.routes.api.modules.init(bp)

# /playsound/:name
# /playsound/:name/play
pajbot.web.routes.api.playsound.init(api)
pajbot.web.routes.api.playsound.init(bp)

app.register_blueprint(bp)
126 changes: 67 additions & 59 deletions pajbot/web/routes/api/banphrases.py
@@ -1,4 +1,6 @@
import json
import logging
from dataclasses import dataclass

import pajbot.modules
import pajbot.utils
Expand All @@ -7,15 +9,27 @@
from pajbot.managers.db import DBManager
from pajbot.models.banphrase import Banphrase, BanphraseManager
from pajbot.models.sock import SocketClientManager
from pajbot.web.schemas.toggle_state import ToggleState, ToggleStateSchema

from flask_restful import Resource, reqparse
import marshmallow_dataclass
from flask import Blueprint, request
from marshmallow import Schema, ValidationError

log = logging.getLogger(__name__)


class APIBanphraseRemove(Resource):
@dataclass
class TestBanphrase(Schema):
message: str


TestBanphraseSchema = marshmallow_dataclass.class_schema(TestBanphrase)


def init(bp: Blueprint) -> None:
@bp.route("/banphrases/remove/<int:banphrase_id>", methods=["POST"])
@pajbot.web.utils.requires_level(500)
def post(self, banphrase_id, **options):
def banphrases_remove(banphrase_id, **options):
with DBManager.create_session_scope() as db_session:
banphrase = db_session.query(Banphrase).filter_by(id=banphrase_id).one_or_none()
if banphrase is None:
Expand All @@ -26,53 +40,56 @@ def post(self, banphrase_id, **options):
SocketClientManager.send("banphrase.remove", {"id": banphrase.id})
return {"success": "good job"}, 200


class APIBanphraseToggle(Resource):
def __init__(self):
super().__init__()

self.post_parser = reqparse.RequestParser()
self.post_parser.add_argument("new_state", required=True)

@bp.route("/banphrases/toggle/<int:row_id>", methods=["POST"])
@pajbot.web.utils.requires_level(500)
def post(self, row_id, **options):
args = self.post_parser.parse_args()
def banphrases_toggle(row_id, **options):
json_data = request.get_json()
if not json_data:
return {"error": "No input data provided"}, 400

try:
new_state = int(args["new_state"])
except (ValueError, KeyError):
return {"error": "Invalid `new_state` parameter."}, 400
data: ToggleState = ToggleStateSchema().load(json_data)
except ValidationError as err:
return {"error": f"Did not match schema: {json.dumps(err.messages)}"}, 400

with DBManager.create_session_scope() as db_session:
row = db_session.query(Banphrase).filter_by(id=row_id).one_or_none()

if not row:
return {"error": "Banphrase with this ID not found"}, 404

row.enabled = new_state == 1
row.enabled = data.new_state
db_session.commit()
payload = {"id": row.id, "new_state": row.enabled}
payload = {"id": row.id, "new_state": data.new_state}
AdminLogManager.post(
"Banphrase toggled", options["user"], "Enabled" if row.enabled else "Disabled", row.id, row.phrase
"Banphrase toggled", options["user"], "Enabled" if data.new_state else "Disabled", row.id, row.phrase
)
SocketClientManager.send("banphrase.update", payload)
return {"success": "successful toggle", "new_state": new_state}


class APIBanphraseTest(Resource):
def __init__(self):
super().__init__()

self.post_parser = reqparse.RequestParser()
self.post_parser.add_argument("message", required=True)

def post(self, **options):
args = self.post_parser.parse_args()

try:
message = str(args["message"])
except (ValueError, KeyError):
return {"error": "Invalid `message` parameter."}, 400
return {"success": "successful toggle", "new_state": data.new_state}

@bp.route("/banphrases/test", methods=["POST"])
def banphrases_test():
if request.is_json:
# Example request:
# curl -XPOST -d'{"message": "xD"}' -H'Content-Type: application/json' http://localhost:7070/api/v1/banphrases/test
json_data = request.get_json()
if not json_data:
return {"error": "Missing json body"}, 400
try:
data: TestBanphrase = TestBanphraseSchema().load(json_data)
except ValidationError as err:
return {"error": f"Did not match schema: {json.dumps(err.messages)}"}, 400
else:
# This endpoint must handle form requests
# Example requests:
# curl -XPOST -H 'Content-Type: application/x-www-form-urlencoded' -d 'message=xD2' http://localhost:7070/api/v1/banphrases/test
# curl -XPOST -F 'message=xD2' http://localhost:7070/api/v1/banphrases/test
try:
data: TestBanphrase = TestBanphraseSchema().load(request.form.to_dict())
except ValidationError as err:
return {"error": f"Did not match schema: {json.dumps(err.messages)}"}, 400

message = data.message

if not message:
return {"error": "Parameter `message` cannot be empty."}, 400
Expand All @@ -91,26 +108,17 @@ def post(self, **options):

return ret


class APIBanphraseDump(Resource):
def __init__(self):
super().__init__()

@staticmethod
def get(**options):
banphrase_manager = BanphraseManager(None).load()
try:
return banphrase_manager.enabled_banphrases
finally:
banphrase_manager.db_session.close()


def init(api):
api.add_resource(APIBanphraseRemove, "/banphrases/remove/<int:banphrase_id>")
api.add_resource(APIBanphraseToggle, "/banphrases/toggle/<int:row_id>")

# Test a message against banphrases
api.add_resource(APIBanphraseTest, "/banphrases/test")

# Dump
# api.add_resource(APIBanphraseDump, '/banphrases/dump')
# @bp.route("/banphrases/dump")
# def banphrases_dump():
# banphrase_manager = BanphraseManager(None).load()
# try:
# payload = {"banphrases": []}
# for bp in banphrase_manager.enabled_banphrases:
# payload["banphrases"].append(bp.jsonify())

# return payload, 200
# except:
# log.exception("Error getting enabled banphrases")
# return {"error": "hmm"}, 500
# finally:
# banphrase_manager.db_session.close()

0 comments on commit a61f24b

Please sign in to comment.