Пример¶
Пример использования Flask-COMBO-JSONAPI API с ComboJSONAPI
from flask import Flask
from marshmallow import pre_load
from combojsonapi.utils import Relationship
from combojsonapi.spec import ApiSpecPlugin
from combojsonapi.permission.permission_system import (
PermissionMixin,
PermissionForGet,
PermissionUser,
)
from combojsonapi.permission import PermissionPlugin
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
from flask_sqlalchemy import SQLAlchemy
from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields
# 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-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 Person(db.Model):
class Meta:
required_fields = {
# OPTIONAL BUT RECOMMENDED
# when using sparse fields (`GET /persons?fields[person]=full_name,email`)
# when serialising obj, property `full_name` will use fields `first_name` and `last_name`
# and they will be loaded one by one
# BUT IF YOU USE PERMISSION PLUGIN
# you can fix it by declaring `required_fields`
# in format {"field_name": ["dependant field one", "another dependant field"]}
"full_name": ["first_name", "last_name"],
}
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String)
last_name = db.Column(db.String)
email = db.Column(db.String)
password = db.Column(db.String)
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
class Computer(db.Model):
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String)
person_id = db.Column(db.Integer, db.ForeignKey("person.id"))
person = db.relationship("Person", backref=db.backref("computers"))
db.create_all()
# Create logical data abstraction (same as data storage for this first example)
class PersonSchema(Schema):
class Meta:
type_ = "person"
self_view = "person_detail"
self_view_kwargs = {"id": "<id>"}
self_view_many = "person_list"
id = fields.Integer(as_string=True, dump_only=True)
first_name = fields.String(required=True)
last_name = fields.String(required=True)
full_name = fields.String(required=True, dump_only=True)
email = fields.Email()
computers = Relationship(
nested="ComputerSchema",
schema="ComputerSchema",
self_view="person_detail",
self_view_kwargs={"id": "<id>"},
related_view="computer_list",
many=True,
type_="computer",
)
@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
class ComputerSchema(Schema):
class Meta:
type_ = "computer"
self_view = "computer_detail"
self_view_kwargs = {"id": "<id>"}
self_view_many = "computer_list"
id = fields.Integer(as_string=True, dump_only=True)
serial = fields.String(required=True)
owner = Relationship(
nested="PersonSchema",
schema="PersonSchema",
attribute="person",
self_view="computer_detail",
self_view_kwargs={"id": "<id>"},
related_view="person_detail",
related_view_kwargs={"int": "<id>"},
type_="person",
)
@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 Permissions
class BaseAllowAllFieldsPermission(PermissionMixin):
ALL_FIELDS = []
def get(
self, *args, many=True, user_permission: PermissionUser = None, **kwargs
) -> PermissionForGet:
"""Setting all available columns"""
self.permission_for_get.allow_columns = (self.ALL_FIELDS, 10)
return self.permission_for_get
class PersonsPermission(BaseAllowAllFieldsPermission):
ALL_FIELDS = [
"id",
"first_name",
"last_name",
"full_name",
"email",
"computers",
]
class ComputersPermission(BaseAllowAllFieldsPermission):
ALL_FIELDS = [
"id",
"serial",
"person",
"owner",
]
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {
"session": db.session,
"model": Person,
"permission_get": [PersonsPermission],
}
class PersonDetail(ResourceDetail):
schema = PersonSchema
data_layer = {
"session": db.session,
"model": Person,
"permission_get": [PersonsPermission],
}
class ComputerList(ResourceList):
schema = ComputerSchema
data_layer = {
"session": db.session,
"model": Computer,
"permission_get": [ComputersPermission],
}
class ComputerDetail(ResourceDetail):
schema = ComputerSchema
data_layer = {
"session": db.session,
"model": Computer,
"permission_get": [ComputersPermission],
}
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={
"Person": "Person API",
"Computer": "Computer API",
},
)
# Create endpoints
api = Api(
app,
plugins=[
api_spec_plugin,
PermissionPlugin(strict=False),
],
)
api.route(PersonList, "person_list", "/persons", tag="Person")
api.route(PersonDetail, "person_detail", "/persons/<int:id>", tag="Person")
api.route(ComputerList, "computer_list", "/computers", tag="Computer")
api.route(ComputerDetail, "computer_detail", "/computers/<int:id>", tag="Computer")
if __name__ == "__main__":
# Start application
app.run(debug=True)
Предупреждение
В этом примере используется Flask-SQLAlchemy.Так что для запуска примера будет необходимо установить эту библиотеку
$ pip install flask_sqlalchemy
Сохраните этот файл как api.py и запустите. В примере активен дебаг режим Flask debugging для авторестарта и более подробных ошибок.
$ python api.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
Предупреждение
Никогда не используйте дебаг режим в продакшене!
Классические CRUD операции¶
Создание объекта¶
Запрос:
POST /persons HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"attributes": {
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com"
}
}
}
Ответ:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Получить объект¶
Запрос:
GET /persons/1 HTTP/1.1
Content-Type: application/vnd.api+json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Получить объекты¶
Запрос:
GET /persons HTTP/1.1
Content-Type: application/vnd.api+json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"email": "john.smith@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/persons"
},
"meta": {
"count": 1
}
}
Выборка полей¶
Необходимо отметить, что мы указали SQLAlchemy что поля full_name использует поля first_name и first_name. Если мы не объявим эту зависимость в Meta.required_fields модели, то во время сериализации каждая модель выполнит по два запроса в БД чтобы получить эти поля.
Это происходит потому, что PermissionPlugin убирает невостребованные поля из SQL запроса в пользу производительности. А Meta.required_fields подскажет плагину PermissionPlugin какие поля необходимо загрузить даже если они не были запрошены напрямую.
Запрос:
GET /persons?fields[person]=full_name,email HTTP/1.1
Content-Type: application/vnd.api+json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"email": "john.smith@example.com",
"full_name": "John Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/persons?fields%5Bperson%5D=full_name%2Cemail"
},
"meta": {
"count": 1
}
}
Связи¶
Создать компьютер¶
Запрос:
POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"attributes": {
"serial": "Amstrad"
}
}
}
Ответ:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"type": "computer"
},
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/computers/1"
}
}
Обновить атрибуты и связи персоны в одном запросе¶
Запрос:
PATCH /persons/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"email": "john@example.com"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "1"
}
]
}
}
}
}
Ответ:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"email": "john@example.com",
"first_name": "John",
"full_name": "John Smith",
"last_name": "Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"relationships": {
"computers": {
"data": [
{
"id": "1",
"type": "computer"
}
]
}
},
"type": "person"
},
"included": [
{
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"type": "computer"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
Получить компьютеры вместе с владельцами (связь person), а также выбрать отдельные поля в том же запросе¶
Запрос:
GET /computers?include=owner&fields[person]=full_name,email HTTP/1.1
Content-Type: application/vnd.api+json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"serial": "Amstrad"
},
"id": "1",
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"data": {
"id": "1",
"type": "person"
}
}
},
"type": "computer"
}
],
"included": [
{
"attributes": {
"email": "john@example.com",
"full_name": "John Smith"
},
"id": "1",
"links": {
"self": "/persons/1"
},
"type": "person"
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost:5000/computers?include=owner&fields%5Bperson%5D=email%2Cfull_name"
},
"meta": {
"count": 1
}
}