はじめに
この記事は Muroran Institute of Technology Advent Calendar 2017 25日目の記事です。
Flaskを使ってREST APIを作りたいときに、同時にドキュメントとしてSwaggerも整備したいこと、あると思います。しかしドキュメントも書きつつAPIも書くとどっちかを更新したときにどっちかを更新し忘れるなどして、だんだん面倒になってきてしまいがちです。
そこで今回は簡単なToDo管理APIを例に、Flask-RESTPlus
を使ってAPI定義とSwaggerによるドキュメント生成を一気に行ってしまう方法をご紹介します。
Python環境の準備
とりあえずPython環境を準備しておきましょう。PyenvとVirtualenvを使ってさくさくっと作っていきます。仮想環境名はrestplus
にしてありますが別に何でもかまいません。今回は最新版であるPython3.6.4を用います。
$ mkdir restplus
$ cd restplus
$ pyenv virtualenv 3.6.4 restplus
$ pyenv local restplus
ファイルの準備
ではファイルやディレクトリを準備していきます。以下のような構造で準備してください。それぞれのファイルが何に使われるかは後述します。
restplus/
├── rest_api/
│ ├── apis/
│ │ └── __init__.py
│ ├── models/
│ │ └── __init__.py
│ ├── __init__.py
│ ├── app.py
│ └── settings.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
Pythonライブラリの導入
requirements.txtの編集
requirements.txtには今回利用するPythonライブラリを列挙していきます。以下のように書いておきましょう。Flaskを使うのでORMにはSQL Alchemyを採用しました。また、データベースはMySQLを利用することにします。それに伴ってflask-sqlalchemy
とpymysql
をインストールしておきます。
flask
flask-restplus
flask-sqlalchemy
pymysql
ライブラリのインストール
編集できたら以下のコマンドでインストールしてしまいます。ぶっちゃけるとDockerで動かすので入れなくても良いといえばいいのですが、IDEなどで補完を効かせるために入れておきましょう。
$ pip install -r requirements.txt
コア部分の作成
ここまでできたらいよいよコーディングに入ります。Flaskを使ったWebアプリをいつも通りな感じで構築していきます。
APIエンドポイントの準備
apis/__init__.py
にAPIエンドポイントを登録する準備をします。Flask RESTPlusで用意されたAPI
オブジェクトを初期化しておきましょう。
from flask_restplus import Api
# API情報を指定して初期化
api = Api(
title='Test API',
version='1.0',
description='Swaggerを統合したREST APIのサンプル'
)
ORMの準備
models/__init__.py
ではSQL Alchemyの初期化を行います。このファイルにはDBのスキーマを定義していきます。
from flask_sqlalchemy import SQLAlchemy
# SQLAlchemyの初期化
db = SQLAlchemy()
アプリの諸設定
settings.py
にはアプリの設定を記述していきます。特にSQL接続情報などの記載は必須です。
from os import environ
# デバッグモードを有効化
DEBUG = True
# Swaggerのデフォルト表示形式をListにする
SWAGGER_UI_DOC_EXPANSION = 'list'
# Validationの有効化
RESTPLUS_VALIDATE = True
# SQL接続情報
# コンテナ側に環境変数として渡すためこの形式で受け取る
SQLALCHEMY_DATABASE_URI = environ['MYSQL_URL']
SQLALCHEMY_TRACK_MODIFICATIONS = True
Flaskアプリの定義
ここまで準備できたらFlaskアプリを定義していきます。app
変数にFlaskオブジェクトを入れて、それを引数で取り回しながらAPIエンドポイントやSQL Alchemyとの連携を行います。最後にrun
メソッドを実行すればOKです。このとき、Dockerの中で実行されるためhost
の指定を忘れないようにしましょう。
from flask import Flask
from rest_api import settings
from rest_api.apis import api
from rest_api.models import db
# Flask本体
app = Flask(__name__)
def configure_app(flask_app: Flask) -> None:
# DB接続先情報やSwaggerの表示形式を指定
flask_app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI
flask_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = settings.SQLALCHEMY_TRACK_MODIFICATIONS
flask_app.config['SWAGGER_UI_DOC_EXPANSION'] = settings.SWAGGER_UI_DOC_EXPANSION
flask_app.config['RESTPLUS_VALIDATE'] = settings.RESTPLUS_VALIDATE
def initialize_app(flask_app: Flask) -> None:
# FlaskへAPIやDB情報を登録
configure_app(flask_app)
api.init_app(flask_app)
db.init_app(flask_app)
db.create_all(app=flask_app)
def main() -> None:
# Flaskを初期化して実行
initialize_app(app)
app.run(host='0.0.0.0', debug=settings.DEBUG)
if __name__ == '__main__':
main()
Dockerの準備
Dockerfile
アプリをDockerに載せるためにDockerfileを書きます。MySQLの起動を待つためにDockerizeも一緒に仕込んでおきます。
FROM python:alpine3.6
WORKDIR /usr/src/app
ENV DOCKERIZE_VERSION v0.6.0
COPY requirements.txt ./
RUN pip install -U --no-cache-dir -r requirements.txt \
&& apk add --no-cache openssl \
&& wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz
COPY . .
CMD [ "python", "rest_api/app.py" ]
docker-compose.yml
DockerfileだけではMySQLとの統合が難しいのでdocker-compose.ymlも書いておきましょう。アプリ内部でのimportを使うため、該当ディレクトリをPYTHONPATH
環境変数に渡してあげるのを忘れないようにしてください。また、MySQLの接続情報も渡してあげましょう。
version: '3'
services:
restapi:
build: .
ports:
- "5000:5000"
environment:
MYSQL_URL: 'mysql+pymysql://test:user_pass@mysql/test'
PYTHONPATH: '/usr/src/app'
links:
- mysql
entrypoint:
- dockerize
- -timeout
- 60s
- -wait
- tcp://mysql:3306
command: python /usr/src/app/rest_api/app.py
mysql:
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: 'root_pass'
MYSQL_DATABASE: 'test'
MYSQL_USER: 'test'
MYSQL_PASSWORD: 'user_pass'
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
起動テスト
ここまでできたら起動のテストを行います。docker-composeでさくさくっとやってしまいましょう。
$ docker-compose build
$ docker-compose up -d
起動し終わったら、http://localhost:5000/ にアクセスすると以下のような画面が表示されます。ここにだんだんとAPIの仕様書ができあがっていきます。
DBスキーマの定義
続いてデータベースのスキーマを定義していきます。models/__init__.py
につらつらと書いていきます。ToDoに使うスキーマを簡単に定義します。models/__init__.py
に以下の記述を追記します。db
変数に格納したSQLAlchemyオブジェクトを流用して、カラムやその型を定義します。このとき、primary_key
に設定したInteger
のカラムはオートインクリメントが適用されます。
from flask_sqlalchemy import SQLAlchemy
# SQLAlchemyの初期化
db = SQLAlchemy()
# Userスキーマの定義
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, nullable=False, primary_key=True)
name = db.Column(db.Text, nullable=False)
email = db.Column(db.Text, nullable=False)
# ToDoスキーマの定義
class ToDo(db.Model):
__tablename__ = 'todo'
id = db.Column(db.Integer, nullable=False, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
title = db.Column(db.Text, nullable=False)
description = db.Column(db.Text, nullable=False)
APIエンドポイントの定義
定義ファイルの作成
データベースの準備ができたらAPIエンドポイントを設計しましょう。apis/
ディレクトリにtodo.py
とuser.py
を新規作成し、そこに定義を書いていきます。
$ touch apis/todo.py apis/user.py
NamespaceとJSONモデルの定義
異なるAPIエンドポイントを区別するためにNamespaceを作成します。さらに、返したいJSONのモデルも定義しましょう。使ってもいないのにResource
をimportしていますが、この後すぐ使うのでここでimportしてしまいます。
Namespaceオブジェクトを生成する際の第1引数にはエンドポイントのプレフィックスを指定します。例えばtodo.pyではtodo
と指定していますが、/todo
以下でこれから定義するエンドポイントが呼べるようになります。作成したNamespaceは忘れずにAPIに登録してください。
JSONモデルの定義はDBのカラム名と同様の文字列をキーにした辞書で行います。各要素について必須かどうかや詳しい説明、例となる値を指定することができます。これらの情報は全てSwaggerドキュメントに反映されます。
from flask_restplus import Namespace, fields, Resource
# Namespaceの初期化
todo_namespace = Namespace('todo', description='ToDoのエンドポイント')
# JSONモデルの定義
todo = todo_namespace.model('ToDo', {
'user_id': fields.Integer(
required=True,
description='ToDoを登録したユーザーID',
example='0'
),
'id': fields.Integer(
required=True,
description='ToDoのID',
example='1'
),
'title': fields.String(
required=True,
description='ToDoタイトル',
example='起きる'
),
'description': fields.String(
required=True,
description='ToDoの詳細',
example='朝7時に起きたい'
)
})
from flask_restplus import Namespace, fields, Resource
# Namespaceの初期化
user_namespace = Namespace('users', description='ユーザー関連のエンドポイント')
# JSONモデルの定義
user = user_namespace.model('User', {
'id': fields.Integer(
required=True,
description='',
example='0'
),
'name': fields.String(
required=True,
description='',
example='Aruneko'
),
'email': fields.String(
required=True,
description='',
example='aruneko@example.com'
)
})
エンドポイントの追加
NemespaceとJSONモデルが定義できたらいよいよエンドポイントを追加します。やり方は簡単で、Resource
クラスを継承したクラスを作ってHTTPメソッドと同名のメソッドを作成し、ルーティングに必要なアノテーションを付加するだけです。これらを踏まえて、apis以下のファイルの最終形は以下のようになります。
各メソッドの先頭にダブルクォートを6つ使った複数行コメントを差し込むことで、API説明をSwaggerに反映させることができます。1行目が簡易的な説明で、2行目以降はMarkdown記法を使った詳細な説明を記入できます。
marshal_with
やmarshal_list_with
では返ってくるJSONの形式を指定できます。前者は単体を、後者はリスト化したものを返します。先ほど定義したJSONモデルを引数にとってあげましょう。expect
はその逆で、送って欲しいJSONのモデルを指定します。
from flask_restplus import Namespace, fields, Resource
from rest_api.apis import api
from rest_api.models import ToDo, db
# Namespaceの初期化と登録
todo_namespace = Namespace('todo', description='ToDoのエンドポイント')
# JSONモデルの定義
todo = todo_namespace.model('ToDo', {
'user_id': fields.Integer(
required=True,
description='ToDoを登録したユーザーID',
example='0'
),
'id': fields.Integer(
required=True,
description='ToDoのID',
example='1'
),
'title': fields.String(
required=True,
description='ToDoタイトル',
example='起きる'
),
'description': fields.String(
required=True,
description='ToDoの詳細',
example='朝7時に起きたい'
)
})
@todo_namespace.route('/')
class ToDoList(Resource):
# todoモデルを利用して結果をパースしてリストで返す
@todo_namespace.marshal_list_with(todo)
def get(self):
"""
一覧取得
"""
return ToDo.query.all()
@todo_namespace.marshal_with(todo)
@todo_namespace.expect(todo, validate=True)
def post(self):
"""
ToDo登録
"""
# ちょっとやっかいなので実装はまた今度
pass
@todo_namespace.route('/<int:todo_id>')
class ToDoController(Resource):
# todoモデルを利用して結果をパースして単体で返す
@todo_namespace.marshal_with(todo)
def get(self, todo_id):
"""
ToDo詳細
"""
# ただし1個も見つからなかったら404を返す
return ToDo.query.filter(ToDo.id == todo_id).first_or_404()
def delete(self, todo_id):
"""
ToDo削除
"""
# 見つからなかったときの処理してないけど許して
target_todo = ToDo.query.filter(ToDo.id == todo_id).first()
db.session.delete(target_todo)
return {'message': 'Success'}, 200
from flask_restplus import Namespace, fields, Resource
from rest_api.apis import api
# Namespaceの初期化と登録
from rest_api.apis.todo import todo
from rest_api.models import User, db, ToDo
user_namespace = Namespace('users', description='ユーザー関連のエンドポイント')
# JSONモデルの定義
user = user_namespace.model('User', {
'id': fields.Integer(
required=True,
description='',
example='0'
),
'name': fields.String(
required=True,
description='',
example='Aruneko'
),
'email': fields.String(
required=True,
description='',
example='aruneko@example.com'
)
})
@user_namespace.route('/')
class UserList(Resource):
@user_namespace.marshal_list_with(user)
def get(self):
"""
ユーザー一覧
"""
return User.query.all()
@user_namespace.marshal_with(user)
@user_namespace.expect(user)
def post(self):
"""
ユーザー登録
"""
# ちょっとやっかいなので実装はまた今度
pass
@user_namespace.route('/<int:user_id>')
class UserController(Resource):
@user_namespace.marshal_with(user)
def get(self, user_id):
"""
ユーザー詳細
"""
return User.query.filter(User.id == user_id).first_or_404()
def delete(self, user_id):
"""
ユーザー削除
"""
target_user = User.query.filter(User.id == user_id).first()
db.session.delete(target_user)
return {'message': 'Success'}, 200
@user_namespace.route('/<int:user_id>/todo')
class ToDoByUser(Resource):
@user_namespace.marshal_list_with(todo)
def get(self, user_id):
"""
ユーザーごとのToDo取得
"""
return ToDo.query.filter(ToDo.user_id == user_id).all()
エンドポイントの登録
エンドポイントを作成し終わったら登録を行います。apis/__init__.py
に記入しましょう。add_namespace
メソッドを利用してください。
from flask_restplus import Api
from rest_api.apis.todo import todo_namespace
from rest_api.apis.user import user_namespace
# API情報を指定して初期化
api = Api(
title='Test API',
version='1.0',
description='Swaggerを統合したREST APIのサンプル'
)
api.add_namespace(todo_namespace)
api.add_namespace(user_namespace)
テスト起動
再びdocker-composeを使ってテスト起動してみます。
$ docker-compose build
$ docker-compose up -d
http://localhost:5000/ を確認してください。以下のように、APIドキュメントが表示されるはずです。
各項目をクリックすると、実際にAPIを試すことができるようになっています。また、先ほど指定したJSONモデルもしっかりと表示されていることがわかります。
さらに、curlなどでアクセスすればきちんとレスポンスが返ってくることも確認できます。今回はデータを入れていないので空のJSONだけが返ってきますけれども。
$ curl http://localhost:5000/todo/
[]
おわりに
ここまでFlask-RESTPlusを使ってSwaggerを統合したRESTfulなAPIを作成してきました。モデルの定義とドキュメント作成が同時に行えるので非常に便利です。
今回は紹介しませんでしたが、このほかにもリクエストのパースやエラーハンドリングなど、多彩な機能を取りそろえています。詳しいことは公式ドキュメントも参考にしてみてください。
最後になりましたが、 Muroran Institute of Technology Advent Calendar 2017 にご参加いただいた皆さん、ありがとうございました!