mirror of
https://github.com/Xevion/power-math.git
synced 2025-12-06 21:15:54 -06:00
flask restful based API handling & exceptions work
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
flask
|
flask
|
||||||
|
flask-restful
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ API interactions, static or dynamic.
|
|||||||
import copy
|
import copy
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from flask import jsonify
|
|
||||||
from flask_restful import Resource, reqparse, abort
|
from flask_restful import Resource, reqparse, abort
|
||||||
|
|
||||||
from server import questions
|
from server import questions, exceptions
|
||||||
from server.helpers import generate_id
|
from server.helpers import generate_id
|
||||||
|
|
||||||
# Stores active questions in memory for checking answers.
|
# Stores active questions in memory for checking answers.
|
||||||
@@ -19,7 +18,10 @@ active_questions = {}
|
|||||||
|
|
||||||
# Question categories, key name, value function for acquiring a random question generator.
|
# Question categories, key name, value function for acquiring a random question generator.
|
||||||
categories = {
|
categories = {
|
||||||
'arithmetic': questions.get_arithmetic
|
'arithmetic': (
|
||||||
|
questions.get_arithmetic,
|
||||||
|
'The basic four mathematical functions, plus fractions, exponents and a little more.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@@ -35,11 +37,17 @@ class Question(Resource):
|
|||||||
They are identified by a String ID.
|
They are identified by a String ID.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, question_id):
|
def get(self, question_id=None):
|
||||||
"""Retrieve information about a given question, including the answer for it."""
|
"""Retrieve information about a given question, including the answer for it."""
|
||||||
pass
|
if question_id is not None:
|
||||||
|
if question_id in active_questions.keys():
|
||||||
|
return active_questions[question_id]
|
||||||
|
else:
|
||||||
|
raise exceptions.InvalidQuestion(404, question=question_id)
|
||||||
|
else:
|
||||||
|
abort(404, message='Use PUT to create questions, otherwise specify a question ID')
|
||||||
|
|
||||||
def put(self, question_id = None):
|
def put(self, question_id=None):
|
||||||
"""
|
"""
|
||||||
Request a new question.
|
Request a new question.
|
||||||
|
|
||||||
@@ -48,26 +56,46 @@ class Question(Resource):
|
|||||||
The Question object is returned, although the answer is omitted.
|
The Question object is returned, although the answer is omitted.
|
||||||
"""
|
"""
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
q_id = None
|
|
||||||
|
# Generate new Question ID
|
||||||
|
q_id = Nonewt
|
||||||
while q_id in active_questions.keys() or q_id is None:
|
while q_id in active_questions.keys() or q_id is None:
|
||||||
q_id = generate_id(5)
|
q_id = generate_id(5)
|
||||||
|
|
||||||
# Get category arg or choose one if not specified.
|
# Get category arg or choose one if not specified.
|
||||||
if args.get('category') is not None:
|
if args.get('category') is not None:
|
||||||
category = args.get('category')
|
category = args.get('category')
|
||||||
|
# Check that the category is valid
|
||||||
if category not in categories.keys():
|
if category not in categories.keys():
|
||||||
abort(404, message=f'Category {category} is not valid.')
|
raise exceptions.InvalidCategory(404, category=category)
|
||||||
else:
|
else:
|
||||||
category = random.choice(list(categories.keys()))
|
category = random.choice(list(categories.keys()))
|
||||||
|
|
||||||
# Acquire a question generator and generate a function, then store the result.
|
# Acquire a question generator and generate a function, then store the result.
|
||||||
active_questions[q_id] = categories[category]()()
|
active_questions[q_id] = categories[category][0]()()
|
||||||
|
|
||||||
# Make a shallow copy, hide 'answer' key.
|
# Make a shallow copy, hide 'answer' key.
|
||||||
question = copy.copy(active_questions[q_id])
|
question = copy.copy(active_questions[q_id])
|
||||||
del question['answer']
|
del question['answer']
|
||||||
|
|
||||||
return question, 200
|
return 201, question
|
||||||
|
|
||||||
|
def delete(self, question_id):
|
||||||
|
"""Delete a question object from the running before it is automatically removed."""
|
||||||
|
if question_id in active_questions.keys():
|
||||||
|
del active_questions[question_id]
|
||||||
|
return {'message': f'Successfully deleted Question ID {question_id}'}
|
||||||
|
else:
|
||||||
|
raise exceptions.InvalidQuestion(404, question=question_id)
|
||||||
|
|
||||||
|
|
||||||
|
class Questions(Resource):
|
||||||
|
"""
|
||||||
|
Simple resource for listing all available question objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return active_questions
|
||||||
|
|
||||||
|
|
||||||
class Category(Resource):
|
class Category(Resource):
|
||||||
@@ -78,7 +106,10 @@ class Category(Resource):
|
|||||||
|
|
||||||
def get(self, category_id):
|
def get(self, category_id):
|
||||||
"""Get all information about a category"""
|
"""Get all information about a category"""
|
||||||
pass
|
if category_id in categories.keys():
|
||||||
|
return categories[category_id][1]
|
||||||
|
else:
|
||||||
|
raise exceptions.InvalidCategory(404, category=category_id)
|
||||||
|
|
||||||
|
|
||||||
class Categories(Resource):
|
class Categories(Resource):
|
||||||
@@ -90,5 +121,3 @@ class Categories(Resource):
|
|||||||
def get(self):
|
def get(self):
|
||||||
"""Get a list of all categories."""
|
"""Get a list of all categories."""
|
||||||
return list(categories.keys()), 200
|
return list(categories.keys()), 200
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
from flask import Flask, render_template
|
"""
|
||||||
|
create_app.py
|
||||||
|
|
||||||
|
The main app creation, registering extensions, API routes, the Vue.js catch all redirect and configs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, jsonify
|
||||||
from flask_restful import Api
|
from flask_restful import Api
|
||||||
|
|
||||||
|
from server import exceptions
|
||||||
|
from server.api import Question, Questions, Category, Categories
|
||||||
from server.config import configs
|
from server.config import configs
|
||||||
|
|
||||||
|
|
||||||
@@ -12,27 +20,23 @@ def create_app(env=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Instantiate Flask-Restful API and register appropriate routes
|
# Instantiate Flask-Restful API and register appropriate routes
|
||||||
from server.api import Question, Category, Categories
|
|
||||||
api = Api(app, prefix='/api/')
|
api = Api(app, prefix='/api/')
|
||||||
api.add_resource(Question, '/question/', '/question/<string:question_id>')
|
api.add_resource(Question, '/question/', '/question/<string:question_id>')
|
||||||
api.add_resource(Category, '/category/<string:category_id>')
|
api.add_resource(Category, '/category/<string:category_id>')
|
||||||
api.add_resource(Categories, '/categories/')
|
api.add_resource(Categories, '/categories/')
|
||||||
|
api.add_resource(Questions, '/questions/')
|
||||||
|
|
||||||
if not env:
|
if not env:
|
||||||
env = app.config['ENV']
|
env = app.config['ENV']
|
||||||
app.config.from_object(configs[env])
|
app.config.from_object(configs[env])
|
||||||
|
|
||||||
# @app.shell_context_processor
|
|
||||||
# def shell_context():
|
|
||||||
# pass
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
from server import api
|
|
||||||
|
|
||||||
@app.route('/', defaults={'path': ''})
|
@app.route('/', defaults={'path': ''})
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
def catch_all(path):
|
def catch_all(path):
|
||||||
return render_template("index.html")
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.errorhandler(exceptions.APIException)
|
||||||
|
def api_exceptions(e):
|
||||||
|
return jsonify(e.json()), e.status_code
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
90
server/exceptions.py
Normal file
90
server/exceptions.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
exceptions.py
|
||||||
|
|
||||||
|
Stores all API exceptions neatly for importing and usage elsewhere.
|
||||||
|
"""
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# TODO: Improve exception management to cut down needless class definitions.
|
||||||
|
# TODO: Add 'extra' message parameter to base APIException kwargs.
|
||||||
|
|
||||||
|
class APIException(Exception):
|
||||||
|
"""Exception from which all API-related exceptions are derived and formatted with"""
|
||||||
|
MESSAGE = "A generic unhandled API Exception has occurred."
|
||||||
|
|
||||||
|
def __init__(self, status_code: int = 500):
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
'error': {
|
||||||
|
'code': self.status_code,
|
||||||
|
'message': self.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UnspecifiedParam(APIException):
|
||||||
|
MESSAGE = 'This API Route requires a parameter that was not satisfied in the latest request.'
|
||||||
|
|
||||||
|
def __init__(self, status_code: int):
|
||||||
|
super().__init__(status_code)
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
'error': {
|
||||||
|
'code': self.status_code,
|
||||||
|
'message': self.MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UnspecifiedQueryParam(APIException):
|
||||||
|
MESSAGE = 'This API Route requires a query parameter that was not satisfied in the latest request.'
|
||||||
|
|
||||||
|
def __init__(self, status_code: int):
|
||||||
|
super().__init__(status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class UnspecifiedURIParam(APIException):
|
||||||
|
MESSAGE = 'This API Route requires a URI parameter that was not satisfied in the latest request.'
|
||||||
|
|
||||||
|
def __init__(self, status_code: int):
|
||||||
|
super().__init__(status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQueryParam(APIException):
|
||||||
|
def __init__(self, status_code: int, query_item: Tuple[str, str]):
|
||||||
|
super().__init__(status_code)
|
||||||
|
self.query_item = query_item
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
error = super().json()
|
||||||
|
error['error']['query'] = {'key': self.query_item[0], 'value': self.query_item[1]}
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidURIParam(APIException):
|
||||||
|
def __init__(self, status_code: int, route_param: str):
|
||||||
|
super().__init__(status_code)
|
||||||
|
self.route_param = route_param
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
error = super().json()
|
||||||
|
error['error']['param'] = self.route_param
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuestion(InvalidURIParam):
|
||||||
|
MESSAGE = "A invalid question was specified in the request URI and could not be resolved."
|
||||||
|
|
||||||
|
def __init__(self, *args, question):
|
||||||
|
super().__init__(*args, route_param=question)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidCategory(InvalidURIParam):
|
||||||
|
MESSAGE = "A invalid category was specified in the request URI and could not be resolved."
|
||||||
|
|
||||||
|
def __init__(self, status_code, category: str):
|
||||||
|
super().__init__(status_code, route_param=category)
|
||||||
Reference in New Issue
Block a user