Пример

Пример использования 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
  }
}