官方文档: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蓝图的理解)和命名空间的实践

  1. project/
  2. ├── static/ # 静态文件
  3. ├── templates/ # 模版文件
  4. ├── index.html
  5. ├── utils
  6. ├── __init__.py
  7. ├── app.py # 创建flask程序实例&链接mongo&加载app.config配置
  8. ├── main.py # 启动flask主程序
  9. └── apis # 资源文件(api路由)
  10. ├── __init__.py # 添加资源(add_namespace
  11. ├── namespace1.py
  1. # apis/namespace1.py
  2. from flask_restx import Resource, fields, Namespace
  3. from flask import render_template, make_response, jsonify
  4. from logzero import logger
  5. from utils.app import DB
  6. # 第一步:将应用程序拆分为可重用的命名空间(组织资源)
  7. api = Namespace(name="test_api", description="这是一个分类描述")
  8. cat = api.model(
  9. "Cat",
  10. {
  11. "id": fields.String(required=True, description="The cat identifier"),
  12. "name": fields.String(required=True, description="The cat name"),
  13. },
  14. )
  15. CATS = [
  16. {"id": "1", "name": "cat1"},
  17. ]
  18. @api.route("/mongo_test")
  19. class MonogoTest(Resource):
  20. @api.doc("这是一个链接mongo数据库测试接口")
  21. def get(self):
  22. """链接mongo数据库"""
  23. res = DB.test.find_one({})
  24. logger.info(f"DB:{res}")
  25. return make_response(jsonify(res['title']))
  26. @api.route("/<id>")
  27. @api.param("id", "The cat identifier")
  28. @api.response(404, "Cat not found")
  29. class Cat(Resource):
  30. @api.doc("get_cat")
  31. @api.marshal_with(cat)
  32. def get(self, id):
  33. """
  34. Fetch a cat given its identifier
  35. args:
  36. id: 可输入值为1
  37. """
  38. for cat in CATS:
  39. if cat["id"] == id:
  40. return cat
  41. api.abort(404)
  42. """make_respoapie函数在视图内控制响应对象的结果
  43. 如果视图返回的是一个响应对象,那么就直接返回它。
  44. 如果返回的是一个字符串,那么根据这个字符串和缺省参数生成一个用于返回的 响应对象。
  45. 如果返回的是一个字典,那么调用 jsonify 创建一个响应对象。
  46. 如果返回的是一个元组,那么元组中的项目可以提供额外的信息。元组中必须至少 包含一个项目,且项目应当由 (respoapie, status) 、 (respoapie, headers) 或者 (respoapie, status, headers) 组成。 status 的值会重载状态代码, headers 是一个由额外头部值组成的列表 或字典
  47. """
  48. @api.route("/index")
  49. class Home(Resource):
  50. @api.doc("这是一个打开index.html测试接口")
  51. def get(self):
  52. """打开index.html"""
  53. resp = make_response(render_template("index.html"), 404)
  54. resp.headers["x-Something"] = "S value"
  55. return resp
  56. todo = api.model('Todo', {
  57. 'id': fields.Integer(readonly=True, description='The task unique identifier'),
  58. 'task': fields.String(required=True, description='The task details')
  59. })
  60. class TodoDAO(object):
  61. def __init__(self):
  62. self.counter = 0
  63. self.todos = []
  64. def get(self, id):
  65. for todo in self.todos:
  66. if todo['id'] == id:
  67. return todo
  68. api.abort(404, "Todo {} doesn't exist".format(id))
  69. def create(self, data):
  70. todo = data
  71. todo['id'] = self.counter = self.counter + 1
  72. self.todos.append(todo)
  73. return todo
  74. def update(self, id, data):
  75. todo = self.get(id)
  76. todo.update(data)
  77. return todo
  78. def delete(self, id):
  79. todo = self.get(id)
  80. self.todos.remove(todo)
  81. DAO = TodoDAO()
  82. @api.route('/')
  83. class TodoList(Resource):
  84. '''Shows a list of all todos, and lets you POST to add new tasks'''
  85. @api.doc('list_todos')
  86. @api.marshal_list_with(todo)
  87. def get(self):
  88. '''List all tasks'''
  89. return DAO.todos
  90. @api.doc('create_todo')
  91. @api.expect(todo)
  92. @api.marshal_with(todo, code=201)
  93. def post(self):
  94. '''Create a new task'''
  95. return DAO.create(api.payload), 201
  96. @api.route('/<int:id>')
  97. @api.response(404, 'Todo not found')
  98. @api.param('id', 'The task identifier')
  99. class Todo(Resource):
  100. '''Show a single todo item and lets you delete them'''
  101. @api.doc('get_todo')
  102. @api.marshal_with(todo)
  103. def get(self, id):
  104. '''Fetch a given resource'''
  105. return DAO.get(id)
  106. @api.doc('delete_todo')
  107. @api.response(204, 'Todo deleted')
  108. def delete(self, id):
  109. '''Delete a task given its identifier'''
  110. DAO.delete(id)
  111. return '', 204
  112. @api.expect(todo)
  113. @api.marshal_with(todo)
  114. def put(self, id):
  115. '''Update a task given its identifier'''
  116. return DAO.update(id, api.payload)
  117. ---------------------------------------------------------------------------------------------------------------------------
  118. # apis/__init__.py
  119. from flask_restx import Api
  120. from .flask_restx_test import Todo, api as resrx_test_api
  121. from flask import Blueprint
  122. from .flask_restx_test import Home, Cat, MonogoTest, TodoList
  123. # 第二步: 创建蓝图
  124. blueprint = Blueprint("api", __name__, url_prefix="/api/v1")
  125. # 第三步:初始化应用程序入口
  126. api = Api(blueprint, title="Zaygee API", version="1.0", description="A simple demo API",)
  127. # Todo: 这种方式添加资源在api文档中无法正确展示命名空间的分类
  128. # 第五步:为给定的 API 命名空间注册资源
  129. # api.add_resource(Home, "/index")
  130. # api.add_resource(MonogoTest, "/mongo_test")
  131. # api.add_resource(Cat, "/<id>")
  132. # api.add_resource(TodoList, "/list")
  133. # 第四步:为api的当前实例注册命名空间
  134. api.add_namespace(resrx_test_api)
  135. ---------------------------------------------------------------------------------------------------------------------------
  136. # utils/app.py
  137. from logzero import logger
  138. from flask_pymongo import PyMongo
  139. from flask import Flask
  140. from dataclasses import dataclass # 定义数据类
  141. from werkzeug.middleware.proxy_fix import ProxyFix
  142. from pathlib import Path
  143. # 静态文件&模板文件目录
  144. static_folder = ''.join([str(Path(__file__).parent.parent), "/static"])
  145. template_folder = ''.join([str(Path(__file__).parent.parent), "/templates"])
  146. """创建flask程序实例&链接mongo"""
  147. app = Flask(__name__, static_folder=static_folder, template_folder=template_folder)
  148. app.wsgi_app = ProxyFix(app.wsgi_app)
  149. # 链接mongodb
  150. app.config["MONGO_URI"] = "mongodb://localhost:27017/test?authSource=admin" # 本地mongo服务
  151. mongo = PyMongo(app)
  152. DB = mongo.db
  153. logger.info(f"成功链接mongo: {DB}")
  154. ---------------------------------------------------------------------------------------------------------------------------
  155. # utils/main.py
  156. from flask_demo.utils.app import app
  157. from api_resource import blueprint as api1
  158. # 第六步:延迟注册app
  159. # api.init_app(app)
  160. # 第六步:注册蓝图
  161. app.register_blueprint(api1)
  162. # 第七步:执行应用程序
  163. app.run(debug=True)

路由&端点

  1. from flask_restx import Resource, Namespace
  2. from flask import make_response, jsonify, url_for
  3. from logzero import logger
  4. from utils.app import DB
  5. api = Namespace(name="route", description="接口路由&端点")
  6. @api.route("/hello", "/world", endpoint='mutil_endpoint')
  7. class MonogoTest(Resource):
  8. @api.doc("这是一个多url的")
  9. def get(self):
  10. """多url接口"""
  11. return "hello world!"

image.png

请求参数解析

location参数可指定提取参数的位置,可选如下:

args:路径参数 form:表单参数 headers:请求头参数 json:json请求体 values:请求体或查询参数

bundle_errors参数设置=True,可绑定错误处理一次返回

parser = reqparse.RequestParser(bundle_errors=True)

解析器继承

  1. from flask_restx import reqparse
  2. parser = reqparse.RequestParser()
  3. parser.add_argument('foo', type=int)
  4. # 复制
  5. parser_copy = parser.copy()
  6. parser_copy.add_argument('bar', type=int)
  7. # 替换
  8. parser_copy.replace_argument('foo', required=True, location='json')
  9. # 删除某个参数
  10. parser_copy.remove_argument('foo')
  1. # demo
  2. from flask_restx import reqparse, Resource, Namespace, abort
  3. from logzero import logger
  4. api = Namespace(name="reparse_test", description="请求参数解析")
  5. todos = {"a": "111", "2": "222"}
  6. @api.route("/todo")
  7. class Todo(Resource):
  8. # 请求参数解析,增加参数验证
  9. parser = reqparse.RequestParser()
  10. parser.add_argument('data', type=str, help='请求参数必须是字符串', required=True, location="json")
  11. parser.add_argument('todo_id', type=str, help='请求参数必须是字符串', required=True, location="json")
  12. parser.add_argument('age', type=int, help='请求参数必须是int', required=True, location="json")
  13. req_parser = reqparse.RequestParser()
  14. req_parser.add_argument('todo_id', type=str, help='请求参数必须是字符串', required=True, location="args")
  15. req_parser.add_argument('age', type=int, help='请求参数必须是int', required=True, location="args")
  16. @api.expect(parser)
  17. def put(self):
  18. """put请求-更新数据"""
  19. # strict=True 为强校验,包含解析器未定义的参数则抛出异常
  20. args = self.parser.parse_args(strict=True)
  21. todo_id = args["todo_id"]
  22. data = args["data"]
  23. age = args["age"]
  24. todos[todo_id] = {"data": data, "age": age}
  25. return {todo_id: todos[todo_id]}, 201
  26. @api.expect(parser)
  27. def post(self):
  28. """post请求-创建数据"""
  29. args = self.parser.parse_args(strict=True)
  30. todo_id = args["todo_id"]
  31. data = args["data"]
  32. age = args["age"]
  33. todos[todo_id] = {"data": data, "age": age}
  34. return {
  35. "status_code": 200,
  36. "msg": "success"
  37. }
  38. @api.expect(req_parser)
  39. def get(self):
  40. """get请求-获取数据"""
  41. args = self.req_parser.parse_args(strict=True)
  42. todo_id = args["todo_id"]
  43. logger.info(f'todo_id: {todo_id}')
  44. age = args['age']
  45. logger.info(f'age: {age}')
  46. if todos.get(todo_id) and dict(todos.get(todo_id)).get("age") == age:
  47. return {todo_id: todos.get(todo_id)}
  48. else:
  49. abort(404, message="Todo {} doesn't exist".format(todo_id))
  50. @api.expect(req_parser)
  51. def delete(self):
  52. """delete请求-删除数据"""
  53. args = self.req_parser.parse_args(strict=True)
  54. todo_id = args["todo_id"]
  55. age = args['age']
  56. if todos.get(todo_id) and dict(todos.get(todo_id)).get("age") == age:
  57. del todos[todo_id]
  58. return {
  59. "status_code": 204,
  60. "msg": "success"
  61. }
  62. else:
  63. abort(404, message="Todo {} 删除失败".format(todo_id))

image.png

响应结果验证

flask-restx通过fields模块提供控制在响应中呈现的数据或者期望作为输入的数据

Swagger文档

  1. @api.route():装饰允许你设置接口路由
  2. @api.doc():装饰允许你包含在文档中的附加信息
  3. @api.expect():装饰允许您指定预期的输入字段
  4. # demo
  5. from flask import Flask
  6. from flask_restx import Api, Resource, fields
  7. app = Flask(__name__)
  8. api = Api(app, version='1.0', title='Sample API',
  9. description='A sample API',
  10. )
  11. @api.route('/my-resource/<id>')
  12. @api.doc(params={'id': 'An ID'})
  13. class MyResource(Resource):
  14. def get(self, id):
  15. return {}
  16. @api.response(403, 'Not Authorized')
  17. def post(self, id):
  18. api.abort(403)
  19. if __name__ == '__main__':
  20. app.run(debug=True)