Python進階(3) Flask & Swagger

0. 前言

  • 之前是Java後端工程師,寫過不少代碼。現在一方面好久沒寫Java了,一方面也想省力,所以就用了Flask。
  • 參考資料:
  • 安裝:
    • 安裝flask:pip install flask
    • 安裝swagger:pip install flask-restplus

1. 基本功能

  • 需要注意的是,使用 flask-restplus 後,設置路徑、參數的方法與原始flask有所不同。
  • 本文記錄的都是 flask-restplus 的功能。
  • 需要實現的功能:
    • 構建URL、設置靜態文件(1.1. 最簡單的實例
    • 設置請求方法(POST/GET/…)(1.2. 設置請求方法
    • 設置參數,包括URL參數和body內參數(1.3. 設置參數

1.1. 最簡單的實例

  • 以下實例來自 Flask-RESTPlus 教程。
from flask import Flask
from flask_restplus import Resource, Api

app = Flask(__name__)
api = Api(app)

@api.route('/hello')
class HelloWorld(Resource):
    def get(self):
        return {'hello': 'world'}

if __name__ == '__main__':
    app.run(debug=True)
  • 應用對象 Flask
    • 主要參數介紹:
      • import_name:應用名稱。
      • static_url_path:靜態路徑對應的URL,默認爲static_folder的文件夾名,如果不爲空則必須以/開頭。
      • static_folder:靜態路徑對應的文件夾,默認是static文件夾。
      • static_path:deprecated,建議使用 static_url_path 替代。
    • 靜態文件獲取主要通過上述幾個參數。
    • 對象定義:
class flask.Flask(import_name, 
                  static_path=None, static_url_path=None, static_folder='static', 
                  template_folder='templates', 
                  instance_path=None, instance_relative_config=False)
  • 應用運行 app.run()
    • 主要參數:port, host, debug
    • host 設置訪問權限,如果是127.0.0.1則只能本地訪問,如果是 0.0.0.0 則服務器公開可用。
    • 調試模式(即debug=True):使得程序修改及時生效。但對於Flask對象的修改不會及時生效。
  • 構建url主要通過 api.route 實現。

1.2. 設置請求方法

  • 主要就是在Resource類中新建對應的方法。
@api.route('/my-resource/<id>', endpoint='my-resource')
class MyResource(Resource):
    def get(self, id):
        return {}
    
    def post(self, id):
        return {}

1.3. 設置參數

  • url參數在 api.route 中定義,可同時設置參數數據類型。
    • 參數類型默認是string,還可以設置爲int/float/string
  • 獲取輸入數據body中的json形式的參數。
    • 通過 request 對象獲取,即request.json
@api.route('/<string:source>/<string:category_name>')
class CompareApis(Resource):
    def get(self, source, category_name):
        return {}

    def post(self, source, category_name):
        json_data = request.json
        attr1 = json_data.get('attr1')
        attr2 = json_data.get('attr2')
        attr3 = json_data.get('attr3')
        return {}

2. 註解介紹

  • 註解分類:
    • 整個swagger頁面的註解(2.1. 基本對象 & 2.2. api.model 的使用
    • 每一類接口的註解(2.1. 基本對象 & 2.3. 每一類接口的註解
    • 每個接口的註解(2.1. 基本對象 & 2.4. 每個接口的註解
    • 接口中每個參數的註解(2.5. url參數註解

2.1. 基本對象

  • Api 對象
    • 主要參數
      • appFlask 對象
      • version:版本,swagger顯示內容之一。
      • title:標題,swagger顯示內容之一
      • description:簡單介紹,swagger顯示內容之一
      • contact:聯繫人,swagger顯示內容之一
      • doc:swagger頁面地址,默認爲/
      • default:默認 namespace 名稱。
    • 猜測:是不是應該把 Api 對象也看作一個 namespace
    • 初始化定義以及對應註釋
'''
The main entry point for the application.
You need to initialize it with a Flask Application: ::

>>> app = Flask(__name__)
>>> api = Api(app)

Alternatively, you can use :meth:`init_app` to set the Flask application
after it has been constructed.

The endpoint parameter prefix all views and resources:

    - The API root/documentation will be ``{endpoint}.root``
    - A resource registered as 'resource' will be available as ``{endpoint}.resource``

:param flask.Flask|flask.Blueprint app: the Flask application object or a Blueprint
:param str version: The API version (used in Swagger documentation)
:param str title: The API title (used in Swagger documentation)
:param str description: The API description (used in Swagger documentation)
:param str terms_url: The API terms page URL (used in Swagger documentation)
:param str contact: A contact email for the API (used in Swagger documentation)
:param str license: The license associated to the API (used in Swagger documentation)
:param str license_url: The license page URL (used in Swagger documentation)
:param str endpoint: The API base endpoint (default to 'api).
:param str default: The default namespace base name (default to 'default')
:param str default_label: The default namespace label (used in Swagger documentation)
:param str default_mediatype: The default media type to return
:param bool validate: Whether or not the API should perform input payload validation.
:param bool ordered: Whether or not preserve order models and marshalling.
:param str doc: The documentation path. If set to a false value, documentation is disabled.
            (Default to '/')
:param list decorators: Decorators to attach to every resource
:param bool catch_all_404s: Use :meth:`handle_error`
    to handle 404 errors throughout your app
:param dict authorizations: A Swagger Authorizations declaration as dictionary
:param bool serve_challenge_on_401: Serve basic authentication challenge with 401
    responses (default 'False')
:param FormatChecker format_checker: A jsonschema.FormatChecker object that is hooked into
    the Model validator. A default or a custom FormatChecker can be provided (e.g., with custom
    checkers), otherwise the default action is to not enforce any format validation.
'''

def __init__(self, 
        app=None,
        version='1.0', title=None, description=None,
        terms_url=None, license=None, license_url=None,
        contact=None, contact_url=None, contact_email=None,
        authorizations=None, security=None, doc='/', default_id=default_id,
        default='default', default_label='Default namespace', validate=None,
        tags=None, prefix='', ordered=False,
        default_mediatype='application/json', decorators=None,
        catch_all_404s=False, serve_challenge_on_401=False, format_checker=None,
        **kwargs):
  • namespace 對象
    • 構建方法:api.namespace()
    • 主要功能:Group resources together,我的理解就是獎若干個接口放到一個組裏一起顯示。
    • 主要參數:
      • name:名稱
      • description:swagger註解,每一類接口的簡單說明。
      • path:相關接口URL統一前綴,默認情況下爲/{name},其中{name}就是第一個參數。
    • 對應初始化函數以及對應註釋。
'''
Group resources together.

Namespace is to API what :class:`flask:flask.Blueprint` is for :class:`flask:flask.Flask`.

:param str name: The namespace name
:param str description: An optionale short description
:param str path: An optional prefix path. If not provided, prefix is ``/+name``
:param list decorators: A list of decorators to apply to each resources
:param bool validate: Whether or not to perform validation on this namespace
:param bool ordered: Whether or not to preserve order on models and marshalling
:param Api api: an optional API to attache to the namespace
'''
def __init__(self, name, description=None, path=None, decorators=None, validate=None,
        authorizations=None, ordered=False, **kwargs):

2.2. api.model 的使用

  • 對應文檔:
  • 作用:
    • 構建接口輸出的形式。
    • 構建接口輸入的形式。
  • 整體思路:每個model擁有一個名稱以及一個字典。
    • 字典表示該model中屬性的名稱(key)以及對應的特徵(value)。
    • model可以嵌套使用。
  • 構建注意事項:
    • 構建方法:api.model
    • 主要通過 flask_restplus.fields 中各各類實現。
    • fields.Raw 是所有類型對象的基類,包括的主要參數有:
      • attribute:重命名屬性
      • default:默認值
      • title:用於文檔註解。
      • description:說明,用於文檔註解。
      • required:bool,用於文檔註解。
      • readonly:bool,用於文檔註解。
  • 如何用於接口輸入、輸出的描述:
    • api.marshal_with(my_model, as_list=False):用於描述接口輸出。可以設置 as_list 來表示輸出的是一個序列。
    • api.expect():用於描述接口的輸入。如果要設置輸入的爲序列,則可以使用 @api.expect[my_model]
  • 舉例(僅關鍵代碼)
    • 構建了model。
    • 將該模型作爲輸出、輸出模型。
person = api.model('Person', {
    'name': fields.String(
        attribute="private_name",
        default="John",
        required=True,
        readonly=True,
        title="person_title",
        description="person_description",
    ),
    'age': fields.Integer,
})

school = api.model('School', {
    'name': fields.String,
    'students': fields.List(fields.Nested(person)),
    'teachers': fields.List(fields.Nested(person)),
})

@api.route('/my-resource/<id>', endpoint='my-resource')
class MyResource(Resource):
    @api.marshal_with(school, as_list=True) # 作爲輸出model
    @api.expect(school) # 作爲輸入model
    def get(self, id):
        return {}
  • 上述實例對應的文檔圖片。
    • 從圖片上看,好像設置的那些參數,如required, default 等都不是特別清晰。
    • image_1dmv0fparp4s1iifna1i91elc1t.png-49.4kB
    • image_1dmv0i67v1aqcorr1lj71b0d1v7v2q.png-23.6kB

2.2. 整個swagger頁面的註解

  • 在初始化 Api 對象時構建,具體查看 2.1. 對應內容。
  • 效果如下圖。
    image_1dmunsmsq1fgcmcj1orihgeb4n9.png-32.6kB

2.3. 每一類接口的註解

  • 在初始化 namespace 對象時構建。
    • 大概形式就是 api.namespace(name='', description="type in here")
  • 效果如下圖。
    image_1dmunu6lm5fuf5u4th1r63md2m.png-60.7kB

2.4. 每個接口的註解

  • 在定義URL的route方法中構建。
    • 大概形式就是@api.route('', doc={"description": "type in here"})
  • 效果如下圖。
    image_1dmuo56a71md01d3fovu1opl105l13.png-95.8kB

2.5. url參數註解

  • 可使用 @api.doc(params={"id": "An ID", description="My resource"}) 註解對應class。
  • 可使用多個 @api.param('id', 'An ID') 註解對應class。
  • 效果如下圖。
    image_1dmv0nbma19t61mnf1o9ufqdvoe37.png-69.6kB

3. 舉例

import sys
import getopt
from flask import Flask, request
from flask_restplus import Api, Resource
from server.apis import category_api, category_models, category_api_text, \
    compare_models, compare_api, compare_api_text

app = Flask(__name__, static_folder="/root/street-score")
api = Api(app,
          version="0.1",
          title="街景圖像研究",
          description="基於街景圖像的安全感評估模型前端展示API",
          doc='/swagger-ui.html')
category_models.get_category_models(api)
compare_models.get_compare_models(api)

port = 8080
nodes_pre = ''
compare_pre = ''


def pre_np(pre, default):
    return default if pre == '' else '{}/{}'.format(pre, default)


argv = sys.argv
opts, args = getopt.getopt(
    argv[1:], '-h-c:-n:-p:', ['help', 'compare=', 'nodes=', 'port='])
for opt_name, opt_value in opts:
    if opt_name in ('-c'):
        compare_pre = opt_value
    if opt_name in ('-n'):
        nodes_pre = opt_value
    if opt_name in ('-p'):
        port = int(opt_value)

nodes = pre_np(nodes_pre, 'nodes')
print('nodes api start at: {}'.format(nodes))

nodes_np = api.namespace(nodes,
                         description='獲取地圖上點座標以及對應得分')

compare = pre_np(compare_pre, 'compare')
print('compare api start at: {}'.format(compare))
compare_np = api.namespace(compare, description='街景對比數據採集')


@nodes_np.route('/',
                doc={'description':
                     category_api_text.api_all_categories_text()})
class AllCategories(Resource):
    @nodes_np.marshal_with(category_models.all_categories_http, mask=None)
    def get(self):
        return category_api.get_category_results(None)


@nodes_np.route('/<string:category_name>',
                doc={'description':
                     category_api_text.api_single_category_text()})
@nodes_np.param('category_name',
                category_api_text.param_category_name_text())
class SingleCategory(Resource):
    @api.marshal_with(category_models.single_category_http, mask=None)
    def get(self, category_name):
        return category_api.get_category_results(category_name)


@nodes_np.route('/<string:category_name>/<int:score_type>',
                doc={'description':
                     category_api_text.api_single_score_type_text()})
@nodes_np.param('category_name',
                category_api_text.param_category_name_text())
@nodes_np.param('score_type',
                category_api_text.param_score_type_text())
class SingleScoreTypeCategory(Resource):
    @nodes_np.marshal_with(category_models.single_category_http, mask=None)
    def get(self, category_name, score_type):
        return category_api.get_category_results(category_name,
                                                 score_type)


@compare_np.route('/<string:source>/<string:category_name>')
@compare_np.param('source', compare_api_text.param_source_text())
@compare_np.param('category_name', compare_api_text.param_category_text())
class CompareApis(Resource):
    @compare_np.doc(description=compare_api_text.api_random_compare_text())
    @compare_np.marshal_with(compare_models.random_compare_http, mask=None)
    def get(self, source, category_name):
        return compare_api.get_random_pair(source, category_name)

    @compare_np.doc(description=compare_api_text.api_compare_result_text())
    @compare_np.marshal_with(compare_models.random_compare_http, mask=None)
    @compare_np.expect(compare_models.compre_result_input)
    def post(self, source, category_name):
        json_data = request.json
        return compare_api.insert_pair(
            source, category_name,
            json_data.get('img1'), json_data.get('img2'),
            json_data.get('result'), json_data.get('user')
        )


def main(argv=None):
    app.run(host='0.0.0.0',
            port=port,)


if __name__ == '__main__':
    sys.exit(main())
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章