需求背景:

自研运维发布系统前后端不分离结构,虽然功能比较完整,但是由于限制较多且规划不完善导致后续扩展较为困难,故开始对代码进行重构,进行前后端分离开发,而想要实现前后端分离,那么后端flask是必须要了解restful的。


REST概念:

由Roy Fielding在其博士论文中介绍了web服务的RESET架构方式,并列出这一架构定义的特征:

客户端-服务器

客户端和服务器之间必须有明确的界线

无状态

客户端发出的请求必须包含所有必要的信息,服务器不能在两次请求之间保存客户端的任何状态


缓存

服务器发出的响应可以标记为可缓存或不可缓存,这样出于优化目的,客户端(或客户端和服务器之间的中间服务)可以使用缓存


接口统一

客户端访问服务器资源时使用的协议必须一致,定义良好,且已经标准化,REST Web服务最常使用的统一接口是HTTP协议


系统分层

在客户端和服务器之间可以按需插入代理服务器,缓存或网关,以提高性能,稳定性和伸缩性


按需代码

客户端可以选择从服务器上下载代码,在客户端的环境中执行


资源就是一切

资源时REST架构方式的核心概念,在REST架构中,资源是程序中要关注的事务,例如,在博客程序中,用户,博客文章和评论都是资源

每个资源都要使用唯一的url表示,以博客程序为例,一篇博客文章可以使用url /api/posts/12345表示,其中12345是这篇文章的唯一标识符,使用文章在数据库中的主键表示,url的格式或内容无关紧要,只要资源的url只表示唯一的一个资源即可。

某一类资源的集合也要有一个url,博客文章集合的url可以是 /api/posts/, 评论集合的url,可以是 /api/comments

API还可以为某一类资源的逻辑子集定义集合URL,例如,编号12345的博客文章,其中的所有评论可以使用URL  /api/posts/12345/comments/表示,表示资源集合的URL习惯在末端加上一个斜线,代表一种“文件夹”结构


请求方法

请求方法

目标

说明

HTTP状态码

GET

单个资源的URL

获取目标资源

200

GET

资源集合的URL

获取资源的集合

200

POST

资源集合的URL

创建新资源并将其加入目标集合,服务器为新资源指派URL,并在响应的location中返回

200

PUT

单个资源的URL

修改一个现有资源,如果客户端能为资源指派url,还可以用来创建新资源

200

DELETE

单个资源的URL

删除一个资源

200

DELETE

集合资源的URL

删除目标集合中的所有数据

200


请求和响应主体

在请求和响应的主体中,资源在客户端和服务器之间来回传送,但REST没有指定编码资源的方式,请求和响应中的Content-Type首部用于指明主体中的编码方式,使用HTTP协议中的内容协商机制,可以找到一种客户端和服务器都支持的编码方式

RESET WEB服务常用的两种编码方式是JavaScript对象表示法(JSON)和可扩展标记语言(XML),一般来说JSON用的是最多的


Flask-RESTful最佳实践

安装

pip install flask-restful


入门

最小的接口

通过pycharm新建一个flask项目

编写一个最小的示例:

from flask import Flask
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

class HelloWorld(Resource):
    def get(self):
        return {'hello': 'world'}

api.add_resource(HelloWorld, '/')

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

运行项目访问打印出的url

Flask Restful风格详解_客户端


资源丰富的路由
from flask import Flask, request
from flask_restful import Resource, Api

app = Flask(__name__)
api = Api(app)

todos = {}

class TodoSimple(Resource):
    def get(self, todo_id):
        return {todo_id: todos[todo_id]}

    def put(self, todo_id):
        todos[todo_id] = request.form['data']
        return {todo_id: todos[todo_id]}

api.add_resource(TodoSimple, '/todos/<string:todo_id>')

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


尝试测试:

Flask Restful风格详解_字段_02


端点

很多时候,在API中,我们的资源会有多个URL ,可以将多个URL的每一个都路由指向到API对象上

api.add_resource(HelloWorld,
    '/',
    '/hello')


还可以将部分路径作为变量与资源方法进行匹配

api.add_resource(Todo,
    '/todo/<int:todo_id>', endpoint='todo_ep')


参数解析

虽然Flask提供了对请求数据(即查询字符串或post的轻松访问 表单编码数据),但验证表单数据仍然很痛苦,flask内置支持使用类似于argparse的库进行请求数据的验证

from flask import Flask
from flask_restful import Api, Resource, reqparse

app = Flask(__name__)
api = Api(app)

# 创建一个RequestParse对象
parser = reqparse.RequestParser()
# 添加参数解析规则
parser.add_argument('name', type=str, help='Name parameter is required', required=True)


class MyResource(Resource):
    def get(self):
        # 解析请求参数
        args = parser.parse_args()
        name = args['name']
        # 处理业务逻辑
        response = {'name': name}
        # 返回响应
        return response


api.add_resource(MyResource, '/myresource')

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


测试请求:

Flask Restful风格详解_客户端_03


数据格式化

默认情况下,返回可迭代对象中的所有字段将按原样呈现,当只处理python数据结构时,这非常有用,但处理对象时可能会变得非常令人沮丧,为了解决这个问题,Flask-RESTful提供了模块和装饰器,类似于django orm和wtform,可以使用fields该模块描述响应的结构

from flask_restful import fields, marshal_with

resource_fields = {
    'task':   fields.String,
    'uri':    fields.Url('todo_ep')
}

class TodoDao(object):
    def __init__(self, todo_id, task):
        self.todo_id = todo_id
        self.task = task

        # This field will not be sent in the response
        self.status = 'active'

class Todo(Resource):
    @marshal_with(resource_fields)
    def get(self, **kwargs):
        return TodoDao(todo_id='my_todo', task='Remember the milk')


完整示例
from flask import Flask
from flask_restful import reqparse, abort, Api, Resource

app = Flask(__name__)
api = Api(app)

TODOS = {
    'todo1': {'task': 'build an API'},
    'todo2': {'task': '?????'},
    'todo3': {'task': 'profit!'},
}


def abort_if_todo_doesnt_exist(todo_id):
    if todo_id not in TODOS:
        abort(404, message="Todo {} doesn't exist".format(todo_id))

parser = reqparse.RequestParser()
parser.add_argument('task')


# Todo
# shows a single todo item and lets you delete a todo item
class Todo(Resource):
    def get(self, todo_id):
        abort_if_todo_doesnt_exist(todo_id)
        return TODOS[todo_id]

    def delete(self, todo_id):
        abort_if_todo_doesnt_exist(todo_id)
        del TODOS[todo_id]
        return '', 204

    def put(self, todo_id):
        args = parser.parse_args()
        task = {'task': args['task']}
        TODOS[todo_id] = task
        return task, 201


# TodoList
# shows a list of all todos, and lets you POST to add new tasks
class TodoList(Resource):
    def get(self):
        return TODOS

    def post(self):
        args = parser.parse_args()
        todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
        todo_id = 'todo%i' % todo_id
        TODOS[todo_id] = {'task': args['task']}
        return TODOS[todo_id], 201

##
## Actually setup the Api resource routing here
##
api.add_resource(TodoList, '/todos')
api.add_resource(Todo, '/todos/<todo_id>')


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


运行项目后测试请求:

获取列表:

Flask Restful风格详解_字段_04

获取单个任务:

Flask Restful风格详解_flask_05


删除任务:

Flask Restful风格详解_客户端_06


添加新任务:

Flask Restful风格详解_flask_07

更新任务:

Flask Restful风格详解_字段_08


请求解析

flask-RESTful的请求解析接口reqparse是模仿的reqparse,它旨在提供对flask中 falsk.request中对象上任何变量的简单而统一的访问。


基本参数
from flask_restful import reqparse

parser = reqparse.RequestParser()
parser.add_argument('rate', type=int, help='Rate cannot be converted')
parser.add_argument('name')
args = parser.parse_args()

如果请求中指定了参数的值,在解析时引发类型错误时,它将呈现错误信息,如果未指定help,默认行为是从类型错误本身返回消息

默认情况下,参数不是必需的,此外,在不属于请求解析器里的请求参数将被忽略

另外注意:在请求解析器中声明,但是请求中未设置的参数 请求本身将默认其为None


必需参数

若要求必需为参数传递值,只需要添加required=True

parser.add_argument('name', required=True,
help="Name cannot be blank!")


多个值和列表

如果要接受建的多个值作为列表,可以传递action='append'

parser.add_argument('name', action='append')

这将允许您进行查询,例如

curl http://api.example.com -d "name=bob" -d "name=sue" -d "name=joe"


其他目的地

如果处于某种原因,希望将参数存储在不同的名称后下一次解析,可以使用关键字参数dest

parser.add_argument('name', dest='public_name')

args = parser.parse_args()
args['public_name']
参数位置

默认情况下,尝试解析flask.request.values 和flask.request.json的值

使用参数指定要从中提取值得备用位置,可以使用任何变量,例如:location

# Look only in the POST body
parser.add_argument('name', type=int, location='form')

# Look only in the querystring
parser.add_argument('PageSize', type=int, location='args')

# From the request headers
parser.add_argument('User-Agent', location='headers')

# From http cookies
parser.add_argument('session_id', location='cookies')

# From file uploads
parser.add_argument('picture', type=werkzeug.datastructures.FileStorage, location='files')


多个地点

可以通过将列表传递给location来指定多个参数位置

parser.add_argument('text', location=['headers', 'values'])


解析器继承

通常,您将为您编写的每个资源创建不同的解析器,问题所在,如果解析器有共同的参数,为了避免重写解析器参数,可以编写包含所有共享参数的父解析器,这样你可以覆盖父级中的任何参数扩展解析器,或将其完全删除

from flask_restful import reqparse

parser = reqparse.RequestParser()
parser.add_argument('foo', type=int)

parser_copy = parser.copy()
parser_copy.add_argument('bar', type=int)

# parser_copy has both 'foo' and 'bar'

parser_copy.replace_argument('foo', required=True, location='json')
# 'foo' is now a required str located in json, not an int as defined
#  by original parser

parser_copy.remove_argument('foo')
# parser_copy no longer has 'foo' argument


错误处理
from flask_restful import reqparse

parser = reqparse.RequestParser(bundle_errors=True)
parser.add_argument('foo', type=int, required=True)
parser.add_argument('bar', type=int, required=True)

# If a request comes in not containing both 'foo' and 'bar', the error that
# will come back will look something like this.

{
    "message":  {
        "foo": "foo error message",
        "bar": "bar error message"
    }
}

请求解析器处理错误的默认方式是在 发生的第一个错误,但我想一次性报出错误返回给客户端,调用具有捆绑错误的请求分析器选项,传入参数,例如:bundle_errors

应用程序配置

from flask import Flask

app = Flask(__name__)
app.config['BUNDLE_ERRORS'] = True


自定义错误消息
from flask_restful import reqparse

parser = reqparse.RequestParser()
parser.add_argument(
    'foo',
    choices=('one', 'two'),
    help='Bad choice: {error_msg}'
)

# If a request comes in with a value of "three" for `foo`:

{
    "message":  {
        "foo": "Bad choice: three is not a valid choice",
    }
}


help后面可以是一个插值语句,这允许 在保留原始错误的同时自定义消息{error_msg}


输出字段

Flask-RESTful提供一种简单的方法来控制你实际呈现的数据使用fields模块你可以使用任何对象在你想要的资源中,让你格式和过滤响应,这样你就不必担心公开内部数据结构

from flask_restful import Resource, fields, marshal_with

resource_fields = {
    'name': fields.String,
    'address': fields.String,
    'date_updated': fields.DateTime(dt_format='rfc822'),
}

class Todo(Resource):
    @marshal_with(resource_fields, envelope='resource')
    def get(self, **kwargs):
        return db_get_todo()  # Some function that queries the db


重命名属性

通常,面向公众的字段名称与内部字段名字不同,若要配置此映射,需要使用到关键字attribute

fields = {
    'name': fields.String(attribute='private_name'),
    'address': fields.String,
}


lambda(或任何可调用对象)也可以指定为attribute

fields = {
    'name': fields.String(attribute=lambda x: x._private_name),
    'address': fields.String,
}


嵌套属性也可以使用attribute

fields = {
    'name': fields.String(attribute='people_list.0.person_dictionary.name'),
    'address': fields.String,
}


默认值

如果由于某种原因,你的数据对象字段列表中没有定义该属性,该属性可以指定要返回的默认值,而不是None

fields = {
    'name': fields.String(default='Anonymous User'),
    'address': fields.String,
}


自定义字段和多个值

有时,您有自己的自定义格式需求。您可以对类进行子类化并实现函数。这尤其 当属性存储多条信息时很有用。例如 位字段,其各个位表示不同的值。您可以使用字段 将单个属性多路复用为多个输出值。format

此示例假定属性中的位 1 表示 “正常”或“紧急”项目,位 2 表示“已读”或“未读”。这些 项目可能很容易存储在位域中,但对于人类可读的输出 将它们转换为单独的字符串字段很好。flags

class UrgentItem(fields.Raw):
    def format(self, value):
        return "Urgent" if value & 0x01 else "Normal"

class UnreadItem(fields.Raw):
    def format(self, value):
        return "Unread" if value & 0x02 else "Read"

fields = {
    'name': fields.String,
    'priority': UrgentItem(attribute='flags'),
    'status': UnreadItem(attribute='flags'),
}


FlaskRestful扩展

内容协商

开箱即用,Flask-RESTful 仅配置为支持 JSON

app = Flask(__name__)
api = Api(app)

@api.representation('application/json')
def output_json(data, code, headers=None):
    resp = make_response(json.dumps(data), code)
    resp.headers.extend(headers or {})
    return resp


自定义字段和输入
class AllCapsString(fields.Raw):
    def format(self, value):
        return value.upper()


# example usage
fields = {
    'name': fields.String,
    'all_caps_name': AllCapsString(attribute=name),
}


输入

对于分析参数,您可能需要执行自定义验证。创建 您自己的输入类型可让您轻松扩展请求解析。

def odd_number(value):
    if value % 2 == 0:
        raise ValueError("Value is not odd")

    return value

请求解析器还将允许您访问 要在错误消息中引用名称的情况。

def odd_number(value, name):
    if value % 2 == 0:
        raise ValueError("The parameter '{}' is not odd. You gave us the value: {}".format(name, value))

    return value


响应格式

要支持其他表示形式(xml、csv、html),您可以使用装饰器representation。

api = Api(app)

@api.representation('text/csv')
def output_csv(data, code, headers=None):
    pass
    # implement csv output!


资源方法装饰器

类上有一个名为 method_decoratorsmethod的属性。您可以对资源进行子类化并添加自己的资源 将添加到资源中所有函数的装饰器。以下面实例,如果要在每个请求中构建自定义身份验证。

def authenticate(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not getattr(func, 'authenticated', True):
            return func(*args, **kwargs)

        acct = basic_authentication()  # custom account lookup function

        if acct:
            return func(*args, **kwargs)

        flask_restful.abort(401)
    return wrapper


class Resource(flask_restful.Resource):
    method_decorators = [authenticate]   # applies to all inherited resources


相关用法

项目结构
myapi/
    __init__.py
    app.py          # this file contains your app and routes
    resources/
        __init__.py
        foo.py      # contains logic for /Foo
        bar.py      # contains logic for /Bar
    common/
        __init__.py
        util.py     # just some common infrastructure
与蓝图一起使用
from flask import Flask, Blueprint
from flask_restful import Api, Resource, url_for

app = Flask(__name__)
api_bp = Blueprint('api', __name__)
api = Api(api_bp)

class TodoItem(Resource):
    def get(self, id):
        return {'task': 'Say "Hello, World!"'}

api.add_resource(TodoItem, '/todos/<int:id>')
app.register_blueprint(api_bp)