2018年7月27日金曜日

flasgger で 404 エラーをカスタマイズする方法

概要

flasgger の 404 はデフォルトだと HTML が返ってきます

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.  If you entered the URL manually please check your spelling and try again.</p>

API の場合は JSON のほうが良いという場合がほとんどだと思います
今回は flasgger で 404 ページをカスタマイズする方法を紹介します

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • flasgger 0.9.0

404 時のエラーレスポンスを返却するメソッドを追加

flask の errorhandler という機能を使います

app = Flask(__name__)
@app.errorhandler(404)
def not_found(error):
    return Response(
        json.dumps({'error': 'not found path'}),
        status=404
    )

これを適当な場所に定義すれば OK です
ちなみに必要な import は以下の通り

import json
from flask import Response
from flask import Flask

最後に

flasgger で 404 ページをカスタマイズする方法を紹介しました
flask の機能を使うことで解決することができます

flasgger は flask や marshmallow, apispec などいろいろなサードパティツールを使っているのでそれらを使うことで解決できることは多いと思います

参考サイト

2018年7月26日木曜日

flasgger + marshmallow schemas で validation と validation_function の挙動を確認してみた

概要

flasgger には validation の機能がデフォルトで備わっています
今回は validation 機能を使って挙動を確認してみました

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • flasgger 0.9.0

サンプルアプリ

以下のサンプルアプリを元に挙動を確認します
POST のリクエストを受け取って処理する簡単なアプリです

# coding: utf-8
from flask import Flask, jsonify
from flasgger import Schema, Swagger, SwaggerView, fields


class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    category = fields.Nested(CategorySchema, many=True)
    name = fields.Str(required=True)


class RandomView(SwaggerView):
    parameters = [
        {
            'in': 'body',
            'name': 'Pet',
            'description': 'Register a pet',
            'required': True,
            'schema': PetSchema
        }
    ]
    responses = {
        200: {
            'description': 'Registered',
            'schema': fields.Str()
        }
    }

    def post(self):
        return 'registered'

app = Flask(__name__)
app.add_url_rule(
    '/random',
    view_func=RandomView.as_view('random'),
    methods=['POST']
)
Swagger(app)


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

正常なリクエストは以下の通りです
validation 機能を入れることでこのリクエストがどうなるか確認します

  • curl -v -XPOST -H 'content-type: application/json' -d '{"category":[{"id":1,"name":"rodent"}],"name":"Mickey"}' 'localhost:5000/random'

validation = True にしてみる

validation = True にするには View クラスのフィールドで有効にするだけです

class RandomView(SwaggerView):
    parameters = [
        {
            'in': 'body',
            'name': 'Pet',
            'description': 'Register a pet',
            'required': True,
            'schema': PetSchema
        }
    ]
    responses = {
        200: {
            'description': 'Registered',
            'schema': fields.Str()
        }
    }
    validation = True

この設定でエラーとなる挙動は以下の通りです

content-type ヘッダがセットされていない場合

  • リクエスト

    • curl -v -XPOST -d '{"category":[{"id":1,"name":"rodent"}],"name":"Mickey"}' 'localhost:5000/random'
  • レスポンス

    • No data to validate

required=True がない場合

  • リクエスト
    • curl -v -XPOST -H 'content-type: application/json' -d '{"category":[{"id":1,"name":"rodent"}]}' 'localhost:5000/random'
  • レスポンス
    • 'name' is a required property Failed validating 'required' in schema: で正しいスキーマ情報が表示される

こんな感じで最低限のチェックを行ってくれる感じです

レスポンス情報をカスタマイズする方法

現状はないようです
もしカスタマイズしたい場合は validation_function を指定して自分で validation 処理を実装する必要があるようです

validation_function を使用する方法

がしかし、どうやら Marshmallow Schemas で validation_function を使用する方法はないです

Marshmallow Schemas の場合すべてクラスで定義します
validation_function を使うためには swagger の定義ファイル or 定義の dictionary オブジェクトが必要になります
無理矢理使うことはできなくはないですが、せっかくクラスで定義したものをファイル or dictionary でもう一度定義しなければいけないのはかなり微妙な感じになると思います

試していないですが marshmallow の機能で Schema からサンプルデータを突っ込んで JSON なり dictionary を生成する機能があるので、それを使えばできなくはないと思います
が、それも微妙かなと思います

ではどうするのがいいか

Marshmallow schemas を使っている場合は以下のどちらかで validation するしかなさそうです

  • デフォルトの機能の validate=True を使う
  • SwaggerView 内で定義したメソッド内で独自の validation 機構を作成する、そこで Response を abort する

かなと思います
自分が調べた限りだとこの 2 つのどちらかになりそうです

最後に

Marshmallow Schemas で validation と validation_function 機能を試してみました
結論としてはデフォルトの validation を使わず自力で validation 用のメソッドを実装するのが一番良いかなと思います

情報が少ないので何とも言えませんがコードを直接見たりすれば他の解決方法が見つかるかもしれません (自分は探せませんでした、というか自力 validation を作る方が楽だと判断しました)

参考サイト

2018年7月25日水曜日

flasgger で path を validation する方法

概要

flasgger でパラメータに path を使っている場合に validation する方法を紹介します
flasgger にはおそらくデフォルトでは path をチェックする機能は備わっていないので自分で実装する必要があります

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • flasgger 0.9.0

Marshmallow Schemas を使って実装したベースアプリ

これをベースに validation 機能を付けてみます
path でパラメータを取るのでこれを validate してみます

  • vim app.py
# coding: utf-8
from flask import Flask, jsonify
from flasgger import Schema, Swagger, SwaggerView, fields


class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    category = fields.Nested(CategorySchema, many=True)
    name = fields.Str()


class RandomView(SwaggerView):
    summary = 'A cute furry animal endpoint.'
    description = 'Get a random pet'
    parameters = [
        {
            'name': 'id',
            'in': 'path',
            'required': True,
            'type': 'integer'
        }
    ]
    responses = {
        200: {
            'description': 'A pet to be returned',
            'schema': PetSchema
        }
    }

    def get(self, id):
        pet = {'category': [{'id': id, 'name': 'rodent'}], 'name': 'Mickey'}
        return jsonify(PetSchema().dump(pet).data)


app = Flask(__name__)
app.add_url_rule(
    '/random/<id>',
    view_func=RandomView.as_view('random'),
    methods=['GET']
)

if __name__ == '__main__':
    app.run(debug=True)
  • pipenv run python3 app.py

で起動して

  • curl localhost:5000/random/1

で以下のようなレスポンスが返ってきます

{
  "category": [
    {
      "id": 1, 
      "name": "rodent"
    }
  ], 
  "name": "Mickey"
}

validation 機能を入れる前は文字列でも問題ありません
これが数字以外の場合はエラーになるようにします

validation 機能を追加する

まず import 系を少し追加します
validation 時にエラーのレスポンスを直接返却する必要があるので、それに関するモジュールやクラスを import します

import json
from werkzeug.exceptions import abort
from flask import Response

上記を追加しましょう
そして validation 用のメソッドを追加します

def my_validate(self, id):
    try:
        int(id)
    except ValueError as e:
        print(e)
        abort(
            Response(
                json.dumps({'error': 'id must be set integer type', 'id': id}),
                status=400
            )
        )

path で取得した id 情報を validation 用のメソッドに渡します
そして integer に変換できるか調査して、もし Exception が発生したらエラーを返却します
あとはこのメソッドをコールするだけです

def get(self, id):
    self.my_validate(id)
    pet = {'category': [{'id': id, 'name': 'rodent'}], 'name': 'Mickey'}
    return jsonify(PetSchema().dump(pet).data)

修正後の全体のコードは以下の通りです

# coding: utf-8
import json
from werkzeug.exceptions import abort
from flask import Response
from flask import Flask, jsonify
from flasgger import Schema, Swagger, SwaggerView, fields


class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    category = fields.Nested(CategorySchema, many=True)
    name = fields.Str()


class RandomView(SwaggerView):
    summary = 'A cute furry animal endpoint.'
    description = 'Get a random pet'
    parameters = [
        {
            'name': 'id',
            'in': 'path',
            'required': True,
            'type': 'integer'
        }
    ]
    responses = {
        200: {
            'description': 'A pet to be returned',
            'schema': PetSchema
        }
    }

    def my_validate(self, id):
        try:
            int(id)
        except ValueError as e:
            print(e)
            abort(
                Response(
                    json.dumps({'error': 'id must be set integer type', 'id': id}),
                    status=400
                )
            )

    def get(self, id):
        self.my_validate(id)
        pet = {'category': [{'id': id, 'name': 'rodent'}], 'name': 'Mickey'}
        return jsonify(PetSchema().dump(pet).data)


app = Flask(__name__)
app.add_url_rule(
    '/random/<id>',
    view_func=RandomView.as_view('random'),
    methods=['GET']
)


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

これで再度実行すると id の部分が文字列の場合にはエラーが返ってくるようになります

  • curl -v 'localhost:5000/random/a'
{"error": "id must be set integer type", "id": "a"}

validation = True と validation_function = None の機能について

実は flasgger には validation の機構が備わっています
validationvalidation_function という機能があります
validation はデフォルトで用意された validator で有効/無効の値しか設定できません
True にした場合に有効になります
やってくれることは例えば

  • HTTP メソッドチェック (405 チェック)
  • ContType のチェック、application/json かどうかのチェック
  • ボディが空でないかのチェック

などです
今回のようにフォーマットチェックなどしたい場合には正直使えません
True にしても余計なことをチェックするケースが多いかなと思います

また validation_function に関してですがこれは基本的に POST 時のボディを validate するときに使うっぽいです (?)
今回のように path のフォーマットチェックをする場合は素直に validation 用のメソッドを作ってしまうほうが早いです

最後に

flasgger の Marshmallow Schemas で path パラメータをチェックするための独自の validation 機能を実装してみました
おそらくこの方法が一番てっとり早いと思います
途中で軽く触れた validationvalidation_function の機能に関しても検証してみたいと思います

この辺りの情報はググっても出てこないので Github の issue やコードを見るしか方法がなさそうです

参考サイト

2018年7月24日火曜日

Python3 で SQLAlchemy 入門

概要

SQLArchemy は Python で使える ORM です
簡単な CRUD 操作からマイグレーションまで幅広い機能を提供しています
今回は簡単なテーブルの作成から CRUD 操作まで行ってみました

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • SQLAlchemy 1.2.10
  • MySQL Server 5.7.22

インストール

  • pipenv install SQLAlchemy mysqlclient

今回は MySQL にアクセスするので mysqlclient も合わせてインストールします

Tips

おそらく ModuleNotFoundError: No module named 'MySQLdb' のエラーが出ると思います
そして pipenv install MySQL-Python をインストールしようとしたのですがどうやらまだ Python3 に対応していないようです
なので PyMySQL をインストールしようとしたのですが状況は変わらずで最終的に mysqlclient をインストールすることで解決しました

事前準備

  • mysql -u root -p -e "create database test;"

test データベースを作成しておきましょう

とりあえず接続してみる

  • vim test1.py
from sqlalchemy import create_engine

url = 'mysql+mysqldb://user:password@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
  • pipenv run python3 test1.py

こんな感じです
ユーザ、パスワードの部分は適当に変更してください
test データベースに接続しています

Base クラスを作成して users テーブルの作成準備をする

test1.py にいろいろ追記していきます

  • vim test2.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)
  • pipenv run python3 test2.py

まず User クラスは Base クラスを継承します
そして __tablename__ 変数でテーブル名を指定します
あとは Column を使ってテーブルに定義するカラムを定義します
__repr__ は必須ではないですが実装しておくとレスポンスをキレイに見せることができます

テーブルを作成する

先ほどのスキーマ定義から実際にテーブルを作成するところまで追加します
と言っても最後の 1 行を追加しているだけです (Base.metadata.create_all(engine))

  • vim test3.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)

Base.metadata.create_all(engine)
  • pipenv run python3 test3.py

これで test データベースは以下に users というテーブルが作成されています

  • mysql -u user -p password test -e "desc users;"
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| name     | varchar(50)  | YES  |     | NULL    |                |
| fullname | varchar(100) | YES  |     | NULL    |                |
| password | varchar(100) | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+

sqlalchemy.exc.CompileError: (in table 'users', column 'name'): VARCHAR requires a length on dialect mysql
というエラーになる場合はカラムを定義している String の部分で引数に文字数を指定しているか確認してください
VARCHAR はちゃんと文字数制限を入れないとエラーとなります

データを登録する

engine を元に Session を作成することでテーブルにアクセスすることができます

  • vim test4.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)

Base.metadata.create_all(engine)

u1= User(name='hawk', fullname='hawksnowlog', password='xxxxxxx')
Session = sessionmaker(bind=engine)
session = Session()
session.add(u1)
session.commit()

最後の session.commit() するまでデータは挿入されません
なのでユーザを複数人登録したり更新処理なども行ってから commit することができます
いわゆるトランザクション処理になります
ちなみに commit する前の状態にロールバックしたい場合は session.rollback() を呼び出せば OK です

データを取得する

  • vim test5.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)

Base.metadata.create_all(engine)

u1= User(name='hawk', fullname='hawksnowlog', password='xxxxxxx')
Session = sessionmaker(bind=engine)
session = Session()
for i in session.query(User).order_by(User.id):
    print(i.name)

登録する部分を削って取得する部分を足します
いわゆる SELECT 文は session.query() を使います
とりあえず order_by を使っていますがデータは少ないので何でも OK です

SELECT した結果 User クラスの配列を返したい場合は session.query(User).order_by(User.id).all() を呼び出せば OK です

データを削除する

  • vim test6.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()
u1 = session.query(User).get(1)
session.delete(u1)
session.commit()

一旦 SELECT してからそのオブジェクトを削除します
あとは commit すれば OK です

データを更新する

削除とほぼ同じです
SELECT してそのオブジェクトの attribute に対してアクセスするだけです

  • vim test7.py
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

url = 'mysql+mysqldb://root@localhost/test?charset=utf8'
engine = create_engine(url, echo=True)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    fullname = Column(String(100))
    password = Column(String(100))

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()
u1 = session.query(User).get(2)
u1.password = 'XXXXXXXX'
session.commit()

最後に commit するのを忘れずに

最後に

Python3 で SQLAlchemy に入門してみました
慣れれば簡単に使えると思います

今回紹介した内容はかなり基本的なことだけです
もっと複雑なクエリの発行や外部キーの制約なども行えます

いろいろなサイトに書いてありましたが一番役に立つのは参考サイトにある公式サイトのチュートリアルなので、これをベースに勉強すると良いと思います
この記事もそのチュートリアルを元に作成しています

参考サイト

2018年7月23日月曜日

ローカルの swagger.yml をコマンド一発で swagger UI で確認する方法

概要

dockerhub で配布されている公式の swagger-ui イメージを使います

環境

  • macOS 10.13.6
  • swagger-ui image 3.17.4

コマンド

  • docker run -p 8080:8080 -v $(pwd)/swagger.yml:/usr/share/nginx/html/swagger.yml -e "API_URL=swagger.yml" swaggerapi/swagger-ui

これで localhost:8080 にアクセスすると確認できます

2018年7月22日日曜日

flasgger Marshmallow Schemas テクニック集

概要

flasgger の Marshmallow Schemas のテクニックを紹介します

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • flasgger 0.9.0

文字列のデフォルト値を設定する方法

hoge = fields.Str(missing='TCP')

文字列を enum として定義する方法

hoge = fields.Str(enum=['A', 'B', 'C'])

その他 fields に関するテクニック

  • 最小値
hoge = fields.Int(min=1)
  • Boolean
hoge = fields.Bool()
  • readOnly
hoge = fields.Str(dump_only=True)

メタ情報を設定する方法

タイトルなどのトップレベルでのメタ情報を定義する方法です

app = Flask(__name__)
meta_info = {
    'info': {
        'description': 'description',
        'title': 'title',
        'contact': {
            'name': 'https://hawksnowlog.blogspot.com'
        },
        'license': {
            'name': 'hawksnowlog'
        },
        'version': '1.0.0',
        'uiversion': 3,
        'termsOfService': '/there_is_no_tos'
    },
    'tags': [
        {
            'name': 'tag1',
            'description': 'tag1 description'
        },
    ],
    'schemes': ['https'],
    'basePath': '/v1/api',
    'host': 'localhost'
}
Swagger(app, template=meta_info)

スキーマクラスに直接 array タイプを定義したい

P.S 20180730 以下のように SwaggerView 側で array を定義することで可能だということがわかりました

class MyView(SwaggerView):
    parameters = [
        {
            'in': 'body',
            'name': 'HogeSpec parameter',
            'description': 'Test array parameter',
            'required': True,
            'schema': {
                'type': 'array',
                'items': {
                    '$ref': '#/definitions/HogeSchema'
                }
            }
        }
    ]

調査中、以下のようなことがやりたいができない

class TestSchema(Schema):
    fields.List(fields.Nested(HogeSchema))

[
  {
    'key': 'value'
  }
]

という構造を作成したいが以下のようにしないと動作しない

class TestSchema(Schema):
    fuga = fields.List(fields.Nested(HogeSchema))
{
  'fuga': [
    {
      'key': 'value'
    }
  ]
}

SwaggerView を使って API のパラメータとレスポンスを定義する

class MyView(SwaggerView):
    tags = ['tag1']
    summary = 'summary'
    description = 'description'
    operationId = 'operationId'
    consumes = ['application/json']
    produces = ['application/json']
    parameters = [
        {
            'in': 'path',
            'name': 'id',
            'required': True,
            'type': 'string',
        }
    ]
    responses = {
        200: {
            'description': 'OK',
            'schema': TestSchema
        }
    }

定義した View は add_url_rule で追加することで API として動作させます

app = Flask(__name__)
app.add_url_rule(
    '/my',
    view_func=MyView.as_view('my'),
    methods=['GET']
)

リクエストボディを取得する方法

from flask import request

request.json.get('Hoge')

スキーマを定義したのに definitions に出てこない

どうやら SwaggerView 側の schema で参照しないと JSON には登場しません
なので

'schema': {
  'type': 'array',
  'items': {
    '$ref': '#/definitions/HogeSchema'
  }
}

こんな感じでスキーマ参照しているとスキーマがないと言われてエラーとなります
そんな場合には definitions というパラメータで明示的に指定してあげることで JSON に登場させることができます

definitions = {
    'HogeSchema': HogeSchema
}

参考: https://github.com/rochacbruno/flasgger/issues/108

2018年7月21日土曜日

flasgger で APISpec を使ってコードから swagger.yml を生成する方法

概要

flasgger は内部的に apispec を使っています
apispec は OpenAPI-Specification ベースの定義ファイルをコードから生成することができるモジュールです
今回は flasgger 上のコードから apispec の機能を使って swagger の定義ファイルを生成してみました

環境

  • macOS X 10.13.6
  • Python 3.6.5
  • flasgger 0.9.0
  • apispec 0.38.0

インストール

  • pipenv install flasgger marshmallow apispec=="0.38.0"

サンプルコード

  • vim test_apispec.py
# coding: utf-8
from flask import Flask, jsonify

from flasgger import APISpec, Schema, Swagger, fields

# Create an APISpec
spec = APISpec(
    title='Flasger Petstore',
    version='1.0.10',
    plugins=[
        'apispec.ext.flask',
        'apispec.ext.marshmallow',
    ],
)

app = Flask(__name__)


class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    category = fields.Nested(CategorySchema, many=True)
    name = fields.Str()


@app.route('/random')
def random_pet():
    """A cute furry animal endpoint.
    ---
    get:
        description: Get a random pet
        responses:
            200:
                description: A pet to be returned
                schema:
                    $ref: '#/definitions/Pet'
    """
    pet = {'category': [{'id': 1, 'name': 'rodent'}], 'name': 'Mickey'}
    return jsonify(PetSchema().dump(pet).data)

template = spec.to_flasgger(
    app,
    definitions=[CategorySchema, PetSchema],
    paths=[random_pet]
)
swag = Swagger(app, template=template)

print(spec.to_dict())
print(spec.to_yaml())

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

最後の print 文 2 つで dictionary 形式と YAML 形式の定義ファイルを出力しています

説明

最初に APISpec を使ってメタデータなどを定義します
plugings は定義ファイルには表示されませんが必須です

spec = APISpec(
    title='Flasger Petstore',
    version='1.0.10',
    plugins=[
        'apispec.ext.flask',
        'apispec.ext.marshmallow',
    ],
)

その後に definitions 用のクラスの定義と paths に応じた処理が書いてあります
そしてその次に spec に対して定義した definitions と paths を登録します

template = spec.to_flasgger(
    app,
    definitions=[CategorySchema, PetSchema],
    paths=[random_pet]
)

更にその後で swag = Swagger(app, template=template) という設定がありますがこれは /apidocs を表示するための設定なのでなくても問題ないです

動作確認

  • pipenv run python3 test_apispec.py

  • curl http://localhost:5000/random

という感じでリクエストすると動作します
また、サーバを動作させたターミナル上で dictionary と YAML の定義情報が表示されているのがわかると思います

definitions:
  Category:
    properties:
      name: {type: string}
      id: {format: int32, type: integer}
    required: [name]
    type: object
  Pet:
    properties:
      name: {type: string}
      category:
        items: {$ref: '#/definitions/Category'}
        type: array
    type: object
info: {title: Flasger Petstore, version: 1.0.10}
parameters: {}
paths:
  /random:
    get:
      description: Get a random pet
      responses:
        200:
          description: A pet to be returned
          schema: {$ref: '#/definitions/Pet'}
swagger: '2.0'
tags: []

今回の定義だと上記のような感じで表示されると思います

最後に

flasgger の APISpec の機能を使ってコードから swagger ファイルを出力してみました
おそらくこれがあるのは swagger ファイルからモデルを生成するのではなくコードから swagger ファイルを生成したいという需要があるからだと思います
普通に考えれば swagger-codegen などを使ってコードを生成しますが、それだと内部がブラックボックスなのと結局コードと swagger ファイルをメンテンスしなければならないので、それであればコードだけを書き続けて swagger ファイルを自動生成するほうが良いということなんだと思います

参考サイト