Back
Featured image of post Flask-REST-JSONAPI官方文档翻译

Flask-REST-JSONAPI官方文档翻译

快速构建REST API的Flask插件。

00-简介

Flask-REST-JSONAPI 是一款快速构建REST API的Flask插件。框架在遵循JSONAPI 1.0规范的同时,保持着极强的灵活性。框架设计之初,就考虑到了生产环境的复杂性,所以在Flask-REST-JSONAPI中就引入了逻辑数据抽象层的概念,框架中叫做Resource(资源)。Resource可以通过Data Layer(数据层) 接入任何ORM框架或数据库。(译者注:逻辑数据对应的是物理数据,数据库储存的是物理数据,但有时API返回的数据,不能直接返回数据库的物理数据,需要进行逻辑加工后,才能返回给用户。)

核心概念

schema.png
schema.png

  • JSON API 1.0 规范(http://jsonapi.org/):广受欢迎的REST JSON API规范,规定了客户端与服务端进行数据交互的方式。遵循规范有助于进行团队合作开发,因为规范的表达十分精确,且易于分享。正是由于规范,API也包含了许多特性,像健壮的request和response结构、过滤、分页、稀疏字段、外键查询以及强大的错误格式化等。

  • 逻辑数据抽象层:实际开发中,我们需要返回数据资源给客户端,但通常来说,返回给用户的数据并与数据库的表结构完全相同。比如,有时我们不想暴露锁一个表的所有属性,亦或需要计算额外的属性,亦或创建资源的数据来自于不同的数据库。Flask-REST-JSONAPI 通过Marshmallow / marshmallow-jsonapi 为我们创建逻辑数据抽象层,便于你以灵活的方式暴露你的数据。

  • 数据层:数据层在框架中作为数据CRUD的接口,将资源和数据的连接起来。正是由于引入了数据层,我们能使用任何数据库或者ORM框架来对接资源。框架已经实现了SQLALchemy ORM的所有功能,但同时我们也可以定制数据层,来接入自己数据库的数据。我们还可以在数据层接入多个数据库和ORM,发送通知或在CRUD过程中加入定制功能。

特性

Flask-REST-JSONAPI 拥有的特性包括:

  • 关系数据管理

  • 强大的过滤功能

  • 外键包含查询

  • 稀疏字段

  • 分页

  • 排序

  • 权限管理

  • OAuth支持

用户指南

以下文档演示了如何配合Flask使用Flask-REST-JSONAPI。

API参考

如果你想要查找函数、类或方法的具体信息,以下文档可供你参考。


01-安装

使用pip安装Flask-REST-JSONAPI

$ pip install flask-rest-jsonapi

开发版本可以在its page at GitHub下载。

git clone https://github.com/miLibris/flask-rest-jsonapi.git
cd flask-rest-jsonapi
mkvirtualenv venv
python setup.py install

提醒

如果你没有安装virtualenv,请先安装它

 $ pip install virtualenv

Flask-REST-JSONAPI 需要Python 2.7或3.4+。

02-快速上手

现在我们就来写第一个REST API。这个教程会假设你是熟悉Flask的,并且确保你已经安装了Flask和Flask-REST-JSONAPI。如果还没有的话,可以先查看安装章节的教程。

本章节,我们会围绕实际的例子和简短的教程,来介绍Flask-REST-JSONAPI的基础使用方法。本教程的数据层,我们将选用默认的Flask-REST-JSONAPI数据层SQLAlchemy。下面,我们就来演示person和computers的API例子。

第一个例子

如下所示,代码构建了Flask-REST-JSONAPI的API:

# -*- coding: utf-8 -*-

from flask import Flask
from flask_rest_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
from flask_rest_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields

# Create the Flask application
app = Flask(__name__)
app.config['DEBUG'] = True


# Initialize SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)


# Create data storage
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    email = db.Column(db.String)
    birth_date = db.Column(db.Date)
    password = db.Column(db.String)


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)
    name = fields.Str(required=True, load_only=True)
    email = fields.Email(load_only=True)
    birth_date = fields.Date()
    display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
    computers = Relationship(self_view='person_computers',
                             self_view_kwargs={'id': '<id>'},
                             related_view='computer_list',
                             related_view_kwargs={'id': '<id>'},
                             many=True,
                             schema='ComputerSchema',
                             type_='computer')


class ComputerSchema(Schema):
    class Meta:
        type_ = 'computer'
        self_view = 'computer_detail'
        self_view_kwargs = {'id': '<id>'}

    id = fields.Integer(as_string=True, dump_only=True)
    serial = fields.Str(required=True)
    owner = Relationship(attribute='person',
                         self_view='computer_person',
                         self_view_kwargs={'id': '<id>'},
                         related_view='person_detail',
                         related_view_kwargs={'computer_id': '<id>'},
                         schema='PersonSchema',
                         type_='person')


# Create resource managers
class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}


class PersonDetail(ResourceDetail):
    def before_get_object(self, view_kwargs):
        if view_kwargs.get('computer_id') is not None:
            try:
                computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
            except NoResultFound:
                raise ObjectNotFound({'parameter': 'computer_id'},
                                     "Computer: {} not found".format(view_kwargs['computer_id']))
            else:
                if computer.person is not None:
                    view_kwargs['id'] = computer.person.id
                else:
                    view_kwargs['id'] = None

    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person,
                  'methods': {'before_get_object': before_get_object}}


class PersonRelationship(ResourceRelationship):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}


class ComputerList(ResourceList):
    def query(self, view_kwargs):
        query_ = self.session.query(Computer)
        if view_kwargs.get('id') is not None:
            try:
                self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            except NoResultFound:
                raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
            else:
                query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
        return query_

    def before_create_object(self, data, view_kwargs):
        if view_kwargs.get('id') is not None:
            person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            data['person_id'] = person.id

    schema = ComputerSchema
    data_layer = {'session': db.session,
                  'model': Computer,
                  'methods': {'query': query,
                              'before_create_object': before_create_object}}


class ComputerDetail(ResourceDetail):
    schema = ComputerSchema
    data_layer = {'session': db.session,
                  'model': Computer}


class ComputerRelationship(ResourceRelationship):
    schema = ComputerSchema
    data_layer = {'session': db.session,
                  'model': Computer}


# Create endpoints
api = Api(app)
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>', '/computers/<int:computer_id>/owner')
api.route(PersonRelationship, 'person_computers', '/persons/<int:id>/relationships/computers')
api.route(ComputerList, 'computer_list', '/computers', '/persons/<int:id>/computers')
api.route(ComputerDetail, 'computer_detail', '/computers/<int:id>')
api.route(ComputerRelationship, 'computer_person', '/computers/<int:id>/relationships/owner')

if __name__ == '__main__':
    # Start application
    app.run(debug=True)

这个例子提供了以下API接口:

url method endpoint action
/persons GET person_list 获取persons集合
/persons POST person_list 创建一个person
/persons/int:id GET person_detail 获取一个person的详情
/persons/int:id PATCH person_detail 更新一个person
/persons/int:id DELETE person_detail 删除一个person
/persons/int:id/computers GET computer_list 获取同某个person关联的computers集合
/persons/int:id/computers POST computer_list 创建一个computer,并将其关联到某个person
/persons/int:id/relationship/computers GET person_computers 获取person与computers的关系
/persons/int:id/relationship/computers POST person_computers 创建person与computers的关系
/persons/int:id/relationship/computers PATCH person_computers 更新person与computers的关系
/persons/int:id/relationship/computers DELETE person_computers 删除person与computers的关系
/computers GET computer_list 获取computers集合
/computers POST computer_list 创建一个computer
/computers/int:id GET computer_detail 获取一个computer的详情
/computers/int:id PATCH computer_detai 更新一个computer
/computers/int:id DELETE computer_detail 删除一个computer
/computers/int:id/owner GET person_detail 获取某个computer的owner的详情
/computers/int:id/owner PATCH person_detail 更新一个computer的owner
/computers/int:id/owner DELETE person_detail 删除一个computer的owner
/computers/int:id/relationship/owner GET person_computers 获取person与computer的关系
/computers/int:id/relationship/owner POST person_computers 创建person与computer的关系
/computers/int:id/relationship/owner PATCH person_computers 更新person与computer的关系
/computers/int:id/relationship/owner DELETE person_computers 删除person与computer的关系

警告

本例中,我们使用了Flask-SQLAlchemy,所以确保你运行前已经安装。

$ pip install flask_sqlalchemy

保存这个文件到api.py,然后用Python运行。请注意代码中我们开启了 Flask debugging 模式,这样代码就在修改后自动重载,并且能更好地提示报错信息。

$ python api.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader

警告

请在生产环境中关闭Debug模式。

经典的CRUD操作

创建对象

Request:

POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "attributes": {
      "serial": "Amstrad"
    }
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "id": "1",
    "attributes": {
      "serial": "Amstrad"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "links": {
      "self": "/computers/1"
    }
  },
  "links": {
    "self": "/computers/1"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

获取对象列表

Request:

GET /computers HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "1",
      "attributes": {
        "serial": "Amstrad"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/1"
      }
    }
  ],
  "meta": {
    "count": 1
  },
  "links": {
    "self": "/computers"
  },
  "jsonapi": {
    "version": "1.0"
  },
}

更新对象

Request:

PATCH /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "id": "1",
    "attributes": {
      "serial": "Amstrad 2"
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "computer",
    "id": "1",
    "attributes": {
      "serial": "Amstrad 2"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "links": {
      "self": "/computers/1"
    }
  },
  "links": {
    "self": "/computers/1"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

删除对象

Request:

DELETE /computers/1 HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "meta": {
    "message": "Object successfully deleted"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

关系

现在我们来看一下关系的使用。第一步,如同上例中,分别创建3个名叫Halo,Nestor和Comodor的电脑。

完成了?接下来,我们继续。

我们假设Halo的id是2,Nestor的id是3,Comodor的id是4。

创建带有关系的对象

Request:

POST /persons?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "person",
    "attributes": {
      "name": "John",
      "email": "john@gmail.com",
      "birth_date": "1990-12-18"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "1"
          }
        ]
      }
    }
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JOHN <john@gmail.com>",
      "birth_date": "1990-12-18"
    },
    "links": {
      "self": "/persons/1"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "1",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
  },
  "included": [
    {
      "type": "computer",
      "id": "1",
      "attributes": {
        "serial": "Amstrad"
      },
      "links": {
        "self": "/computers/1"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      }
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/persons/1"
  }
}

你可以看到,我在url中添加了查询语句参数“include”。

POST /persons?include=computers HTTP/1.1

正是使用了这个参数,在创建人的同时,也允许添加了关联的电脑。如果你想了解更多细节,请查看包含关联对象章节。

更新对象及其关系

现在,John卖了他的电脑Amstrad,同时买了台新电脑Nestor(id:3)。所以我们需要关联这个新电脑给John。另外,John还填错了生日,所以我们现在需要同时更新两个数据。

Request:

PATCH /persons/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "birth_date": "1990-10-18"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "3"
          }
        ]
      }
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JOHN <john@gmail.com>",
      "birth_date": "1990-10-18",
    },
    "links": {
      "self": "/persons/1"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "3",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
  },
  "included": [
    {
      "type": "computer",
      "id": "3",
      "attributes": {
        "serial": "Nestor"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/3"
      }
    }
  ],
  "links": {
    "self": "/persons/1"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

创建关系

现在John买了台叫Comodor的新电脑,让我们把他关联给John。

Request:

POST /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "4"
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JOHN <john@gmail.com>",
      "birth_date": "1990-10-18"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "3",
            "type": "computer"
          },
          {
            "id": "4",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
    "links": {
      "self": "/persons/1"
    }
  },
  "included": [
    {
      "type": "computer",
      "id": "3",
      "attributes": {
        "serial": "Nestor"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/3"
      }
    },
    {
      "type": "computer",
      "id": "4",
      "attributes": {
        "serial": "Comodor"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/4/owner",
            "self": "/computers/4/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/4"
      }
    }
  ],
  "links": {
    "self": "/persons/1/relationships/computers"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

删除关系

现在,John卖了旧的Nestor电脑,让我们给John移除关联。

Request:

DELETE /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "3"
    }
  ]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JOHN <john@gmail.com>",
      "birth_date": "1990-10-18"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "4",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
    "links": {
      "self": "/persons/1"
    }
  },
  "included": [
    {
      "type": "computer",
      "id": "4",
      "attributes": {
        "serial": "Comodor"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/4/owner",
            "self": "/computers/4/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/4"
      }
    }
  ],
  "links": {
      "self": "/persons/1/relationships/computers"
  },
  "jsonapi": {
      "version": "1.0"
  }
}

如果你想了解更多例子,请访问 JSON API 1.0 specification

03-逻辑数据抽象层

当开始一个项目时,我们要做的第一件事就是创建逻辑数据抽象层。这一步,是为了描述Schema(资源结构),它表示哪些数据是完全映射数据库的数据结构,哪些是不完全映射。声明资源结构是使用Marshmallow / marshmallow-jsonapi框架。Marshmallow是一个非常著名的序列化和反序列化的框架,它提供了许多可以用来抽象数据结构的特性;marshmallow-jsonapi则更进一步实现了JSONAPI 1.0规范,并且支持Flask集成。

例:

本例中,我们假设已经有了Person和Computer的ORM model,我们需要为它们创建数据抽象层。

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    email = db.Column(db.String)
    birth_date = db.Column(db.String)
    password = db.Column(db.String)


class Computer(db.Model):
    computer_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'))

接下来,让我们创建逻辑数据抽象层,来解释model与资源的映射关系。

from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields

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)
    name = fields.Str(required=True, load_only=True)
    email = fields.Email(load_only=True)
    birth_date = fields.Date()
    display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
    computers = Relationship(self_view='person_computers',
                             self_view_kwargs={'id': '<id>'},
                             related_view='computer_list',
                             related_view_kwargs={'id': '<id>'},
                             many=True,
                             schema='ComputerSchema',
                             type_='computer',
                             id_field='computer_id')


class ComputerSchema(Schema):
    class Meta:
        type_ = 'computer'
        self_view = 'computer_detail'
        self_view_kwargs = {'id': '<id>'}

    id = fields.Str(as_string=True, dump_only=True, attribute='computer_id')
    serial = fields.Str(required=True)
    owner = Relationship(attribute='person',
                         self_view='computer_person',
                         self_view_kwargs={'id': '<id>'},
                         related_view='person_detail',
                         related_view_kwargs={'computer_id': '<id>'},
                         schema='PersonSchema',
                         type_='person')

我们能看到数据库model 与 schema的几个区别。

首先,让我们来比较下Person和PersonSchema:

  • 我们能看到Person有password字段,但我们不想在API中暴露它,所以在PersonSchema中,没有设定该属性。
  • PersonSchema中,有一个display_name属性,该属性实际上是拼接了name和email字段。
  • 在PersonSchema中,我们定义了一个computers=Relationship(…),我们设定了id_field=‘computer_id’,因为它是Computer(db.model)的主键。如果你没有设定id_field,那么它的默认值就是‘id’。

其次,让我们再来比较下Computer和ComputerSchema:

  • computer_id字段,在api中,是以id属性呈现的。
  • Computer与Person的关联字段person,在api中,是以owner属性呈现的,因为这样表达更直观。

最终,可以发现,基于数据库中的数据结构,我们能以非常灵活的方式,选择性地暴露数据。

04-资源管理器

资源管理器,起着连接器的作用,它连接着数据抽象层、数据层以及其他可选的功能。其内部可以管理资源的各项逻辑。

Flask-REST-JSONAPI提供了三种资源管理器,实现了JSONAPI 1.0规范的默认方法。

  • ResourceList:提供GET和POST方法,分别用于读取资源列表和创建资源。
  • ResourceDetail:提供GET、PATCH和DELETE方法,分别用于读取资源详情、更新资源以及删除资源。
  • ResourceRelationship:提供GET、POST、PATCH和DELETE方法,对关系进行增删改查。

我们能重写任意的默认方法,来定制功能。整个资源管理器是保持解耦的,我们可以在不配置其他属性的情况下,对所有方法重写、对某个方法重写,亦或禁用某个方法。

必选属性

如果我们想使用资源管理器的默认方法实现,我们必须设置两个属性:schema和data_layer。

  • schema: 逻辑数据抽象层,它必须继承自marshmallow_jsonapi.schema.Schema。
  • data_layer: 这个属性用于初始化你的数据层。如果想了解更多,请查看数据层文档。

例:

from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db

class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}

可选属性

所有的资源管理器都继承自flask.views.MethodView,所以我们能在资源管理器中,使用Flask的可选属性。

  • methods: 资源管理器可用的HTTP方法,如果没指定,那么所有方法都可用。
  • decorators: 针对所有方法的装饰器元组。

另外,我们还能给资源管理器的每个HTTP方法,设定不同schema的参数:

  • get_schema_kwargs: GET方法的schema默认字典参数
  • post_schema_kwargs: POST方法的schema默认字典参数
  • patch_schema_kwargs: PATCH方法的schema默认字典参数
  • delete_schema_kwargs: DELETE方法的schema默认字典参数

同时,每个方法,都有一对前置和后置处理方法。前置处理方法,会将view args和kwarg作为参数;后置处理方法,会将返回的结果作为参数。正是有了这对方法,我们才能对前置和后置做定制化的处理。可以重写的方法如下:

  • before_get: GET的前置处理方法
  • after_get: GET的后置处理方法
  • before_post: POST的前置处理方法
  • after_post: POST的后置处理方法
  • before_patch: PATCH的前置处理方法
  • after_patch: PATCH的后置处理方法
  • before_delete: DELETE的前置处理方法
  • after_delete: DELETE的后置处理方法

ResourceList 资源列表

ResourceList 管理器有它特有的可选参数:

  • view_kwargs: 如果你设置了这个参数为True,那么读取列表url时,就会读取view kwargs的值。如果我们配置了如下路由:/persons/int:id/computers,那么这个值则必须为True。

例:

from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db

class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}

以上为最小的ResourceList实现。它提供了GET和POST方法,用于读取对象集合以及创建对象,同时,它实现了分页、排序、稀疏字段、过滤以及包含关联字段的特性。

如果我们的schema包含关系字段,我们也能够创建对象时,同时创建关联对象。具体例子,请查看快速上手章节。

ResourceDetail 资源详情

例:

from flask_rest_jsonapi import ResourceDetail
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db

class PersonDetail(ResourceDetail):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}

以上为最小的ResourceDetail实现。它提供了GET,PATCH和DELETE方法,用于读取对象详情、更新对象以及删除对象,同时,它实现了所有强大的特性,如稀疏字段和包含关联字段的特性等。

如果我们的schema包含关系字段,我们也能够更新对象时,同时更新关联对象。具体例子,请查看快速上手章节。

ResourceRelationship 资源关系

例:

from flask_rest_jsonapi import ResourceRelationship
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db

class PersonRelationship(ResourceRelationship):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}

以上为最小的ResourceRelationship实现。它提供了GET,POST,PATCH和DELETE方法,用于读取、创建关系、更新关系以及删除关系,同时,它实现了所有强大的特性,如稀疏字段和包含关联字段的特性等。

05-数据层

数据层是资源管理器和数据交互的CRUD接口。它能以非常灵活的方式使用任何ORM或数据储存。我们甚至还能创建基于多个ORM和数据储存的数据层,以此来管理我们的对象。数据层针对对象和关系实现了CRUD接口,同时管理着分页、过滤和排序功能。

Flask-REST-JSONAPI基于知名的ORM SQLAlchemy框架已经实现了一个全功能的数据层。

注:

资源管理器默认的数据层使用的是SQLAlchemy。所以,如果我们要使用它,不必在资源管理器中明确声明。

为了配置数据层,我们必须在资源管理器中设置一些必要参数。

例:

from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person

class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {'session': db.session,
                  'model': Person}

我们也能在资源管理中,给数据层,嵌入额外的方法。这里有两种额外的方法:

  • query(查询方法):“query”方法使用view_kwargs作为参数,并返回替代的查询结果。使用ResourceList资源管理器的GET方法时,会调用query,获取对象集合。
  • pre / post process(前置后置方法):所有CRUD和关系操作都有一对前置/后置处理方法。因此,我们能在数据层的每次操作前和后,添加额外的处理。每个前置/后置方法中,使用的具体参数可以参考基类flask_rest_jsonapi.data_layers.base.Base

例:

from sqlalchemy.orm.exc import NoResultFound
from flask_rest_jsonapi import ResourceList
from flask_rest_jsonapi.exceptions import ObjectNotFound
from your_project.models import Computer, Person

class ComputerList(ResourceList):
    def query(self, view_kwargs):
        query_ = self.session.query(Computer)
        if view_kwargs.get('id') is not None:
            try:
                self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            except NoResultFound:
                raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
            else:
                query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
        return query_

    def before_create_object(self, data, view_kwargs):
        if view_kwargs.get('id') is not None:
            person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            data['person_id'] = person.id

    schema = ComputerSchema
    data_layer = {'session': db.session,
                  'model': Computer,
                  'methods': {'query': query,
                              'before_create_object': before_create_object}}

注:

数据层方法可以不必在资源管理器中声明。我们能在专用的模块或模型中声明它。

例:

from sqlalchemy.orm.exc import NoResultFound
from flask_rest_jsonapi import ResourceList
from flask_rest_jsonapi.exceptions import ObjectNotFound
from your_project.models import Computer, Person
from your_project.additional_methods.computer import before_create_object

class ComputerList(ResourceList):
    schema = ComputerSchema
    data_layer = {'session': db.session,
                  'model': Computer,
                  'methods': {'query': Computer.query,
                              'before_create_object': before_create_object}}

SQLAlchemy

必要参数:

  • session:数据层使用的session连接
  • model:数据层使用的ORM模型

可选参数:

  • id_field:自定义识别列,用于替代模型的主键
  • url_field:自定义路由中的参数名,用于替代默认的参数名id

默认情况下,当我们使用inlcude参数查询时,SQLAlchemy会懒加载关联的数据。如果我们想停用这个特性,我们可以将数据层的eagerload_includes设置为False。

定制数据层

之前提到过,我们也能创建使用我们自己的数据层。定制的数据层必须继承自基类flask_rest_jsonapi.data_layers.base.Base。通过这个基类,我们能看到在数据层中,所有可以定制的内容。

例:

from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.data_layers import MyCustomDataLayer

class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {'class': MyCustomDataLayer,
                  'param_1': value_1,
                  'param_2': value_2}

注:

在资源管理器的data_layer字典中,除了“class”的所有参数,都会作为数据层实例的属性。这样,便于数据层内部进行调用。


06-路由

路由系统很简单,具体可以参考如下框架:

api.route(<Resource manager>, <endpoint name>, <url_1>, <url_2>, ...)

例:

# all required imports are not displayed in this example
from flask_rest_jsonapi import Api

api = Api()
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>', '/computers/<int:computer_id>/owner')
api.route(PersonRelationship, 'person_computers', '/persons/<int:id>/relationships/computers')
api.route(ComputerList, 'computer_list', '/computers', '/persons/<int:id>/computers')
api.route(ComputerDetail, 'computer_detail', '/computers/<int:id>')
api.route(ComputerRelationship, 'computer_person', '/computers/<int:id>/relationships/owner')

07-过滤

Flask-REST-JSONAPI有非常灵活的过滤系统。过滤系统的操作,完全由ResourceList管理器的数据层提供的接口来实现。这里,我们将介绍SQLAlchemy数据层的过滤接口,同样,我们也可以实现定制的数据层过滤接口。过滤功能的使用,我只需要在url中添加“filter”查询参数即可。当然,语句也必须符合JSONAPI 1.0的规范。

注:

为了可读性,以下例子未进行url编码。

SQLAlchemy

SQLAlchemy数据层的过滤系统接口与Flask-Restless提供的完全一样。以下为第一个例子:

GET /persons?filter=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json

在这个例子中,我们过滤得到了名为John的人。我们发现,过滤接口其实就是SQLalchemy原生提供的方法:列表+过滤信息。

  • name: 我们想要过滤的字段名
  • op: 操作符(这要SQLAlchemy提供,就可以使用)
  • val: 我们想要比较的值。我们也可以在此用“field”替换,即另一个字段,来进行比较。
GET /persons?filter=[{"name":"name","op":"eq","field":"birth_date"}] HTTP/1.1
Accept: application/vnd.api+json

在上面的例子中,我们想要过滤人名和生日一样的人。虽然例子比较荒唐,但我们只是为了解释下这种用法。

如果,我们想要过滤关系型数据,我们可以:

GET /persons?filter=[
  {
    "name": "computers",
    "op": "any",
    "val": {
      "name": "serial",
      "op": "ilike",
      "val": "%Amstrad%"
    }
  }
] HTTP/1.1
Accept: application/vnd.api+json

注:

当我们过滤关系时,使用“any”操作符来过滤“对多”关系;使用“has”操作符来过滤“对一”关系。

另外,我们也能使用一种快捷的方式实现同样的功能:

GET /persons?filter=[{"name":"computers__serial","op":"ilike","val":"%Amstrad%"}] HTTP/1.1
Accept: application/vnd.api+json

我们也能用“与或非”来联结操作:

GET /persons?filter=[
  {
    "name":"computers__serial",
    "op":"ilike",
    "val":"%Amstrad%"
  },
  {
    "or": {
      [
        {
          "not": {
            "name": "name",
            "op": "eq",
            "val":"John"
          }
        },
        {
          "and": [
            {
              "name": "name",
              "op": "like",
              "val": "%Jim%"
            },
            {
              "name": "birth_date",
              "op": "gt",
              "val": "1990-01-01"
            }
          ]
        }
      ]
    }
  }
] HTTP/1.1
Accept: application/vnd.api+json

常用操作符:

  • any: 过滤对多关系
  • between: 过滤一个字段位于两个值之间
  • endswith: 检查是否以某个字符串结尾
  • eq: 等于
  • ge: 大于或等于
  • gt: 大于
  • has: 过滤对一关系
  • ilike: 检查是否包含某个字符串(大小写不明感)
  • in_: 检查是否包含在list中
  • is_: 检查字段是否是某个值
  • isnot: 检查字段是否不是某个值
  • like: 检查是否包含某个字符串
  • le: 小于或等于
  • lt: 小于
  • match: 匹配
  • ne: 不等于
  • notilike: 检查是否不包含某个字符串(大小写不明感)
  • notin_: 检查是否不包含在list中
  • notlike: 检查是否不包含某个字符串
  • startswith: 检查是否以某个字符串开始

注:

使用常用操作符时,要看model的字段类型是否支持该操作。

简单过滤

简单过滤仅支持“eq”操作符。每个简单过滤语句,都会转换成其默认的过滤语句,并添加到过滤列表的末尾。

例:

GET /persons?filter[name]=John HTTP/1.1
Accept: application/vnd.api+json

等同于:

GET /persons?filter[name]=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json

同样,我们也能在请求中,使用多个简单过滤:

GET /persons?filter[name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json

等同于:

GET /persons?filter[name]=[{"name":"name","op":"eq","val":"John"}, {"name":"gender","op":"eq","val":"male"}] HTTP/1.1

08-包含关联对象

使用include查询参数,我们能在Response中返回关联对象的详情。同样,我们能在任何路由中(传统CRUD路由,或关系路由),以及任何返回数据的HTTP方法中使用include参数。

返回结果会多一个“included”的key。

例:

Request:

GET /persons/1?include=computers HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JEAN <jean@gmail.com>",
      "birth_date": "1990-10-10"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "1"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
    "links": {
      "self": "/persons/1"
    }
  },
  "included": [
    {
      "type": "computer",
      "id": "1",
      "attributes": {
        "serial": "Amstrad"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/1"
      }
    }
  ],
  "links": {
    "self": "/persons/1"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

我们也能在relationship中使用include。

例:

Request:

GET /persons/1?include=computers.owner HTTP/1.1
Accept: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "display_name": "JEAN <jean@gmail.com>",
      "birth_date": "1990-10-10"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "1"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
    "links": {
      "self": "/persons/1"
    }
  },
  "included": [
    {
      "type": "computer",
      "id": "1",
      "attributes": {
        "serial": "Amstrad"
      },
      "relationships": {
        "owner": {
          "data": {
            "type": "person",
            "id": "1"
          },
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      },
      "links": {
        "self": "/computers/1"
      }
    },
    {
      "type": "person",
      "id": "1",
      "attributes": {
        "display_name": "JEAN <jean@gmail.com>",
        "birth_date": "1990-10-10"
      },
      "relationships": {
        "computers": {
          "links": {
            "related": "/persons/1/computers",
            "self": "/persons/1/relationships/computers"
          }
        }
      },
      "links": {
        "self": "/persons/1"
      }
    }
  ],
  "links": {
    "self": "/persons/1"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

尽管例子比较荒唐,但我们只是为了解释用法。

09-稀疏字段

使用查询参数“fields”,我们能限制api返回的字段。这种做法有助于提升性能,因为客户端可以按需所取。同样,我们能在任何路由中(传统CRUD路由,或关系路由),以及任何返回数据的HTTP方法中使用fields参数。

注:

为了可读性,例子的url未进行编码。

稀疏字段的语法如下:

?fields[<resource_type>]=<list of fields to return>

例:

GET /persons?fields[person]=display_name HTTP/1.1
Accept: application/vnd.api+json

上述例子中,结果只会返回person的display_name字段,并不会返回关系数据。这样就可以达到提高性能的目的,因为查询外键是很费时的。

我们能管理整个响应返回的字段,甚至是include中的数据。

例:

如果我们不想得到某个人电脑的关系字段,我们可以做如下操作:

GET /persons/1?include=computers&fields[computer]=serial HTTP/1.1
Accept: application/vnd.api+json

当然我们也可以组合使用多个稀疏字段:

GET /persons/1?include=computers&fields[computer]=serial&fields[person]=name,computers HTTP/1.1
Accept: application/vnd.api+json

注:

如果我们同时使用了fields和include,别忘了在fields中注明关系的名称,否则include可能会失败。(可以观察上例中的computers,在两处都出现了)


10-分页

我们时候ResourceList默认实现的GET方法时,返回结果默认会分页。默认页面大小是30,同时,我们也能通过查询参数“page”,从客户端的角度来控制页面大小。

注:

为了可读性,以下例子未进行url编码。

页面大小

如下操作,控制页面大小:

GET /persons?page[size]=10 HTTP/1.1
Accept: application/vnd.api+json

页码

如下操作,控制页面数量:

GET /persons?page[number]=2 HTTP/1.1
Accept: application/vnd.api+json

页面大小+页码

当然,我们也能同时控制两者:

GET /persons?page[size]=10&page[number]=2 HTTP/1.1
Accept: application/vnd.api+json

禁用分页

我们也能禁用分页,通过设置size=0

GET /persons?page[size]=0 HTTP/1.1
Accept: application/vnd.api+json

11-排序

我们也能通过查询参数“sort”,从客户端的角度来控制页面大小。

注:

为了可读性,以下例子未进行url编码。

例:

GET /persons?sort=name HTTP/1.1
Accept: application/vnd.api+json

多项排序

我们能为多个字段进行排序:

GET /persons?sort=name,birth_date HTTP/1.1
Accept: application/vnd.api+json

降序

我们能使用字符“-”来实现降序:

GET /persons?sort=-name HTTP/1.1
Accept: application/vnd.api+json

多项排序+降序

当然,我们也能同时使用两者:

GET /persons?sort=-name,birth_date HTTP/1.1
Accept: application/vnd.api+json

12-错误

JSONAPI 1.0规范推荐的返回错误形式如下:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "status": "422",
      "source": {
        "pointer":"/data/attributes/first-name"
      },
      "title":  "Invalid Attribute",
      "detail": "First name must contain at least three characters."
    }
  ],
  "jsonapi": {
    "version": "1.0"
  }
}

其中,“source”字段给出的是,输入数据或查询字段的错误信息。

上例,演示了出现输入数据的错误信息。下例,演示的是查询字段“include”的错误信息:

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json

{
  "errors": [
    {
      "status": "400",
      "source": {
        "parameter": "include"
      },
      "title":  "BadRequest",
      "detail": "Include parameter is invalid"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  }
}

Flask-REST-JSONAPI 提供了两类错误处理的模块:

  • the errors module:我们能从jsonapi_errors模块导入errors module。通过这个错误处理的包,我们能创建一系列,符合JSONAPI 1.0规范,结构化的错误返回结果。
  • the exception module:我们能从这个模块中导入许多异常,同样,我们也能创建一系列,符合JSONAPI 1.0规范,结构化的异常返回结果。

当我们定制api时,强烈建议使用Flask-REST-JSONAPI的exception模块。因为JsonApiException会被框架捕获,并输出符合JSONAPI 1.0规范的内容。

例:

# all required imports are not displayed in this example
from flask_rest_jsonapi.exceptions import ObjectNotFound

class ComputerList(ResourceList):
    def query(self, view_kwargs):
        query_ = self.session.query(Computer)
        if view_kwargs.get('id') is not None:
            try:
                self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            except NoResultFound:
                raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
            else:
                query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
        return query_

13-Api

我们在实例化Api时,以元组形式,提供全局装饰器。

例:

from flask_rest_jsonapi import Api
from your_project.security import login_required

api = Api(decorators=(login_required,))

14-权限

Flask-REST-JSONAPI 提供了强大的权限系统支持。

例:

from flask import Flask
from flask_rest_jsonapi import Api
from your_project.permission import permission_manager

app = Flask(__name__)

api = Api()
api.init_app(app)
api.permission_manager(permission_manager)

上例中,API会在每个方法方法前,检查权限。检查权限的方式就是依次调用permission_manager中的函数。

定义permission_manager必须符合下述规范:

def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
    """The function use to check permissions

    :param callable view: the view
    :param list view_args: view args
    :param dict view_kwargs: view kwargs
    :param list args: decorator args
    :param dict kwargs: decorator kwargs
    """

注:

Flask-REST-JSONAPI利用装饰器has_permission,来为每个方法检查权限。我们能给装饰器提供args和kwargs,这样,我们就能在permission_manager中获取args和kwargs了。默认情况下,权限系统不会给装饰器,提供任何的args或kwargs。

当发生权限不足的情况,建议使用以下方式抛出异常。

raise JsonApiException(<error_source>,
                       <error_details>,
                       title='Permission denied',
                       status='403')

我们能为某个资源禁用权限系统,或者,定制权限检查的方式,如下:

from flask_rest_jsonapi import ResourceList
from your_project.extensions import api

class PersonList(ResourceList):
    disable_permission = True

    @api.has_permission('custom_arg', custom_kwargs='custom_kwargs')
    def get(*args, **kwargs):
        return 'Hello world !'

警告:

如果我们要同时使用权限系统和oauth支持,比如在权限系统下,从oauth读取用户信息(requests.oauth.user)。我们必须先初始化oauth,再初始化权限系统,因为装饰器检查是串联执行的。

例:

from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
from your_project.permission import permission_manager

app = Flask(__name__)
oauth2 = OAuth2Provider()

api = Api()
api.init_app(app)
api.permission_manager(permission_manager) # initialize permission system first
api.oauth_manager(oauth2) # initialize oauth support second

15-OAuth

Flask-REST-JSONAPI通过Flask-OAuthlib包,支持OAuth功能。

例:

from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider

app = Flask(__name__)
oauth2 = OAuth2Provider()

api = Api()
api.init_app(app)
api.oauth_manager(oauth2)

上述例子中,Flask-REST-JSONAPI的资源只要添加了装饰器,就会进行保护

oauth2.require_oauth(<scope>)

指定范围模式如下:

<action>_<resource_type>

动作包括:

  • list:ResourceList的get方法
  • create:ResourceList的post方法
  • get:ResourceDetail的get方法
  • update:ResourceDetail的get方法
  • delete:ResrouceDetail的delete方法

例:

list_person

如果我们想定制权限范围,我们可以提供一个计算定制范围的函数。函数格式如下:

def get_scope(resource, method):
        """Compute the name of the scope for oauth

        :param Resource resource: the resource manager
        :param str method: an http method
        :return str: the name of the scope
        """
        return 'custom_scope'

用例:

from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider

app = Flask(__name__)
oauth2 = OAuth2Provider()

api = Api()
api.init_app(app)
api.oauth_manager(oauth2)
api.scope_setter(get_scope)

注:

我们能给定制的范围方法,起任意的名字。但是,我们必须设定两个必须参数:resource和method。就像上述例子一样。

如果我们需要禁用OAuth,或者为某个资源定制保护方法,我们可以给资源管理器添加如下选项:

例:

from flask_rest_jsonapi import ResourceList
from your_project.extensions import oauth2

class PersonList(ResourceList):
    disable_oauth = True

    @oauth2.require_oauth('custom_scope')
    def get(*args, **kwargs):
        return 'Hello world !'

16-配置

我们有5个可以配置的参数:

  • PAGE_SIZE:一次列表请求,默认返回的数量(默认为30)
  • MAX_PAGE_SIZE:一次列表请求,最多返回的数量。如果请求的数量大于这个数字,就会返回400。
  • MAX_INCLUDE_DEPTH:调用include关系参数时,允许的最大深度。
  • ALLOW_DISABLE_PAGINATION:如果我们想要禁止关闭分页,那么把该参数设为False。
  • CATCH_EXCEPTIONS:如果我们想让flask_rest_jsonapi捕捉所有异常并返回JsonApiException标准结果设置为True。(默认为True)

参考

github: https://github.com/miLibris/flask-rest-jsonapi/

英文文档: https://flask-rest-jsonapi.readthedocs.io

中文翻译: https://flask-rest-jsonapi-cn.readthedocs.io

沪ICP备20004885号-2
jonathan.nuance@outlook.com
Built with Hugo
Theme Stack designed by Jimmy