Flask 项目笔记

2018 年 10 月 18 日 • 阅读数: 109

Flask REST API 项目笔记

项目初始化

新建一个文件夹,这里命名为 ginger

切换到文件夹中,打开命令行,创建虚拟环境

pipenv install

项目的依赖

依赖如下

flask = "==1.0"
flask-sqlalchemy = "==2.3.2"
flask-wtf = "==0.14.2"
cymysql = "==0.9.1"
flask-cors = "==2.1.0"
flask-httpauth = "==2.7.0"
requests = "==2.18.4"

新建项目的入口文件 ginger.py

创建目录结构

.
├── app
|   ├── api
|   |	 └── v1
|	├── config
|	|	├── secure.py
|   |	└── setting.py
|	├── libs
|	├── models
|	├── validators
|   └── app.py
├── ginger.py
├── pipfile
└── pipfile.lock

**注:**在 api 下建一个 v1 目录是为了方便以后的版本升级,所以我们的API视图都是在 v1 目录下进行编写

路由的分发

我们使用蓝图和自定义红图的方式来实现路由的分发

项目的路由架构

通过自定义红图来实现视图路由的注册,然后将红图注册到蓝图中,最后通过将蓝图注册到Flask的核心对象中

红图对应每一个组件,蓝图对应一个版本

自定义红图的实现

在红图中,我们主要需要实现3个方法,分别是

  • 构造方法:在创建红图对象的时候,需要传入一个红图的 name
  • route():用于视图函数到路由的注册
  • register:用户红图到蓝图的注册
class Redprint(object):
    def __init__(self, name):
        self.name = name
        self.mound = []

    def route(self, rule, **options):
        def decorator(f):
            self.mound.append((f, rule, options))
            return f
        return decorator

    def register(self, bp, url_prefix=None):
        if url_prefix is None:
            url_prefix = '/' + self.name
        for f, rule, options in self.mound:
            endpoint = options.pop("endpoint", f.__name__)
            bp.add_url_rule(url_prefix+rule, endpoint, f, **options)

路由分发的实现

在视图中我们定义一个简单的测试视图

from app.libs.redprint import Redprint

# 实例化一个红图对象
api = Redprint('user')

# 将视图函数注册到红图中
@api.route('', methods=['GET'])
def get_user():
    return 'I am Amor'

创建一个蓝图,将我们自定义的红图注册到蓝图中(蓝图在版v1的 init.py 中创建)

from flask import Blueprint

from app.api.v1 import user


def create_blueprint_v1():
    bp_v1 = Blueprint('v1', __name__)

    user.api.register(bp_v1)

    return bp_v1

将蓝图注册到Flask的核心对象中(核心对象在 app.py 中创建)

def register_blueprint(app):
    app.register_blueprint(create_blueprint_v1(), url_prefix='/v1')
    
def create_app():
    app = Flask(__name__)
    
	...
    register_blueprint(app)

    return app

客户端注册

在API的设计中,前端可以是多样的,所以对应的可以通过多种方式来注册

定义枚举类型

前端通过不同的标志位来确定类型

from enum import Enum


class ClientTypeEnum(Enum):
    USER_EMAIL = 100
    USER_MOBILE = 101

    USER_MINA = 200
    USER_WX = 2001

视图的实现

通过字典和枚举类型实现多客户端的注册

通过 request.json 获取到请求中 json 类型的数据

将数据传递到自定义的 Form 进行验证,从验证后的数据中获取到注册的类型,和枚举的类型保持一致

通过定义的字典调用对应的注册函数(这里只实现了一种注册类型 __register_user_by_email

在注册函数中再次调用验证类,这个验证类是基于上面一个类的进一步验证,通过验证后,调用Model的注册方法

@api.route('/register', methods=['POST'])
def create_client():
    data = request.json
    form = ClientForm(data=data)
    if form.validate():
        promise = {
            ClientTypeEnum.USER_EMAIL: __register_user_by_email
        }
        promise[form.type.data]()
    else:
        raise ClientTypeError
    return 'success'


def __register_user_by_email():
    form = UserEmailForm(data=request.json)
    if form.validate():
        User.register_by_email(form.nickname.data, form.account.data, form.secret.data)

定义Form验证

前面我们已经提到了有两次验证,如下

  • ClientForm :是一个注册的总的验证
  • UserEmailForm :是一个特定注册方式的进一步验证
class ClientForm(Form):
    account = StringField(validators=[DataRequired(), length(min=5, max=32)])
    secret = StringField()
    type = IntegerField(validators=[DataRequired()])

    def validate_type(self, value):
        try:
            client = ClientTypeEnum(value.data)
        except ValueError as e:
            raise e
        self.type.data = client


class UserEmailForm(ClientForm):
    account = StringField(validators=[Email(message='邮箱')])
    secret = StringField(validators=[DataRequired(),
                                     Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
    nickname = StringField(validators=[DataRequired(), length(min=2, max=22)])

    def validate_account(self, value):
        if User.query.filter_by(email=value.data).first():
            raise ValidationError()

定义Model类

这里继承的 Base 是一个自定义的类,不过是和 sqlalchemy 相关的,这里不过多介绍

需要注意的是它有一个静态方法 register_by_email(),在这里面我们完成了通过邮箱注册

class User(Base):
    id = Column(Integer, primary_key=True)
    email = Column(String(24), unique=True, nullable=False)
    nickname = Column(String(24), unique=True)
    auth = Column(SmallInteger, default=1)
    _password = Column('password', String(100))

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, raw):
        self._password = generate_password_hash(raw)

    @staticmethod
    def register_by_email(nickname, account, secret):
        with db.auto_commit():
            user = User()
            user.nickname = nickname
            user.email = account
            user.password = secret
            db.session.add(user)

**注:**在一个类中本来是不可以实例化自己的,但如果是静态方法或者类方法中可以

自定义异常处理

当我们验证失败后需要响应一个错误

自定义异常响应我们需要继承 HTTPException ,这里我们定义了 code=400 表示请求参数错误

from werkzeug.exceptions import HTTPException


class ClientTypeError(HTTPException):
    code = 400
    description = (
        'client is invalid'
    )

JSON异常返回

默认的 HTTPException 返回的是一个 HTML 类型的页面,而在API中,我们希望他可以返回一个JSON类型的信息,所以这里我们继承了 HTTPException 并重写了它的 get_body()get_headers() 方法

get_body() :用来构建返回的数据的

get_headers() :用来构建返回的请求体的

from flask import request, json
from werkzeug.exceptions import HTTPException


class APIException(HTTPException):
    code = 500
    msg = 'sorry, we make a mistake (;´༎ຶД༎ຶ`)'
    error_code = 999

    def __init__(self, msg=None, code=None, error_code=None, headers=None):
        if code:
            self.code = code
        if error_code:
            self.error_code = error_code
        if msg:
            self.msg = msg

        super().__init__(msg, None)

    def get_body(self, environ=None):
        body = dict(msg=self.msg, error_code=self.error_code,
                    request=request.method + ' ' + self.get_url_no_param())
        text = json.dumps(body)
        return text

    def get_headers(self, environ=None):
        return [('Content-Type', 'application/json')]

    @staticmethod
    def get_url_no_param():
        full_path = str(request.full_path)
        main_path = full_path.split('?')
        return main_path[0]

重写异常处理

我们有了自定义的JSON类型的异常处理类,这里就可以直接继承它并且重新定义返回参数

from .error import APIException


class ClientTypeError(APIException):
    code = 400
    msg = 'client is invalid'
    error_code = 1006

更改默认的异常处理

WTForm在默认的情况下,验证不通过的时候也不会抛出异常,而是将异常保存在 errors 属性中

但现在我们希望它可以在验证不通过的情况下给我们响应异常信息,所以我们需要自定义一个 Form 验证器

这里我们并没有重写它默认的 validate() 方法,而是重新定义了一个 validate_for_api 方法

在这里面我们调用了默认的验证方法,如果没有通过验证,我们将抛出一个异常,并且将 errors 信息传递出去

from wtforms import Form
from app.libs.error_code import ParameterException

__author__ = '骆杨'


class BaseForm(Form):
    def __init__(self):
        data = request.json
        super().__init__(data=data)

    def validate_for_api(self):
        valid = super().validate()
        if not valid:
            raise ParameterException(msg=self.errors)
        return self

**注:**在构造方法中我们直接获取到 request.json 数据,并调用父类的构造方法,传入数据


在异常处理类,我们依然继承前面定义的 APIException 用于响应Json数据

class ParameterException(APIException):
    code = 400
    msg = 'invalid parameter'
    error_code = 1000

在视图类中,我们就可以直接调用 validate_for_api 方法,而不需要做判断来自己抛出异常了

并且这里也不需要出入 data 属性了,前面我们在验证类中已经自动获取到了

@api.route('/register', methods=['POST'])
def create_client():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: __register_user_by_email
    }
    promise[form.type.data]()
    return 'success'

定义成功的响应

在创建成功后,我们也应该响应一个Json信息,这里我们可以直接使用一个简单的方式来完成正确的响应,虽然在成功后返回一个异常对象,不太符合情理,但这样可以使得我们的响应保持一个高度的一致性

class Success(APIException):
    code = 201
    msg = 'success'
    error_code = 0

在视图的最后直接 return Success()

全局的异常处理

在Flask的核心对象中有一个方法可以捕获到所有被抛出的异常,我们可以对这些异常进行处理,将它们都转换成我们自己定义的 APIException ,这一步的目的也是为了响应的一致性

@app.errorhandler(Exception)
def framework_error(e):

    if isinstance(e, APIException):
        return e
    if isinstance(e, HTTPException):
        code = e.code
        msg = e.description
        error_code = 1007
        return APIException(msg, code, error_code)
    else:
        if not app.config['DEBUG']:
            return APIException()
        else:
            raise e

总结

最后我们花了这么多精力来定义 APIExceptionBaseForm 、Model的 Base 等等,这一切显得非常的复杂,但在我们的视图函数中,在我们调用这些方法的时候就非常的好用了

因此,编码的一个基本原则就是,可以接受定义时的复杂,但不能接受调用时候的复杂

登录与验证

生成Token

我们重新定义一个视图函数,用来生成用户Token

generate_auth_token() 函数中,我们接受4个参数

  • uid:用户ID
  • ac_type:用户类型
  • scope
  • expiration:Token有效时间

生成Token我们需要用到 TimedJSONWebSignatureSerializer 这个类,在后面验证Token的时候同样也需要

from flask import current_app, jsonify
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

from app.libs.enums import ClientTypeEnum
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm


api = Redprint('token')


@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify
    }
    identify = promise[form.type.data](form.account.data, form.secret.data)
    expiration = current_app.config['TOKEN_EXPIRATION']
    token = generate_auth_token(identify['uid'], form.type.data, None, expiration)
    t = {'token': token.decode('ascii')}
    return jsonify(t), 201


def generate_auth_token(uid, ac_type, scope=None, expiration=7200):
    s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    return s.dumps({'uid': uid, 'type': ac_type.value})

登录验证

在我们接受到用户传递过来的数据后,需要进行用户验证,这一步只是简单的账户密码验证,我们可以将验证逻辑直接写在Model类的定义中,这里需要注意,密码时加密后的,所以需要用到 check_password_hash() 方法

    @staticmethod
    def verify(email, password):
        user = User.query.filter_by(email=email).first()
        if not user:
            raise NotFound(msg='user not found')
        if not user.check_password(password):
            raise AuthFailed
        return {'uid': user.id}

    def check_password(self, raw):
        if not self._password:
            return False
        return check_password_hash(self._password, raw)

异常处理

当用户不存在,或密码验证失败,我们应该响应对应的 APIException

class NotFound(APIException):
    code = 404
    msg = 'the resource are not found o(TヘTo)'
    error_code = 1001


class AuthFailed(APIException):
    code = 401
    msg = 'authorization failed'
    error_code = 1005

用户验证

定义验证函数,当客户端已经获取到了Token,并且携带Token发起请求的时候,当数据在到达视图函数之前,我们需要捕获到这个信息,并做判断,是否让请求通过

这里使用了Flask提供的一个 HTTPBasicAuth 类,通过这个对象,我们可以通过 @auth.verify_password 使一个函数变成用户验证函数,它会在加上了 @auth.login_required 装饰器的视图函数被调用前调用

from flask_httpauth import HTTPBasicAuth


auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(account, password):
    return True

在需要验证的视图上方加上装饰器

from app.libs.token_auth import auth


@api.route('', methods=['GET'])
@auth.login_required
def get_user():
    return 'I am Amor'

Token验证

默认情况下我们需要接收 accountpassword 两个参数,但是在使用Token的情况下我们就不需要验证密码,因为在Token中已经携带了用户信息,我们可以直接获取到用户对应的ID,只需要验证Token的合法性即可

from collections import namedtuple

from flask import current_app, g
from flask_httpauth import HTTPBasicAuth
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired

from app.libs.error_code import AuthFailed


auth = HTTPBasicAuth()
User = namedtuple('User', ['uid', 'ac_type', 'scope'])


@auth.verify_password
def verify_password(token, password):
    user_info = verify_auth_token(token)
    if not user_info:
        return False
    else:
        g.user = user_info
        return True


def verify_auth_token(token):
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(token)
    except BadSignature:
        raise AuthFailed(msg='token is invalid', error_code=1002)
    except SignatureExpired:
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['type']
    return User(uid, ac_type, None)

**注:**在验证通过后,我们将获取到的用户信息放到了 g.user 中,这是一个Flask的全局属性

模型序列化

在Flask中,我们可以使用 jsonify() 函数来序列化一个字典类型的数据,但对于一个对象,它往往不能直接序列化,默认情况下 jsonify() 不能序列化的对象,它会调用 defailt() 函数来对他进行处理,所以,我们需要重写 defailt() 函数来实现,我们自定义的序列化方法

自定义JSONEncoder

我们不可以直接修改源码,所以只能通过重写Flask的方式来实现重写 JSONEncoderdefault() 函数

这里我们直接调用了 dict() 函数来将对象转换成字典

from datetime import date

from flask import Flask as _Flask
from flask.json import JSONEncoder as _JSONEncoder

from app.libs.error_code import ServerError


class JSONEncoder(_JSONEncoder):
    def default(self, o):
        if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
            return dict(o)
        if isinstance(o, date):
            return o.strftime('%Y-%m-%d')
        raise ServerError()


class Flask(_Flask):
    json_encoder = JSONEncoder

重写模型对象

要将一个对象转换成一个字典,我们需要实现两个方法,keys()__getitem__()

这里这两个方法,我们直接在Model的基类中实现(keys()dict() 所需要的)

    def __getitem__(self, item):
        return getattr(self, item)
    
    def keys(self):
        return self.fields

它需要我们在创建Model类的时候定义一个 fields 属性

fields = ('id', 'email', 'nickname', 'auth')

在使用 dict() 的时候他会根据这个属性来转化指定的字段

通过jsonify调用

在视图函数中我们可以直接通过 jsonify() 调用

from flask import jsonify

@api.route('/<int:uid>', methods=['GET'])
@auth.login_required
def get_user(uid):
    user = User.query.get_or_404(uid)
    return jsonify(user)

权限的管理

我们需要在数据库中添加一个字段来表示权限的类型,这一步在前面创建User表的时候已经添加了 auth 字段就是用来做权限分类的(目前支持2种类型,一个是管理员一个是普通用户)

在前面我们定义了一个获取用户Token的接口,但在这个接口中,我们并没有返回任何权限相关的数据,如果我们,所以这里我们可以先修改这个Token的接口,让它保存一个用户的权限信息

重写Token接口

用户验证的时候,返回用户的权限类型 scope

    @staticmethod
    def verify(email, password):
        user = User.query.filter_by(email=email).first()
        if not user:
            raise NotFound(msg='user not found')
        if not user.check_password(password):
            raise AuthFailed
        scope = 'AdminScope' if user.auth == 2 else 'UserScope'
        return {'uid': user.id, 'scope': scope}

通过用户验证后,将 scope 保存到Token中

@api.route('', methods=['POST'])
def get_token():
    form = ClientForm().validate_for_api()
    promise = {
        ClientTypeEnum.USER_EMAIL: User.verify
    }
    identify = promise[form.type.data](form.account.data, form.secret.data)
    expiration = current_app.config['TOKEN_EXPIRATION']
    token = generate_auth_token(identify['uid'], form.type.data,
                                identify['scope'], expiration)
    t = {'token': token.decode('ascii')}
    return jsonify(t), 201


def generate_auth_token(uid, ac_type, scope=None, expiration=7200):
    s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    return s.dumps({'uid': uid, 'type': ac_type.value, 'scope': scope})

重写权限验证类

当用户携带Token访问权限保护的接口的时候,我们需要对用户权限类型进行验证,这里从Token中获取用户的权限类型 scope = data['scope'] ,然后调用验证方法 is_in_scope() 成功之后返回一个 User 对象

User = namedtuple('User', ['uid', 'ac_type', 'scope'])


def verify_auth_token(token):
    s = Serializer(current_app.config['SECRET_KEY'])
    try:
        data = s.loads(token)
    except BadSignature:
        raise AuthFailed(msg='token is invalid', error_code=1002)
    except SignatureExpired:
        raise AuthFailed(msg='token is expired', error_code=1003)
    uid = data['uid']
    ac_type = data['type']
    scope = data['scope']
    allow = is_in_scope(scope, request.endpoint)
    if not allow:
        raise Forbidden()
    return User(uid, ac_type, scope)

权限管理的实现

在前面我们获得了 scope 的类型,和访问的接口,在下面只需要对不同的权限调用不同的类即可

我们定义了一个 Scope 基类,两个子类 AdminScopeUserScope 分别对应两种权限,实现如下

class Scope(object):

    allow_api = []
    allow_module = []

    forbidden = []

    def __add__(self, scope):
        self.allow_api = self.allow_api + scope.allow_api
        self.allow_api = list(set(self.allow_api))
        self.allow_module = self.allow_module + scope.allow_module
        self.allow_module = list(set(self.allow_module))
        return self


class AdminScope(Scope):
    allow_api = ['get_user_list']
    allow_module = ['v1.user']

    def __init__(self):
        self + UserScope()


class UserScope(Scope):
    allow_api = ['get_user', 'delete_user']
    allow_module = []


def is_in_scope(scope, endpoint):
    scope = globals()[scope]()
    splits = endpoint.split('+')
    red_name = splits[0]
    api_name = splits[1]
    if api_name in scope.forbidden:
        return False
    if red_name in scope.allow_module:
        return True
    if api_name in scope.allow_api:
        return True
    else:
        return False

红图的修改

需要注意的是,我们需要获取到访问接口的详细信息,所以,这里在注册接口的时候,添加了一个红图的 name

class Redprint(object):
	...

    def register(self, bp, url_prefix=None):
        if url_prefix is None:
            url_prefix = '/' + self.name
        for f, rule, options in self.mound:
            endpoint = self.name + '+' + options.pop("endpoint", f.__name__)
            bp.add_url_rule(url_prefix+rule, endpoint, f, **options)

业务的完善

图书接口的完善

这里面我们简单的编写了2个接口,一个用于图书的搜索,一个用于查看图书的详情

我们本地并没有完善的图书数据,所以,这里如果没有对应的数据,我们会调用爬虫去访问外部的API,然后保存到本地数据库,并返回数据给前端页面

下面的代码需要注意的是:

  • or_ :或查询
  • like :模糊查询(查询字符串需要前后加上 %
  • hide :隐藏不需要序列化的字段,通过它我们可以动态的指定返回的数据包含的字段
from flask import jsonify
from sqlalchemy import or_

from app.libs.redprint import Redprint
from app.models.book import Book
from app.validators.forms import BookSearchForm
from app.spider.yushu_book import YuShuBook


api = Redprint('book')


@api.route('/search')
def search():
    form = BookSearchForm().validate_for_api()
    q = '%' + form.q.data + '%'
    books = Book.query.filter(or_(Book.title.like(q), Book.publisher.like(q))).all()
    if not books:
        q = form.q.data
        book = YuShuBook()
        book.search_by_keyword(q)
        books = book.books
    books = [book.hide('summary') for book in books]
    return jsonify(books)


@api.route('/<isbn>/detail')
def detail(isbn):
    book = Book.query.filter_by(isbn=isbn).first_or_404()
    return jsonify(book)

爬虫获取API数据

逻辑如下,在该类中我们主要是构造了URL,获取数据的逻辑在 Http 类中

from flask import current_app

from app.libs.httper import Http


class YuShuBook(object):

    # isbn_url = 'https://api.douban.com/v2/book/isbn/{}'
    # keyword_url = 'https://api.douban.com/v2/book/search?q={}&count={}&start={}'

    isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
    keyword_url = 'http://t.yushu.im/v2/book/search?q={}&count={}&start={}'

    http = Http()

    def __init__(self):
        self.total = 0
        self.books = []

    def _fill_single(self, data):
        if data:
            self.total = 1
            self.books.append(data)

    def _fill_collection(self, data):
        if data:
            self.total = data['total']
            self.books = data['books']

    @staticmethod
    def calculate_start(page):
        return (page-1) * current_app.config['PER_PAGE']

    def search_by_isbn(self, isbn):
        url = self.isbn_url.format(isbn)
        result = self.http.get(url)
        self._fill_single(result)

    def search_by_keyword(self, keyword, page=1):
        url = self.keyword_url.format(keyword, current_app.config['PER_PAGE'],
                                      YuShuBook.calculate_start(page))
        result = self.http.get(url)
        self.http.save()

        self._fill_collection(result)

    @property
    def first(self):
        return self.books[0] if self.total >= 1 else None

在该类中,我们通过 requests 库进行数据的请求,并且通过 save() 方法,保存数据到数据库

import requests

from app.models.base import db
from app.models.book import Book


class Http(object):
    """
    获取数据
    """
    def __init__(self):
        self.books = []

    def get(self, url, return_json=True):
        r = requests.get(url)
        if r.status_code != 200:
            return {} if return_json else ''
        self.books = r.json()['books']
        return r.json() if return_json else r.text

    def save(self):
        for book in self.books:
            with db.auto_commit():
                model = Book()
                model.title = book['title']
                model.author = ','.join(book['author'])
                model.binding = book['binding']
                model.publisher = book['publisher']
                model.price = book['price']
                model.pages = book['pages']
                model.pubdate = book['pubdate']
                model.isbn = book['isbn']
                model.summary = book['summary']
                model.image = book['image']

                db.session.add(model)

图书模型的实现

这里需要介绍一个 @orm.reconstructor 为什么要打这个装饰器呢,因为默认情况下 sqlalchemy 不会调用我们自己编写的初始化方法,它内部使用了元类编程的模式,所以,需要加上这个装饰器,它才会调用构造方法

这里我们将 fields 设为实例方法的原因,是为了上面提到的 hide ,它的逻辑在 Base 中实现的

from sqlalchemy import Column, Integer, String, Text, orm

from app.models.base import Base

__author__ = '骆杨'


class Book(Base):
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(50), nullable=False)
    author = Column(String(300), default='未名')
    binding = Column(String(20))
    publisher = Column(String(50))
    price = Column(String(20))
    pages = Column(String(10))
    pubdate = Column(String(20))
    isbn = Column(String(15), nullable=False, unique=True)
    summary = Column(Text())
    image = Column(String(50))

    @orm.reconstructor
    def __init__(self):

        self.fields = ['id', 'title', 'author', 'binding', 'publisher', 'price',
                       'pages', 'pubdate', 'isbn', 'summary', 'image']

Base 的部分源码如下

前面我们说了 keys 这里方法是 dict 所需要的,序列化的时候会根据 keys 的返回进行相应的字段的序列化

而如果我们不将 fields 设为实例属性的话,它就会被任意实例所改变,无法达到我们的目的

    def keys(self):
        return self.fields

    def hide(self, *keys):
        for key in keys:
            self.fields.remove(key)
        return self

    def append(self, *keys):
        for key in keys:
            self.fields.append(key)
        return self

礼物接口的完善

from app.libs.error_code import DuplicateGift, Success
from app.libs.redprint import Redprint
from app.libs.token_auth import auth
from app.models.base import db
from app.models.book import Book
from app.models.gift import Gift

__author__ = '骆杨'


api = Redprint('gift')


@api.route('/<isbn>', methods=['POST'])
@auth.login_required
def create(isbn):
    uid = g.user.uid
    with db.auto_commit():
        Book.query.filter_by(isbn=isbn).first_or_404()
        gift = Gift.query.filter_by(isbn=isbn, uid=uid).firse()
        if gift:
            raise DuplicateGift()
        gift = Gift()
        gift.isbn = isbn
        gift.uid = uid
        db.session.add(gift)
    return Success()

礼物模型的实现

from sqlalchemy import Column,String, Boolean, Integer, ForeignKey
from sqlalchemy.orm import relationship

from app.models.base import Base

__author__ = '骆杨'


class Gift(Base):
    id = Column(Integer, primary_key=True)
    user = relationship('User')
    uid = Column(Integer, ForeignKey('user.id'))
    isbn = Column(String(15), nullable=False)
    launched = Column(Boolean, default=False)
标签: FlaskREST

召唤伊斯特瓦尔