Intro
Pythonで実装した機械学習や画像処理をバックエンドにしたWebアプリをサクッと作るための技術スタックとして、FastAPI+TypeScript+OpenAPIを紹介します。
モチベーション
- PythonでサクッとWebサーバ(APIサーバ)を立てたい
- 今まではFlaskを使ってたような用途
- 「Pythonで」
- 機械学習・画像処理のサービスなので
- 「サクッと」
- バリデーションとか楽したい
- サーバ、クライアント共に型の保証が欲しい
- 機械学習や画像処理のアプリはパラメータが多くなりがち・一貫した慣習が無いのでミスしやすい
-
width
orw
- 値の範囲は
[0, w]
or[0, 1]
?
-
- →型アノテーションでカバーしたい
- 機械学習や画像処理のアプリはパラメータが多くなりがち・一貫した慣習が無いのでミスしやすい
- やりたいこと
- API endpoint公開
- メディアファイルアップロード・ダウンロード
- Additional: 非同期通信、WebSocket
検討した選択肢
Webフレームワーク
- (Flask)
- Sanic
- Starlette
- FastAPI
型保証
- gRPC-web
- OpenAPI
先に結論
- FastAPIで簡単に、宣言的にWebAPIが作れる
- Pydanticによるスキーマ定義
- FastAPIでのサーバサイドの定義から、OpenAPIを経由してフロントエンドライブラリを生成できる。
- 本稿では生成先言語としてTypeScriptを使う。サーバサイドと一致した型定義を利用できる。
FastAPI?
- starletteベース
- ASGI
- 国内では2019年初頭にバズった
Demo
上記モチベーションのうち、
- サクッとAPIを立てて
- 型を保証したクライアントが使える
ことのデモ
サンプルRepositoryはこちら: https://github.com/tuttieee/fastapi-typescript-openapi-example
基本的にFastAPIのチュートリアルの通りに進めていくが、↑のために若干改変してある。
- WebAppを作るところまでカバーするため、frontend (TypeScript)もサンプルに含める
- Dockerで開発、デプロイする
Dockerで立ち上げる
https://fastapi.tiangolo.com/deployment/ の通り。
- frontendとのモノレポにするため、1階層掘って
/backend
に置く。 - あとでmigrationなどを足すため、もう1階層掘って
/backend/app/app
とする。/backend/app
がdockerコンテナにマウントされる - docker-composeで管理する
docker-compose build
, docker-compose up
のあと、curl localhost:8000
をやってみる
$ curl localhost:8000
{"Hello":"World"}
開発用設定
-
docker-compose.devel.yml
- Host側ディレクトリをマウント
- auto-reloadを有効化
- docker run のcommandに
/start-reload.sh
を指定するとオートリロードが有効になる(利用している 公式Dockerイメージ のオプション)
- docker run のcommandに
SQLと繋がったAPIを作ってみる
基本的に↓に従って進める。
MySQLを使う部分は独自に追記。
SQLAlchemyインストール
とりあえずDockerfileで直に RUN pip install sqlalchemy
まずSQLite
-
app/app/__init__.py
,app/app/database.py
,app/app/models.py
を作成 - 動くか確かめてみる
- shellに入り、
python -i
- ↓を叩いてみる
- shellに入り、
from app import models
from app.database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
user = models.User(email='test@example.com', hashed_password='')
db = SessionLocal()
db.add(user)
db.commit()
db.close()
db = SessionLocal()
db.query(models.User).all()
MySQL
backendコンテナに関連ソフトウェアをインストール
RUN apt-get update && apt-get install -y \
default-mysql-client \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN pip install sqlalchemy PyMySQL
サンプル:
- https://github.com/tuttieee/fastapi-typescript-openapi-example/commit/feceece4e736153a63386549e4470fec9b7eabac
- https://github.com/tuttieee/fastapi-typescript-openapi-example/commit/13716ae2ba492c49ab158a1ba1cfa7cffc731ad4
MySQLコンテナ追加
-
公式イメージ を利用。基本的に、READMEの通りに
docker-compose.yml
に設定を書く - Credentialsを
.env
に追記、docker-compose.yml
で環境変数として渡す
Alembic
-
Dockerfile
にpip install alembic
追記(いずれrequirements.txt化?) ->docker-compose build
-
database.py
を更新- database URLをMySQL用に更新
-
create_engine
をMySQL用に更新(不要な引数を除去) - naming conventionを追記
Tips: starletteのクラスがそのまま使える
その後shellに入り、以下の作業をする
初期化
-
alembic init migrations
Ref: https://alembic.sqlalchemy.org/en/latest/tutorial.html
設定
Ref: https://alembic.sqlalchemy.org/en/latest/autogenerate.html
-
alembic.ini
編集-
sqlalchemy.url
を削除(database.py
から取るようにするので)
-
-
migrations/env.py
編集- ↓を追記
from app.models import Base
from app.database import SQLALCHEMY_DATABASE_URL
config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL)
target_metadata = Base.metadata
Migration生成
-
models.py
の定義をMySQL用にしておく(String
に長さを指定する) alembic revision --autogenerate -m "Add users and items tables"
Migration実行
alembic upgrade head
試してみる
-
python -i
で以下を実行
from app import models
from app.database import SessionLocal
user = models.User(email='test@example.com', hashed_password='')
db = SessionLocal()
db.add(user)
db.commit()
db.close()
db = SessionLocal()
db.query(models.User).all()
API作成
Pydanticモデル作成
See https://fastapi.tiangolo.com/tutorial/sql-databases/#create-initial-pydantic-models-schemas
app/schemas.py
作成
CRUD utils作成
See https://fastapi.tiangolo.com/tutorial/sql-databases/#crud-utils
app/crud.py
作成
メインAPI作成
See https://fastapi.tiangolo.com/tutorial/sql-databases/#main-fastapi-app
app/main.py
更新
動作確認
cURL例:
curl localhost:8000/users/
curl -X POST -d '{"email": "testtest@example.com", "password": "test"}' localhost:8000/users/
OpenAPIから
http://localhost:8000/docs
MISC
-
prestart.sh
- Wait DB container
- Migration
- サンプル: https://github.com/tuttieee/fastapi-typescript-openapi-example/commit/9a52eb1415a484647da43c6f65002213647cc577
- Makefile
- API endpoint名を簡略化する(関数名に一致させる)
-
backend/app/app/main.py
に追記 - サンプル: https://github.com/tuttieee/fastapi-typescript-openapi-example/commit/36474ed029fbf6a03dc3d815ecfe7462ac3cf1fe
-
from fastapi.routing import APIRoute
...
def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
route.operation_id = route.name
use_route_names_as_operation_ids(app)
Frontend
Create React App
$ yarn create react-app frontend --template typescript
$ cd frontend
$ yarn start
(とりあえず)APIアクセス
- CORS設定
-
backend/app/app/main.py
に以下を追記
-
# TODO: This is for development. Remove it for production.
origins = [
"<http://localhost:3000",>
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
yarn add axios
-
frontend/src/App.tsx
で雑にfetchしてみる
import React, { useState, useEffect } from 'react';
import './App.css';
import { DefaultApi, Configuration, User } from './api-client';
const config = new Configuration({ basePath: 'http://localhost:8000' }); // TODO: This is for dev
export const apiClient = new DefaultApi(config);
const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
apiClient.readUsers().then((response) => {
setUsers(response.data);
})
})
return (
<div className="App">
<ul>
{users.map(user =>
<li key={user.id}>{user.email}</li>
)}
</ul>
</div>
);
}
export default App;
現コードベースの問題点
- 手動で型定義しなければならない
- 自分で
interface User
とか書く
- 自分で
OpenAPIによるクライアント自動生成
Intro
OpenAPI: 旧Swagger
FastAPIがOpenAPIのJSON定義を生成してくれる: http://localhost:8000/openapi.json
このJSONを食って様々な言語のクライアントを生成してくれるOpenAPI Generator
DockerでOpenAPI Generatorを動かす
See https://openapi-generator.tech/docs/installation.html#docker
-
docker-compose.openapi-generator.yml
を作成
version: "3.7"
services:
openapi-generator:
image: openapitools/openapi-generator-cli
volumes:
- ./frontend:/frontend
working_dir: /frontend
command:
- generate
- -g
- typescript-axios
- -i
- <http://backend/openapi.json>
- -o
- /frontend/src/api-client
- --additional-properties=supportsES6=true,modelPropertyNaming=original
# modelPropertyNaming=original is necessary though camelCase is preferred
# See <https://github.com/OpenAPITools/openapi-generator/issues/2976>
-
Makefile
にコマンドを追記
oapi/gen:
docker-compose -f docker-compose.yml -f docker-compose.openapi-generator.yml up openapi-generator \
&& docker-compose -f docker-compose.yml -f docker-compose.openapi-generator.yml rm -f openapi-generator
Frontendを自動生成コードを使うよう書き換える
frontend/src/App.tsx
import React, { useState, useEffect } from 'react';
import './App.css';
import { DefaultApi, Configuration, User } from './api-client';
const config = new Configuration({ basePath: '<http://localhost:8000'> }); // TODO: This is for dev
export const apiClient = new DefaultApi(config);
const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
apiClient.readUsers().then((response) => {
setUsers(response.data);
})
})
return (
<div className="App">
<ul>
{users.map(user =>
<li>{user.email}</li>
)}
</ul>
</div>
);
}
export default App;
その他
このDemoで扱わなかったこと
実際のプロジェクトでは、Unittest, CI, Linterなどは適宜採用
FastAPIのその他の利点、なぜ他の技術と比べてFastAPIを採用したか
- Python
- 画像扱ったり、機械学習的なことをしたり→Pillowやnumpyは欲しい
- 非同期通信(WebSocket)のサポート
- ASGIベース
- Flaskでやろうとするとgeventとかハックが必要
- 普通のHTTP
- メディアファイルのアップロードなどはmultipart/formdataでやりたいし、ダウンロードは直接ファイルを落としたい
- gRPC-webだとこの辺が微妙
- https://github.com/grpc/grpc-web/issues/517
- メディアファイルのアップロードなどはmultipart/formdataでやりたいし、ダウンロードは直接ファイルを落としたい
- オールインワン
- 「サクッと」に通じる
- ドキュメントが充実、production-readyなdockerイメージも公式が提供
- 例えば、FlaskやSanicとかでも、プラグインを入れればOpenAPIを使えるなど