本文最早發佈在博客: https://blog.ihypo.net/152551...
Flask 是一個以自由度高、靈活性強著稱的 Python Web 框架。但高靈活性也意味着無盡的代碼維護成本、高自由度意味着代碼質量更依賴程序員自身而沒有一致的標準和規範。所以團隊內開發時 Flask 項目更須要創建代碼和文檔規範以保證不會出現太大的誤差。html
本文從 Api 的角度探究 Flask 項目的 Api 規範以及得到 Api 文檔的最佳姿式。衆數週知,文檔的編寫和整理工做將花費巨大精力甚至不亞於代碼的編寫,所以在時間緊任務重的狀況下,文檔是首先被忽略的工做。不過,就算項目在初期存在文檔,但在後面的迭代中,文檔落後嚴重,其產生的誤導比沒有文檔更加可怕。python
所以,我的認爲 文檔隨代碼走,代碼改動時文檔也應該跟進變更,但本着 人是不可靠的 原則,文檔理想上是應該由代碼生成,而不是靠人工維護。若是代碼有任何改動,文檔也能自動更新,這將是一件很是優雅的事情。雖然對不少文檔來講這並不現實,但對於 Api 文檔來講,實現成本並不高。程序員
對於 REST Api 來講,Flask-RESTPlus
是一個優秀的 Api 文檔生成工具,這個包將會替換 Flask 路由層的編寫方式,經過本身的語法來規定 Api 細節,並生成 Api 文檔。數據庫
安裝 Flask-RESTPlus
:json
pip install flask-restplus
或者:flask
easy_install flask-restplus
使用 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,包含 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-RESTPlus
的 class::Api
將直接掛在在藍圖下面,這麼咱們即利用了 Flask 的藍圖進行對功能模塊分類,也能夠利用 Api
的版本對 Api 版本進行管理,對於小的模塊分類,咱們能夠利用 Api
的 namespace,着這裏咱們能夠分爲 user namespace
,book namespace
和 order 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,代碼以下:
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。
而後介紹下目前用到的裝飾器:
@ns.doc
來標記這個 api 的做用@ns.marshal_with
來標記如何渲染返回的 json@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 和新引入了兩個裝飾器:
@ns.response
用來標記可能出現的 Response Status Code 並渲染在文檔中@ns.param
用來標記 URL 參數運行程序以後咱們能夠嘗試根據 id 得到一個用戶:
注意: namespace 的 name 會被拼接到 url 中,好比上面 url 中的 「users」 便是 namespace name。
用戶 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
的使用,可是目前只是從零到一的寫一個完成的項目,所以看起來很是容易上手,可是若是是舊項目改造,咱們須要作什麼?
經過上述代碼,咱們能夠看到要作的主要是兩件事:
Api 層改造涉及到兩點,由於 url 是由 blueprint、api obj、namespace 三個東西共同組成的,所以須要設計怎麼分配,可能還有重寫部分 api 的實現。可是理想的 api-service-model 架構的程序, api 應該是比較薄的一層,要接入並不困難,只是瑣碎。
Api Model 通常是原有項目沒有的,須要引入,其中包括的參數檢查的 model(Flask-RESTPlus
提供了 Request Parsing,本文並沒討論,能夠參考文檔: Request Parsing )和解析 Response 的 model,這些須要梳理全部 api 和字段,工做量不小,若是數據庫模型設計合理的話也許能減輕部分工做量。
Swagger 是一款很是流行的 Api 文檔管理、交互工具,適用於在團隊中的 Api 管理,以及服務組件對接。其好用與重要程度沒必要贅言,下面基於上文的 demo,完成一個 Swagger 文檔以及基於文檔生成用於對接的 client。
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實驗室