Flask Api 文檔管理與 Swagger 上手

本文最早發佈在博客: https://blog.ihypo.net/152551...

Flask 是一個以自由度高、靈活性強著稱的 Python Web 框架。但高靈活性也意味着無盡的代碼維護成本、高自由度意味着代碼質量更依賴程序員自身而沒有一致的標準和規範。所以團隊內開發時 Flask 項目更須要創建代碼和文檔規範以保證不會出現太大的誤差。html

本文從 Api 的角度探究 Flask 項目的 Api 規範以及得到 Api 文檔的最佳姿式。衆數週知,文檔的編寫和整理工做將花費巨大精力甚至不亞於代碼的編寫,所以在時間緊任務重的狀況下,文檔是首先被忽略的工做。不過,就算項目在初期存在文檔,但在後面的迭代中,文檔落後嚴重,其產生的誤導比沒有文檔更加可怕。python

所以,我的認爲 文檔隨代碼走,代碼改動時文檔也應該跟進變更,但本着 人是不可靠的 原則,文檔理想上是應該由代碼生成,而不是靠人工維護。若是代碼有任何改動,文檔也能自動更新,這將是一件很是優雅的事情。雖然對不少文檔來講這並不現實,但對於 Api 文檔來講,實現成本並不高。程序員

Flask-RESTPlus

對於 REST Api 來講,Flask-RESTPlus 是一個優秀的 Api 文檔生成工具,這個包將會替換 Flask 路由層的編寫方式,經過本身的語法來規定 Api 細節,並生成 Api 文檔。數據庫

安裝

安裝 Flask-RESTPlusjson

pip install flask-restplus

或者:flask

easy_install flask-restplus

最小 Demo

使用 Flask-RESTPlus 時須要按照這個庫規定的方式編寫 Api 層,包括 request 的參數解析,以及 response 的返回格式。一個 hello world 級的示範:api

from flask import Flask
from flask_restplus import Resource, Api

app = Flask(__name__)
api = Api(app, prefix="/v1", title="Users", description="Users CURD api.")

@api.route('/users')
class UserApi(Resource):
    def get(self):
        return {'user': '1'}

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

運行以後效果以下:
bash

實踐

這裏我會實現一個完整的小項目來實踐和介紹 Flask-RESTPlus 這個庫。咱們實現一個簡單的 圖書訂單系統 ,實現用戶、圖書和訂單的 CURD。架構

Model

用戶 model,包含 id 和 username:app

class User(object):
    user_id = None
    username = None

    def __init__(self, username: str):
        self.user_id = str(uuid.uuid4())
        self.username = username

圖書 model,包含 id,名稱和價格:

class Book(object):
    book_id = None
    book_name = None
    price = None

    def __init__(self, book_name: str, book_price: float):
        self.book_id = str(uuid.uuid4())
        self.book_name = book_name
        self.price = book_price

訂單 model,包含 id,購買者 id,圖書 id 和建立時間:

class Order(object):
    order_id = None
    user_id = None
    book_id = None
    created_at = None

    def __init__(self, user_id, book_id):
        self.order_id = str(uuid.uuid4())
        self.user_id = user_id
        self.book_id = book_id
        self.created_at = int(time.time())

藍圖

在 Flask 中構建大型 Web 項目,能夠經過藍圖爲路由分組,並在藍圖中添加通用的規則(url 前綴、靜態文件路徑、模板路徑等)。這個項目咱們只用一個 api 藍圖,在實際中可能會使用 openapi 藍圖,internal api 藍圖來區分大的分類。

Flask-RESTPlusclass::Api 將直接掛在在藍圖下面,這麼咱們即利用了 Flask 的藍圖進行對功能模塊分類,也能夠利用 Api 的版本對 Api 版本進行管理,對於小的模塊分類,咱們能夠利用 Api 的 namespace,着這裏咱們能夠分爲 user namespacebook namespaceorder namespace:

Api 藍圖:

from flask import Blueprint
from flask_restplus import Api

api_blueprint = Blueprint("open_api", __name__, url_prefix="/api")
api = Api(api_blueprint, version="1.0",
          prefix="/v1", title="OpenApi", description="The Open Api Service")

而後,就能夠建立出不一樣的 namespace,來編寫本身的 api 代碼了。而只須要在 app 工廠中註冊該 blueprint,即可將本身的編寫的 api 掛載到 flask app 中。

def create_app():
    app = Flask("Flask-Web-Demo")

    # register api namespace
    register_api()

    # register blueprint
    from apis import api_blueprint
    app.register_blueprint(api_blueprint)

    return app

要注意的是,由於 Api 中不少工具方法依賴 api 對象,所以在註冊 namespace 的時候要避免循環引用,並且,這注冊藍圖的時候,須要先將 namespace 註冊,不然會 404。這個庫的不少方法太依賴 api 對象,感受設計並不合理,很容易就循環引用,並非很是優雅。

註冊 namespace:

def register_api():
    from apis.user_api import ns as user_api
    from apis.book_api import ns as book_api
    from apis.order_api import ns as order_api
    from apis import api
    api.add_namespace(user_api)
    api.add_namespace(book_api)
    api.add_namespace(order_api)

下面就是 Api 的編寫了。

編寫 Api

列表和建立

咱們先完成用戶的列表和建立 Api,代碼以下:

from flask_restplus import Resource, fields, Namespace

from model import User
from apis import api

ns = Namespace("users", description="Users CURD api.")

user_model = ns.model('UserModel', {
    'user_id': fields.String(readOnly=True, description='The user unique identifier'),
    'username': fields.String(required=True, description='The user nickname'),
})
user_list_model = ns.model('UserListModel', {
    'users': fields.List(fields.Nested(user_model)),
    'total': fields.Integer,
})


@ns.route("")
class UserListApi(Resource):
    # 初始化數據
    users = [User("HanMeiMei"), User("LiLei")]

    @ns.doc('get_user_list')
    @ns.marshal_with(user_list_model)
    def get(self):
        return {
            "users": self.users,
            "total": len(self.users),
        }

    @ns.doc('create_user')
    @ns.expect(user_model)
    @ns.marshal_with(user_model, code=201)
    def post(self):
        user = User(api.payload['username'])
        return user

解釋下上面的代碼,首先須要建立一個 user model 來讓 Flask-RESTPlus 知道咱們如何渲染和解析 json:

user_model = ns.model('UserModel', {
    'user_id': fields.String(readOnly=True, description='The user unique identifier'),
    'username': fields.String(required=True, description='The user nickname'),
})

這裏面定義了字段以及字段的描述,這些字段並不參與參數檢查,而只是渲染到 api 文檔上,來標記 api 將返回什麼結果,以及應該怎麼調用 api。

而後介紹下目前用到的裝飾器:

  1. @ns.doc 來標記這個 api 的做用
  2. @ns.marshal_with 來標記如何渲染返回的 json
  3. @ns.expect 來標記咱們預期什麼樣子的 request

運行程序咱們能夠看到如下結果:

咱們也能夠經過 try it 來調用 api:

查詢和更新

由於路由是綁定到一個類上的,所以限定了這個類能處理的 url,對於 '/users/user_id' 相似的路徑,須要單獨的類來處理:

@ns.route("/<string:user_id>")
@ns.response(404, 'User not found')
@ns.param('user_id', 'The user identifier')
class UserInfoApi(Resource):
    users = [User("HanMeiMei"), User("LiLei")]
    print([u.user_id for u in users])

    @ns.doc("get_user_by_id")
    @ns.marshal_with(user_model)
    def get(self, user_id):
        for u in self.users:
            if u.user_id == user_id:
                return u
        ns.abort(404, "User {} doesn't exist".format(user_id))

    @ns.doc("update_user_info")
    @ns.expect(user_model)
    @ns.marshal_with(user_model)
    def put(self, user_id):
        user = None
        for u in self.users:
            if u.user_id == user_id:
                user = u
        if not user:
            ns.abort(404, "User {} doesn't exist".format(user_id))
        user.username = api.payload['username']
        return user

在這裏面能夠看到更改了 url 和新引入了兩個裝飾器:

  1. @ns.response 用來標記可能出現的 Response Status Code 並渲染在文檔中
  2. @ns.param 用來標記 URL 參數

運行程序以後咱們能夠嘗試根據 id 得到一個用戶:

注意namespace 的 name 會被拼接到 url 中,好比上面 url 中的 「users」 便是 namespace name。

帶嵌套的 Api

用戶 Api 和圖書 Api 基本同樣並且簡單,可是對於訂單 Api 中,須要包含用戶信息和圖書信息,在實現上略微不一樣。

from flask_restplus import Resource, fields, Namespace

from model import Order, Book, User
from apis.user_api import user_model
from apis.book_api import book_model

ns = Namespace("order", description="Order CURD api.")

order_model = ns.model('OrderModel', {
    "order_id": fields.String(readOnly=True, description='The order unique identifier'),
    "user": fields.Nested(user_model, description='The order creator info'),
    "book": fields.Nested(book_model, description='The book info.'),
    "created_at": fields.Integer(readOnly=True, description='create time: unix timestamp.'),
})
order_list = ns.model('OrderListModel', {
    "orders": fields.List(fields.Nested(order_model)),
    "total": fields.Integer(description='len of orders')
})

book = Book("Book1", 10.5)
user = User("LiLei")
order = Order(user.user_id, book.book_id)


@ns.route("")
class UserListApi(Resource):

    @ns.doc('get_order_list')
    @ns.marshal_with(order_list)
    def get(self):
        return {
            "orders": [{
                "order_id": order.order_id,
                "created_at": order.created_at,
                "user": {
                    "user_id": user.user_id,
                    "username": user.username,
                },
                "book": {
                    "book_id": book.book_id,
                    "book_name": book.book_name,
                    "price": book.price,
                }
            }],
            "total": 1}

    @ns.doc('create_order')
    @ns.expect(order_model)
    @ns.marshal_with(order_model, code=201)
    def post(self):
        return {
            "order_id": order.order_id,
            "created_at": order.created_at,
            "user": {
                "user_id": user.user_id,
                "username": user.username,
            },
            "book": {
                "book_id": book.book_id,
                "book_name": book.book_name,
                "price": book.price,
            }
        }

這裏使用了更靈活的格式組合,包括 fields.Nested 能夠引入其餘 model,由於 model 能夠相互引用,所以仍是有必要把這些 model 放在一塊兒,來避免循環引用。不過由此也能夠看出,Response 解析仍是比較自由的。

備註:這裏 return 的是一個字典,可是理想狀態下應該是一個類(user 字段和 book 字段),只是由於沒有數據庫操做,簡化處理。

到這裏,這個小項目就是寫完了,最後運行效果圖以下:

改造

能夠經過這個簡單的 Demo 瞭解 Flask-RESTPlus 的使用,可是目前只是從零到一的寫一個完成的項目,所以看起來很是容易上手,可是若是是舊項目改造,咱們須要作什麼?

經過上述代碼,咱們能夠看到要作的主要是兩件事:

  1. Api 層的改造
  2. 設計 Api Model

Api 層改造涉及到兩點,由於 url 是由 blueprint、api obj、namespace 三個東西共同組成的,所以須要設計怎麼分配,可能還有重寫部分 api 的實現。可是理想的 api-service-model 架構的程序, api 應該是比較薄的一層,要接入並不困難,只是瑣碎。

Api Model 通常是原有項目沒有的,須要引入,其中包括的參數檢查的 model(Flask-RESTPlus 提供了 Request Parsing,本文並沒討論,能夠參考文檔: Request Parsing )和解析 Response 的 model,這些須要梳理全部 api 和字段,工做量不小,若是數據庫模型設計合理的話也許能減輕部分工做量。

Swagger

Swagger 是一款很是流行的 Api 文檔管理、交互工具,適用於在團隊中的 Api 管理,以及服務組件對接。其好用與重要程度沒必要贅言,下面基於上文的 demo,完成一個 Swagger 文檔以及基於文檔生成用於對接的 client。

得到 Swagger 文檔

Flask-RESTPlus 是已經集成了 Swagger UI 的,在運行時所得到界面便是經過 Swagger UI 渲染的。而咱們目前須要的是獲取 Swagger 文檔 json 或 yaml 文件。

在控制檯能夠看到,在訪問程序時:

是的,這就是 Swagger 文檔:

代碼生成

使用 Swagger 生成文檔須要

在 macOS 下載:

brew install swagger-codegen

而後能夠經過 help 名稱查看幫助:

Hypo-MBP:~ hypo$ swagger-codegen help
usage: swagger-codegen-cli <command> [<args>]

The most commonly used swagger-codegen-cli commands are:
    config-help   Config help for chosen lang
    generate      Generate code with chosen lang
    help          Display help information
    langs         Shows available langs
    meta          MetaGenerator. Generator for creating a new template set and configuration for Codegen.  The output will be based on the language you specify, and includes default templates to include.
    validate      Validate specification
    version       Show version information

See 'swagger-codegen-cli help <command>' for more information on a specific
command.

生成 Python client:

swagger-codegen generate -i http://127.0.0.1:5000/api/swagger.json -l python

執行完成後,即可以在當前路徑的 swagger_client 下找到 api client 了。

總結

本文介紹了 Flask-RESTPlus 的使用,由於其自己就支持 Swagger 語法並內置了 Swagger UI,因此 Swagger 對接簡單異常。所以,主要工做量放在了編寫 api 層上,包括 model,以及 api 中起到解釋說明做用的裝飾器。雖然在代碼上須要編寫很多沒必要要的代碼(介紹說明用的描述等),可是這些額外代碼輔助生成了與代碼一致的文檔,在組件對接和維護上,實則下降了成本。

歡迎關注我的公衆號:CS實驗室

相關文章
相關標籤/搜索