第十四章 应用编程接口(四)

一. 资源的序列化和反序列化准备

       把内部格式转换为JSON格式的过程称为序列化,我们将资源序列化的函数定义在各模型类中,提供给客户端的资源表示没有必要与数据库模型字段完全一致。返回的数据中除了自身描述,我们还添加了返回其它资源的URL:

class User(UserMixin, db.Model):
    def to_json(self):
        json_user = {
            'self_url': url_for('api.get_user', id=self.id),
            'username': self.username,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts_url': url_for('api.get_user_posts', id=self.id),
            'followed_posts_url': url_for('api.get_followed_posts', id=self.id),
            'post_count': self.posts.count()
        }
        return json_user

class Post(db.Model):
    def to_json(self):
        json_post = {
            'self-url': url_for('api.get_post', id=self.id),
            'body': self.body,
            'body_html': self.body_html,
            'author_url': url_for('api.get_user', id=self.author_id),
            'comments_url': url_for('api.get_post_comments', id=self.id),
            'comment_count': self.comments.count()
        }
        return json_post

class Comment(db.Model):
    def to_json(self):
        comment_json = {
            'self_url': url_for('api.get_comment', id=self.id),
            'post_url': url_for('api.get_post', id=self.post_id),
            'body': self.body,
            'body_html': self.body_html,
            'timestamp': self.timestamp,
            'author_url': url_for('api.get_user', id=self.author_id)
        }
        return comment_json
    

       序列化的逆向操作称为反序列化。把JSON反序列化成模型时,客户端提供的数据可能无效,错误或者多余。因此在进行反序列化时进行参数验证是必不可少的。

class Post(db.Model):
    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body.strip() == '':
            raise ValidationError('The post body is Invalid.')
        return Post(body=body)

class Comment(db.Model):
    @staticmethod
    def from_json(json_comment):
        body = json_comment.get('body')
        if body is None or body.strip() == '':
            raise ValidationError('Invalid Comment')
        return Comment(body=body)

二. 实现资源的各个端点,并对大型资源集合进行分页

下面我们将分别实现如下API资源

用户资源端点的实现:

from flask import jsonify, request, current_app, url_for
from . import api
from ..models import User, Post


@api.route('/users/<int:id>')
def get_user(id):
    user = User.query.get_or_404(id)
    return jsonify(user.to_json())


@api.route('/users/<int:id>/posts/')
def get_user_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('id', 1, type=int)
    pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_user_posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_user_posts', id=id, page=page+1)
    first = url_for('api.get_user_posts', id=id, page=1)
    last = url_for('api.get_user_posts', id=id, page=pagination.pages)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'first': first,
        'next': next,
        'prev': prev,
        'last': last,
        'count': pagination.total,
        'pages': pagination.pages
    })


@api.route('/users/<int:id>/followed/posts/')
def get_followed_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(
        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_followed_posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_followed_posts', id=id, page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total,
        'pages': pagination.pages,
        'first': url_for('api.get_followed_posts', id=id, page=1),
        'last': url_for('api.get_followed_posts', id=id, page=pagination.pages)
    })

文章资源端点的实现:

from flask import jsonify, request, g, url_for,current_app
from ..models import Post, Permission
from .. import db
from . import api
from .decorators import permission_required
from .errors import forbidden


@api.route('/posts/')
def get_posts():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.order_by(Post.timestamp.desc()).paginate(
        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_posts', page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_posts', page=page+1)
    return jsonify({
        'posts': [ post.to_json() for post in posts ],
        'prev': prev,
        'next': next,
        'count': pagination.total,
        'pages': pagination.pages,
        'first': url_for('api.get_posts', page=1),
        'last': url_for('api.get_posts', page=pagination.pages)
    })


@api.route('/posts/<int:id>')
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())


@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE)
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json()), 201, {'Location': url_for('api.get_post', id=post.id)}


@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and not g.current_user.can(Permission.ADMIN):
        return forbidden('Insufficient permissions')
    post.body = request.json.get('body', post.body)
    db.session.commit()
    return jsonify(post.to_json())

       第一个路由处理获取文章集合的请求,使用列表推导生成文章的JSON版本,并使用paginate分页返回。prev_url和next_url分别是前一页和后一页的URL。如果某个方向没有分页了,则相应的字段为None。count是集合中元素的总数,pages是总页数。

       博客资源的POST请求处理程序把一篇博客文章插入数据库,返回201状态码,并把Location首部的值设为刚创建的资源的URL。注意,为了便于客户端操作,响应的主体中还包含了新建的资源。如此一来,客户端就无需在创建资源后再发起一次GET请求来获取资源。

       在创建和修改文章资源时,我们还使用到了一个装饰器:permission_required装饰器,用来检查权限,拒绝未授权用户访问相关视图:

app/api/decorators.py:permission_required装饰器

from functools import wraps
from flask import g
from .errors import forbidden


def permission_required(permission):
    def decorate(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not g.current_user.can(permission):
                return forbidden('Insufficient permissions')
            return f(*args, **kwargs)
        return decorated_function
    return decorate

       此外,视图函数中还需要检查用户是文章的作者或者管理员,如果这种检查要应用到多个视图函数中,为避免代码重复,也可以定义相关的装饰器。

comment资源端点的实现与post基本一致,不再赘述,直接贴出代码:

from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Comment, Post, Permission
from . import api
from .decorators import permission_required


@api.route('/comments/')
def get_comments():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_comments', page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_comments', page=page+1)
    return jsonify({
        'comments': [ comment.to_json() for comment in comments ],
        'prev': prev,
        'next': next,
        'total': pagination.total,
        'pages': pagination.pages,
        'first': url_for('api.get_comments', page=1),
        'last': url_for('api.get_comments', page=pagination.pages)
    })


@api.route('/comments/<int:id>')
def get_comment(id):
    comment = Comment.query.get_or_404(id)
    return jsonify(comment.to_json())


@api.route('/posts/<int:id>/comments/')
def get_post_comments(id):
    post = Post.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_post_comments', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_post_comments', id=id, page=page+1)
    return jsonify({
        'comments': [comment.to_json() for comment in comments],
        'prev': prev,
        'next': next,
        'total': pagination.total,
        'pages': pagination.pages,
        'first': url_for('api.get_post_comments', id=id, page=1),
        'last': url_for('api.get_post_comments', id=id, page=pagination.pages)
    })


@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMENT)
def new_post_comment(id):
    post = Post.query.get_or_404(id)
    comment = Comment.from_json(request.json)
    comment.author = g.current_user
    comment.post = post
    db.session.add(comment)
    db.session.commit()
    return jsonify(comment.to_json()), 201, {'Location': url_for('api.get_comment', id=comment.id)}

发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/104803871