官方文档:https://flask-restx.readthedocs.io/en/latest/ https://dev.to/po5i/how-to-set-up-a-rest-api-in-flask-in-5-steps-5b7d
项目结构实践
结合蓝图(Flask蓝图的理解)和命名空间的实践
project/
├── static/ # 静态文件
├── templates/ # 模版文件
├── index.html
├── utils
│ ├── __init__.py
│ ├── app.py # 创建flask程序实例&链接mongo&加载app.config配置
│ ├── main.py # 启动flask主程序
└── apis # 资源文件(api路由)
├── __init__.py # 添加资源(add_namespace)
├── namespace1.py
# apis/namespace1.py
from flask_restx import Resource, fields, Namespace
from flask import render_template, make_response, jsonify
from logzero import logger
from utils.app import DB
# 第一步:将应用程序拆分为可重用的命名空间(组织资源)
api = Namespace(name="test_api", description="这是一个分类描述")
cat = api.model(
"Cat",
{
"id": fields.String(required=True, description="The cat identifier"),
"name": fields.String(required=True, description="The cat name"),
},
)
CATS = [
{"id": "1", "name": "cat1"},
]
@api.route("/mongo_test")
class MonogoTest(Resource):
@api.doc("这是一个链接mongo数据库测试接口")
def get(self):
"""链接mongo数据库"""
res = DB.test.find_one({})
logger.info(f"DB:{res}")
return make_response(jsonify(res['title']))
@api.route("/<id>")
@api.param("id", "The cat identifier")
@api.response(404, "Cat not found")
class Cat(Resource):
@api.doc("get_cat")
@api.marshal_with(cat)
def get(self, id):
"""
Fetch a cat given its identifier
args:
id: 可输入值为1
"""
for cat in CATS:
if cat["id"] == id:
return cat
api.abort(404)
"""make_respoapie函数在视图内控制响应对象的结果
如果视图返回的是一个响应对象,那么就直接返回它。
如果返回的是一个字符串,那么根据这个字符串和缺省参数生成一个用于返回的 响应对象。
如果返回的是一个字典,那么调用 jsonify 创建一个响应对象。
如果返回的是一个元组,那么元组中的项目可以提供额外的信息。元组中必须至少 包含一个项目,且项目应当由 (respoapie, status) 、 (respoapie, headers) 或者 (respoapie, status, headers) 组成。 status 的值会重载状态代码, headers 是一个由额外头部值组成的列表 或字典
"""
@api.route("/index")
class Home(Resource):
@api.doc("这是一个打开index.html测试接口")
def get(self):
"""打开index.html"""
resp = make_response(render_template("index.html"), 404)
resp.headers["x-Something"] = "S value"
return resp
todo = api.model('Todo', {
'id': fields.Integer(readonly=True, description='The task unique identifier'),
'task': fields.String(required=True, description='The task details')
})
class TodoDAO(object):
def __init__(self):
self.counter = 0
self.todos = []
def get(self, id):
for todo in self.todos:
if todo['id'] == id:
return todo
api.abort(404, "Todo {} doesn't exist".format(id))
def create(self, data):
todo = data
todo['id'] = self.counter = self.counter + 1
self.todos.append(todo)
return todo
def update(self, id, data):
todo = self.get(id)
todo.update(data)
return todo
def delete(self, id):
todo = self.get(id)
self.todos.remove(todo)
DAO = TodoDAO()
@api.route('/')
class TodoList(Resource):
'''Shows a list of all todos, and lets you POST to add new tasks'''
@api.doc('list_todos')
@api.marshal_list_with(todo)
def get(self):
'''List all tasks'''
return DAO.todos
@api.doc('create_todo')
@api.expect(todo)
@api.marshal_with(todo, code=201)
def post(self):
'''Create a new task'''
return DAO.create(api.payload), 201
@api.route('/<int:id>')
@api.response(404, 'Todo not found')
@api.param('id', 'The task identifier')
class Todo(Resource):
'''Show a single todo item and lets you delete them'''
@api.doc('get_todo')
@api.marshal_with(todo)
def get(self, id):
'''Fetch a given resource'''
return DAO.get(id)
@api.doc('delete_todo')
@api.response(204, 'Todo deleted')
def delete(self, id):
'''Delete a task given its identifier'''
DAO.delete(id)
return '', 204
@api.expect(todo)
@api.marshal_with(todo)
def put(self, id):
'''Update a task given its identifier'''
return DAO.update(id, api.payload)
---------------------------------------------------------------------------------------------------------------------------
# apis/__init__.py
from flask_restx import Api
from .flask_restx_test import Todo, api as resrx_test_api
from flask import Blueprint
from .flask_restx_test import Home, Cat, MonogoTest, TodoList
# 第二步: 创建蓝图
blueprint = Blueprint("api", __name__, url_prefix="/api/v1")
# 第三步:初始化应用程序入口
api = Api(blueprint, title="Zaygee API", version="1.0", description="A simple demo API",)
# Todo: 这种方式添加资源在api文档中无法正确展示命名空间的分类
# 第五步:为给定的 API 命名空间注册资源
# api.add_resource(Home, "/index")
# api.add_resource(MonogoTest, "/mongo_test")
# api.add_resource(Cat, "/<id>")
# api.add_resource(TodoList, "/list")
# 第四步:为api的当前实例注册命名空间
api.add_namespace(resrx_test_api)
---------------------------------------------------------------------------------------------------------------------------
# utils/app.py
from logzero import logger
from flask_pymongo import PyMongo
from flask import Flask
from dataclasses import dataclass # 定义数据类
from werkzeug.middleware.proxy_fix import ProxyFix
from pathlib import Path
# 静态文件&模板文件目录
static_folder = ''.join([str(Path(__file__).parent.parent), "/static"])
template_folder = ''.join([str(Path(__file__).parent.parent), "/templates"])
"""创建flask程序实例&链接mongo"""
app = Flask(__name__, static_folder=static_folder, template_folder=template_folder)
app.wsgi_app = ProxyFix(app.wsgi_app)
# 链接mongodb
app.config["MONGO_URI"] = "mongodb://localhost:27017/test?authSource=admin" # 本地mongo服务
mongo = PyMongo(app)
DB = mongo.db
logger.info(f"成功链接mongo: {DB}")
---------------------------------------------------------------------------------------------------------------------------
# utils/main.py
from flask_demo.utils.app import app
from api_resource import blueprint as api1
# 第六步:延迟注册app
# api.init_app(app)
# 第六步:注册蓝图
app.register_blueprint(api1)
# 第七步:执行应用程序
app.run(debug=True)
路由&端点
from flask_restx import Resource, Namespace
from flask import make_response, jsonify, url_for
from logzero import logger
from utils.app import DB
api = Namespace(name="route", description="接口路由&端点")
@api.route("/hello", "/world", endpoint='mutil_endpoint')
class MonogoTest(Resource):
@api.doc("这是一个多url的")
def get(self):
"""多url接口"""
return "hello world!"
请求参数解析
location参数可指定提取参数的位置,可选如下:
args:路径参数 form:表单参数 headers:请求头参数 json:json请求体 values:请求体或查询参数
bundle_errors参数设置=True,可绑定错误处理一次返回
parser = reqparse.RequestParser(bundle_errors=True)
解析器继承
from flask_restx import reqparse
parser = reqparse.RequestParser()
parser.add_argument('foo', type=int)
# 复制
parser_copy = parser.copy()
parser_copy.add_argument('bar', type=int)
# 替换
parser_copy.replace_argument('foo', required=True, location='json')
# 删除某个参数
parser_copy.remove_argument('foo')
# demo
from flask_restx import reqparse, Resource, Namespace, abort
from logzero import logger
api = Namespace(name="reparse_test", description="请求参数解析")
todos = {"a": "111", "2": "222"}
@api.route("/todo")
class Todo(Resource):
# 请求参数解析,增加参数验证
parser = reqparse.RequestParser()
parser.add_argument('data', type=str, help='请求参数必须是字符串', required=True, location="json")
parser.add_argument('todo_id', type=str, help='请求参数必须是字符串', required=True, location="json")
parser.add_argument('age', type=int, help='请求参数必须是int', required=True, location="json")
req_parser = reqparse.RequestParser()
req_parser.add_argument('todo_id', type=str, help='请求参数必须是字符串', required=True, location="args")
req_parser.add_argument('age', type=int, help='请求参数必须是int', required=True, location="args")
@api.expect(parser)
def put(self):
"""put请求-更新数据"""
# strict=True 为强校验,包含解析器未定义的参数则抛出异常
args = self.parser.parse_args(strict=True)
todo_id = args["todo_id"]
data = args["data"]
age = args["age"]
todos[todo_id] = {"data": data, "age": age}
return {todo_id: todos[todo_id]}, 201
@api.expect(parser)
def post(self):
"""post请求-创建数据"""
args = self.parser.parse_args(strict=True)
todo_id = args["todo_id"]
data = args["data"]
age = args["age"]
todos[todo_id] = {"data": data, "age": age}
return {
"status_code": 200,
"msg": "success"
}
@api.expect(req_parser)
def get(self):
"""get请求-获取数据"""
args = self.req_parser.parse_args(strict=True)
todo_id = args["todo_id"]
logger.info(f'todo_id: {todo_id}')
age = args['age']
logger.info(f'age: {age}')
if todos.get(todo_id) and dict(todos.get(todo_id)).get("age") == age:
return {todo_id: todos.get(todo_id)}
else:
abort(404, message="Todo {} doesn't exist".format(todo_id))
@api.expect(req_parser)
def delete(self):
"""delete请求-删除数据"""
args = self.req_parser.parse_args(strict=True)
todo_id = args["todo_id"]
age = args['age']
if todos.get(todo_id) and dict(todos.get(todo_id)).get("age") == age:
del todos[todo_id]
return {
"status_code": 204,
"msg": "success"
}
else:
abort(404, message="Todo {} 删除失败".format(todo_id))
响应结果验证
flask-restx通过fields模块提供控制在响应中呈现的数据或者期望作为输入的数据
Swagger文档
@api.route():装饰允许你设置接口路由
@api.doc():装饰允许你包含在文档中的附加信息
@api.expect():装饰允许您指定预期的输入字段
# demo
from flask import Flask
from flask_restx import Api, Resource, fields
app = Flask(__name__)
api = Api(app, version='1.0', title='Sample API',
description='A sample API',
)
@api.route('/my-resource/<id>')
@api.doc(params={'id': 'An ID'})
class MyResource(Resource):
def get(self, id):
return {}
@api.response(403, 'Not Authorized')
def post(self, id):
api.abort(403)
if __name__ == '__main__':
app.run(debug=True)