Events Example¶
EventPlugin example for Flask-COMBO-JSONAPI API with ComboJSONAPI:
from pathlib import Path
from flask import Flask
from flask_combo_jsonapi.exceptions import ObjectNotFound, BadRequest
from marshmallow import pre_load
from combojsonapi.event import EventPlugin
from combojsonapi.event.resource import EventsResource
from combojsonapi.spec import ApiSpecPlugin
from flask import request
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
from flask_sqlalchemy import SQLAlchemy
from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields
CURRENT_DIR = Path(__file__).resolve().parent
UPLOADS_DIR_NAME = Path("uploads")
UPLOADS_DIR = CURRENT_DIR / UPLOADS_DIR_NAME
UPLOADS_DIR.mkdir(exist_ok=True)
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif"}
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
# Create the Flask application
app = Flask(__name__)
app.config["DEBUG"] = True
# Initialize SQLAlchemy
app.config["SQLALCHEMY_ECHO"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/api-events-example.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
# swagger / openapi config
app.config["OPENAPI_URL_PREFIX"] = "/api/swagger"
app.config["OPENAPI_VERSION"] = "3.0.0"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/"
app.config["OPENAPI_SWAGGER_UI_VERSION"] = "3.45.0"
# Create models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
username = db.Column(db.String)
email = db.Column(db.String)
avatar_path = db.Column(db.String)
db.create_all()
# Create logical data abstraction (same as data storage for this first example)
class UserSchema(Schema):
class Meta:
type_ = "user"
self_view = "user_detail"
self_view_kwargs = {"id": "<id>"}
self_view_many = "user_list"
id = fields.Integer(as_string=True, dump_only=True)
name = fields.String(required=False)
username = fields.String(required=True)
email = fields.String(required=True)
avatar_path = fields.String(required=False, dump_only=True)
@pre_load
def remove_id_before_deserializing(self, data, **kwargs):
"""
We don't want to allow editing ID on POST / PATCH
Related issues:
https://github.com/AdCombo/flask-combo-jsonapi/issues/34
https://github.com/miLibris/flask-rest-jsonapi/issues/193
"""
if "id" in data:
del data["id"]
return data
# Create events
class UserResourceListEvents(EventsResource):
def event_get_info(self, *args, **kwargs):
return {"message": "some info"}
def event_post_info(self, *args, **kwargs):
data = request.json
data.update(message="POST request info")
return data
class UserResourceDetailEvents(EventsResource):
def event_update_avatar(self, *args, id: int = None, **view_kwargs):
# language=YAML
"""
---
summary: Update user's avatar
tags:
- User
parameters:
- in: path
name: id
required: True
type: integer
format: int32
description: "user's id"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
new_avatar:
type: string
format: binary
consumes:
- multipart/form-data
responses:
201:
content:
application/json:
schema:
type: object
properties:
avatar_url:
type: string
example: "uploads/avatar.gif"
"""
avatar = request.files.get("new_avatar")
if not avatar:
raise BadRequest("avatar file is required! please fill `new_avatar` form field")
user = User.query.filter_by(id=id).one_or_none()
if user is None:
raise ObjectNotFound(
"User #{} not found".format(id),
source={"parameter": "id"},
)
filename = avatar.filename
avatar_path = str(UPLOADS_DIR / filename)
avatar.save(avatar_path)
user.avatar_path = str(UPLOADS_DIR_NAME / filename)
db.session.commit()
return {"avatar_url": user.avatar_path}, 201
event_update_avatar.extra = {
"url_suffix": "update_avatar",
}
# Create resource managers
class UserList(ResourceList):
schema = UserSchema
events = UserResourceListEvents
data_layer = {
"session": db.session,
"model": User,
}
class UserDetail(ResourceDetail):
schema = UserSchema
events = UserResourceDetailEvents
data_layer = {
"session": db.session,
"model": User,
}
api_spec_plugin = ApiSpecPlugin(
app=app,
# Declaring tags list with their descriptions, so API gets organized into groups.
# This is optional: when there are no tags,
# api will be grouped automatically by type schemas names (type_)
tags={
"User": "User API",
},
)
# Create endpoints
api = Api(
app,
plugins=[
api_spec_plugin,
EventPlugin(trailing_slash=False),
],
)
api.route(UserList, "user_list", "/users", tag="User")
api.route(UserDetail, "user_detail", "/users/<int:id>", tag="User")
if __name__ == "__main__":
# Start application
app.run(debug=True)
Warning
In this example Flask-SQLAlchemy is used, so you’ll need to install it before running this example.
$ pip install flask_sqlalchemy
Save this file as api.py and run it using your Python interpreter. Note that we’ve enabled Flask debugging mode to provide code reloading and better error messages.
$ python api.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
Warning
Debug mode should never be used in a production environment!
Classical CRUD operations¶
Create user object¶
Request:
POST /users HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "user",
"attributes": {
"name": "John Smith",
"username": "johny",
"email": "john@example.com"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"avatar_path": null,
"email": "john@example.com",
"name": "John Smith",
"username": "johny"
},
"id": "1",
"links": {
"self": "/users/1"
},
"type": "user"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/users/1"
}
}
Get custom event info¶
Request:
GET /users/event_get_info HTTP/1.1
Content-Type: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"jsonapi": {
"version": "1.0"
},
"message": "some info"
}
Post custom event info¶
Request:
POST /users/event_post_info HTTP/1.1
Content-Type: application/vnd.api+json
{
"spam": "eggs",
"foo": "bar"
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"foo": "bar",
"jsonapi": {
"version": "1.0"
},
"message": "POST request info",
"spam": "eggs"
}
Update user’s avatar¶
Upload file using multipart/form-data
Request:
POST /users/1/update_avatar HTTP/1.1
Content-Type: multipart/form-data; boundary=WebKitFormBoundaryQwe
------WebKitFormBoundaryQwe
Content-Disposition: form-data; name="new_avatar"; filename="my-avatar.gif"
Content-Type: image/gif
GIF89a!
,L;
------WebKitFormBoundaryQwe--
Response:
HTTP/1.1 201
Content-Type: application/json, application/vnd.api+json
{
"avatar_url": "uploads/my-avatar.gif",
"jsonapi": {
"version": "1.0"
}
}