add swagger and update api endpoints

This commit is contained in:
dominik24c 2022-06-06 21:30:30 +02:00
parent 3d36569844
commit 30917eb7d7
7 changed files with 98 additions and 63 deletions

View File

@ -1,6 +1,6 @@
import os import os
from flask import Flask from apiflask import APIFlask
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_cors import CORS from flask_cors import CORS
@ -9,14 +9,14 @@ from .dependencies import db, ma
from .commands.startapp import startapp from .commands.startapp import startapp
from .utils import import_models from .utils import import_models
from .api import api_bp from .api import api_bp
from .errors import request_entity_too_large from .errors import request_entity_too_large, register_error_handlers
def create_app(config_name: str = None) -> Flask: def create_app(config_name: str = None) -> APIFlask:
if config_name is None: if config_name is None:
config_name = os.environ.get("FLASK_ENV") config_name = os.environ.get("FLASK_ENV")
app = Flask(__name__) app = APIFlask(__name__, docs_path='/')
app.config.from_object(config.get(config_name) or config.get("development")) app.config.from_object(config.get(config_name) or config.get("development"))
if app.config['ENABLE_CORS']: if app.config['ENABLE_CORS']:
@ -37,6 +37,7 @@ def create_app(config_name: str = None) -> Flask:
app.cli.add_command(startapp) app.cli.add_command(startapp)
# register errors # register errors
app.register_error_handler(413, request_entity_too_large) register_error_handlers(app)
# app.register_error_handler(413, request_entity_too_large)
return app return app

View File

@ -17,13 +17,16 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = f'sqlite:///{BASE_DIR / "db.sqlite"}' SQLALCHEMY_DATABASE_URI = f'sqlite:///{BASE_DIR / "db.sqlite"}'
DESCRIPTION = 'System PRI'
OPENAPI_VERSION = '3.0.2'
class ProductionConfig(Config): class ProductionConfig(Config):
DB_SERVER = "0.0.0.0" DB_SERVER = "0.0.0.0"
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
pass DEBUG = True
class TestingConfig(Config): class TestingConfig(Config):

View File

@ -1,7 +1,7 @@
from flask import Blueprint from apiflask import APIBlueprint
from .students import bp as students_bp from .students import bp as students_bp
bp = Blueprint("coordinator", __name__, url_prefix="/coordinator") bp = APIBlueprint("coordinator", __name__, url_prefix="/coordinator")
bp.register_blueprint(students_bp) bp.register_blueprint(students_bp)

View File

@ -1,117 +1,114 @@
from typing import Tuple
from random import randint from random import randint
from itertools import islice from itertools import islice
from flask import Blueprint, request, Response from flask import abort
from apiflask import APIBlueprint
from marshmallow import ValidationError from marshmallow import ValidationError
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from flask_sqlalchemy import get_debug_queries from flask_sqlalchemy import get_debug_queries
from ...students.models import Student from ...students.models import Student
from ..schemas import StudentSchema, StudentEditSchema, StudentCreateSchema from ..schemas import StudentSchema, StudentEditSchema, StudentsPaginationSchema, \
StudentCreateSchema, MessageSchema, FileSchema, StudentQuerySchema
from ...dependencies import db from ...dependencies import db
from ..utils import parse_csv from ..utils import parse_csv
from ..exceptions import InvalidNameOrTypeHeaderException from ..exceptions import InvalidNameOrTypeHeaderException
from ...base.utils import paginate_models, is_allowed_extensions from ...base.utils import paginate_models, is_allowed_extensions
bp = Blueprint("students", __name__, url_prefix="/students") bp = APIBlueprint("students", __name__, url_prefix="/students")
@bp.route("/", methods=["GET"]) @bp.route("/", methods=["GET"])
def list_students() -> Tuple[dict, int]: @bp.input(StudentQuerySchema, location='query')
students_schema = StudentSchema(many=True) @bp.output(StudentsPaginationSchema)
def list_students(query: dict) -> dict:
fullname = request.args.get('fullname') fullname = query.get('fullname')
order_by_first_name = request.args.get('order_by_first_name') order_by_first_name = query.get('order_by_first_name')
order_by_last_name = request.args.get('order_by_last_name') order_by_last_name = query.get('order_by_last_name')
page = request.args.get('page') page = query.get('page')
student_query = Student.search_by_fullname_and_order_by_first_name_or_last_name(fullname, order_by_first_name, student_query = Student.search_by_fullname_and_order_by_first_name_or_last_name(fullname, order_by_first_name,
order_by_last_name) order_by_last_name)
response = paginate_models(page, student_query) response = paginate_models(page, student_query)
if isinstance(response, tuple): if (message := response.get('message')) is not None:
return response abort(response['status_code'], message)
# print(get_debug_queries()[0]) # print(get_debug_queries()[0])
return {"students": students_schema.dump(response['items']), "max_pages": response['max_pages']}, 200 return {
"students": response['items'],
"max_pages": response['max_pages']
}
@bp.route("/<int:index>/", methods=["GET"]) @bp.route("/<int:index>/", methods=["GET"])
def detail_student(index: int) -> Tuple[dict, int]: @bp.output(StudentSchema)
student_schema = StudentSchema() def detail_student(index: int) -> Student:
student = Student.query.filter_by(index=index).first() student = Student.query.filter_by(index=index).first()
if student is not None: if student is None:
return student_schema.dump(student), 200 abort(404, f"Student with {index} index doesn't exist!")
return {"error": f"Student with {index} index doesn't exist!"}, 404 return student
@bp.route("/<int:index>/", methods=["DELETE"]) @bp.route("/<int:index>/", methods=["DELETE"])
def delete_student(index: int) -> Tuple[dict, int]: @bp.output(MessageSchema, status_code=202)
def delete_student(index: int) -> dict:
student = Student.query.filter_by(index=index).first() student = Student.query.filter_by(index=index).first()
if student is not None: if student is None:
db.session.delete(student) abort(404, f"Student with {index} index doesn't exist!")
db.session.commit() db.session.delete(student)
return {"message": "Student was deleted!"}, 202 db.session.commit()
return {"error": f"Student with {index} index doesn't exist!"}, 404 return {"message": "Student was deleted!"}
@bp.route("/<int:index>/", methods=["PUT"]) @bp.route("/<int:index>/", methods=["PUT"])
def edit_student(index: int) -> Tuple[dict, int]: @bp.input(StudentEditSchema)
request_data = request.get_json() @bp.output(MessageSchema)
student_schema = StudentEditSchema() def edit_student(index: int, data: dict) -> dict:
try: try:
data = student_schema.load(request_data)
if not data: if not data:
return {"error": "You have passed empty data!"}, 400 abort(400, 'You have passed empty data!')
student_query = Student.query.filter_by(index=index) student_query = Student.query.filter_by(index=index)
student = student_query.first() student = student_query.first()
if student is None: if student is None:
return {"error": "Not Found student!"}, 404 abort(404, 'Not found student!')
student_query.update(data) student_query.update(data)
db.session.commit() db.session.commit()
except ValidationError as e: except ValidationError as e:
return {"error": e.messages}, 422 abort(422, e.messages)
return {"message": "Student was updated!"}, 200 return {"message": "Student was updated!"}
@bp.route("/", methods=["POST"]) @bp.route("/", methods=["POST"])
def create_student() -> Tuple[dict, int] or Response: @bp.input(StudentCreateSchema)
request_data = request.get_json() @bp.output(MessageSchema)
student_schema = StudentCreateSchema() def create_student(data: dict) -> dict:
try: try:
data = student_schema.load(request_data)
index = data['index'] index = data['index']
student = Student.query.filter_by(index=index).first() student = Student.query.filter_by(index=index).first()
if student is not None: if student is not None:
return {"error": "Student has already exists!"}, 400 abort(400, "Student has already exists!")
dummy_email = f'student{randint(1, 300_000)}@gmail.com' dummy_email = f'student{randint(1, 300_000)}@gmail.com'
student = Student(**data, email=dummy_email) student = Student(**data, email=dummy_email)
db.session.add(student) db.session.add(student)
db.session.commit() db.session.commit()
except ValidationError as e: except ValidationError as e:
return {'error': e.messages}, 422 abort(422, e.messages)
return {"message": "Student was created!"}, 200 return {"message": "Student was created!"}
@bp.route("/upload/", methods=["POST"]) @bp.route("/upload/", methods=["POST"])
def upload_students() -> Tuple[dict, int]: @bp.input(FileSchema, location='form_and_files')
"""Maybe in the future move to celery workers""" @bp.output(MessageSchema)
if (uploaded_file := request.files.get('file')) is None or uploaded_file.filename == '': def upload_students(file: dict) -> dict:
return {"error": "You didn't attach a csv file!"}, 400 uploaded_file = file.get('file')
if uploaded_file and is_allowed_extensions(uploaded_file.filename): if uploaded_file and is_allowed_extensions(uploaded_file.filename):
try: try:
students = parse_csv(uploaded_file) students = parse_csv(uploaded_file)
except InvalidNameOrTypeHeaderException:
return {"error": "Invalid format of csv file!"}, 400
try:
while True: while True:
sliced_students = islice(students, 5) sliced_students = islice(students, 5)
list_of_students = list(sliced_students) list_of_students = list(sliced_students)
@ -120,11 +117,13 @@ def upload_students() -> Tuple[dict, int]:
break break
db.session.add_all(list_of_students) db.session.add_all(list_of_students)
db.session.commit() db.session.commit()
except InvalidNameOrTypeHeaderException:
abort(400, "Invalid format of csv file!")
except IntegrityError as e: except IntegrityError as e:
# print(e) # print(e)
# in the future create sql query checks index and add only these students, which didn't exist in db # in the future create sql query checks index and add only these students, which didn't exist in db
return {"error": "These students have already exist!"}, 400 abort(400, "These students have already exist!")
else: else:
return {"error": "Invalid extension of file"}, 400 abort(400, "Invalid extension of file")
return {"message": "Students was created by uploading csv file!"}, 200 return {"message": "Students was created by uploading csv file!"}

View File

@ -23,6 +23,11 @@ class StudentSchema(ma.SQLAlchemyAutoSchema):
model = Student model = Student
class StudentsPaginationSchema(ma.Schema):
students = fields.List(fields.Nested(StudentSchema))
max_pages = fields.Integer()
class StudentCreateSchema(ma.Schema): class StudentCreateSchema(ma.Schema):
first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True) first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True) last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
@ -35,3 +40,18 @@ class StudentEditSchema(ma.Schema):
last_name = fields.Str(validate=validate.Length(min=1, max=255)) last_name = fields.Str(validate=validate.Length(min=1, max=255))
index = fields.Integer(validate=validate_index) index = fields.Integer(validate=validate_index)
mode = fields.Boolean() mode = fields.Boolean()
class MessageSchema(ma.Schema):
message = fields.Str(required=True)
class FileSchema(ma.Schema):
file = fields.Raw(type='file', required=True)
class StudentQuerySchema(ma.Schema):
fullname = fields.Str()
order_by_first_name = fields.Str()
order_by_last_name = fields.Str()
page = fields.Integer()

View File

@ -1,7 +1,18 @@
import json
from typing import Tuple from typing import Tuple
from werkzeug.exceptions import RequestEntityTooLarge from apiflask import APIFlask
from werkzeug.exceptions import RequestEntityTooLarge, HTTPException
def request_entity_too_large(error: RequestEntityTooLarge) -> Tuple[dict, int]: def request_entity_too_large(error: RequestEntityTooLarge) -> Tuple[dict, int]:
return {'error': 'File too large!'}, 413 return {'error': 'File too large!'}, 413
def register_error_handlers(app: APIFlask):
@app.errorhandler(HTTPException)
def handle_http_exception(e):
response = e.get_response()
response.data = json.dumps({'error': e.description})
response.content_type = 'application/json'
return response

View File

@ -35,3 +35,4 @@ SQLAlchemy==1.4.36
tomli==2.0.1 tomli==2.0.1
Werkzeug==2.1.2 Werkzeug==2.1.2
zipp==3.8.0 zipp==3.8.0
apiflask==1.0.2