需求背景:
自研运维发布系统前后端不分离结构,虽然功能比较完整,但是由于限制较多且规划不完善导致后续扩展较为困难,故开始对代码进行重构,进行前后端分离开发,而想要实现前后端分离,那么后端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
资源丰富的路由
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)
尝试测试:
端点
很多时候,在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)
测试请求:
数据格式化
默认情况下,返回可迭代对象中的所有字段将按原样呈现,当只处理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的请求解析接口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_decorators
method
的属性。您可以对资源进行子类化并添加自己的资源 将添加到资源中所有函数的装饰器。以下面实例,如果要在每个请求中构建自定义身份验证。
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)