Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第36回「FastAPI を使い始める」

f:id:kanjirz50:20190104142720j:plain

こんにちは。 DSOC R&D グループの高橋寛治です。

私は Python で WebAPI を開発する際には、Flask をよく利用しています。 最近は、FastAPI がいいらしいという話を同僚から聞くようになりました。 少し公式ドキュメントを覗いてみると、Flask をより便利に高機能にしたものに感じました。

これは使わねばと思い、開発環境の構築からテストまでやってみたため、ここで紹介します。 (しっかり理解したい場合は、充実した公式のチュートリアルがおすすめです。ドキュメントがしっかり整備されています。)

FastAPI

FastAPI は、Python の Webフレームワークです。 特徴の詳細については、公式ドキュメントに列挙されています。

以下に私が触った感想を列挙します。

  • 記法が簡単かつ直感的
    • Flask と似ている箇所もあり、扱いやすいです
  • API ドキュメントの自動生成が便利
    • 管理が煩雑になりがちな API ドキュメントをコードから自動生成します
  • FastAPI のドキュメントがしっかりしている
    • かなり詳細に書かれており親切です

一言でまとめると、Flask から FastAPI への移行を決意させる魅力的な機能ばかりです。

開発環境の構築

今回は、次を満たす環境とします。

  • Python パッケージ化する
  • pytest でテストを行う

次のようにディレクトリを構築します。

.
├── requirements.txt
├── setup.py
├── src
│   └── sample
│       ├── __init__.py
│       └── api.py
└── tests
    ├── __init__.py
    └── test_api.py

あとは API とテストを書くだけという状況にするために、 requirements.txt, setup.py を次のように書いて、Python パッケージ化します。

requirements.txt

fastapi[all]==0.63
uvicorn[standard]==0.13
pydantic==1.7.3
pytest==6.2.2

setup.py

from glob import glob
from os.path import basename
from os.path import splitext

from setuptools import setup
from setuptools import find_packages


def _requires_from_file(filename):
    return open(filename).read().splitlines()


setup(
    name="sample",
    version="0.1.0",
    license="",
    description="",
    author="Sansan, Inc.",
    url="",
    packages=find_packages("src"),
    package_dir={"": "src"},
    py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')],
    include_package_data=True,
    zip_safe=False,
    install_requires=_requires_from_file('requirements.txt'),
    setup_requires=["pytest-runner"],
    tests_require=["pytest", "pytest-cov"]
)

そしてプロジェクトのルートで、 pip install -e . で開発用に Python ライブラリとしてインストールして、環境構築は終了です。

エンドポイントを作成する

api.py に簡単なエンドポイントを定義します。

from fastapi import FastAPI

app = FastAPI(
    title="Sample API",
    description="This is a simple sample api.",
    version="1.0.0"
)


@app.get("/")
def read_root():
    return {"message": "Sample API"}

Flask とほとんど同じです。 流れとしては、 FastAPI モジュールをインポートし、 app という変数にインスタンスを代入します*1。 その後、app に対して、ルートへの GET アクセスを read_root メソッドに紐付けます。 紐付けはデコレータによる視覚的にわかりやすい記法となっています。

さて、次のコマンドで動かします。 ここで sample は Python パッケージの src/samplesample です。

$ uvicorn sample.api:app --reload --port 8000

ブラウザもしくは curl で、期待したレスポンスが返ってくるか確認しましょう。

$ curl localhost:8000
{"message":"Sample API"}

意図したメッセージが返ってきました。

テストを行う

test_api.py に次のように記述しテストを行います。

from fastapi.testclient import TestClient

from sample.api import app


client = TestClient(app)


def test_read_root():
    response = client.get("/")

    assert response.status_code == 200
    assert response.json() == {"message": "Sample API"}

流れとしては、テスト用のクライアントを作成し、ルートへの GET アクセスを行い、そのステータスコードやレスポンスの中身を確認します。

テストの実行は、プロジェクトのルートで pytest コマンドを実行し、行います。

API ドキュメントを見る

GET リクエストでパラメータ を送り、その結果を取得するというエンドポイントを定義します。 また、定義したエンドポイントについてAPI ドキュメントが自動生成されていることを確認します。

test_api.py に次のテストを追加します。

def test_get_document():
    response = client.get("/document/1")

    assert response.status_code == 200
    assert response.json() == {
        "id": "1",
        "title": "Sample Title",
        "date": "2021-01-01",
    }

document というリソースの1を取得すると、Document が取得できるというものです。

これを api.py に実際に定義します。なお、 Document の内容をここでは固定値とします。(実際は DB などから取得します)

### api.py に以下を追記
import datetime
from pydantic import BaseModel, Field


class Document(BaseModel):
    id: str = Field(..., title="文書のID", example="1")
    title: str = Field(..., title="文書のタイトル", example="Sample Title")
    date: datetime.date = Field(..., title="日付", example="2021-01-01")


@app.get(
    "/document/{document_id}",
    response_model=Document,
    description="Read a document",
)
def read_document(document_id: str):
    document = Document(
        id=document_id,
        title="Sample Title",
        date=datetime.date(2021, 1, 1)
    )
    return document

pydantic と呼ばれるライブラリで Document を定義します。 それぞれの変数について、値の例や説明、期待する初期値など柔軟に記述できます。 ここで変数に対して記述した内容がAPI ドキュメントに表示されます。

定義した Document とエンドポイントの紐付けは、デコレータで行います。 ここだと、response_model という引数によりエンドポイントの戻り値が Document 型であることを示します。

さて、pytest でテストが通ることを確認してから、API ドキュメントを閲覧します。

ブラウザで、 localhost:8000/docs を閲覧します。 そして、 GET/document/{document_id} となっているところを展開してみましょう。

f:id:kanjirz50:20210131153731p:plain
FastAPIで自動生成されたAPI ドキュメント

このように、先ほど Python コードで定義したエンドポイントとエンドポイントのパラメータが表示されます。 コードと API ドキュメントの管理が、コードから生成されることはいいですね*2

「Try it out」を押すと、この画面から API リクエストを送り、結果を確認することができます。

f:id:kanjirz50:20210131154104p:plain
APIリクエスト例

document_id に 2 を設定し、実行してみました。 Response body も期待通りの値となっています。

おわりに

FastAPI は非常に簡単かつ簡潔に API エンドポイントの定義および実装が行えます。 また、公式ドキュメントが非常に充実しており、理解や導入がしやすいです。

私なりに例を考えてみましたが、テストまで一連で行うという点以外は、公式ドキュメントでも同様の流れで記述されています。

リクエストとレスポンスの型を Python コードとして簡単に定義できる点は、モデル作成者が OpenAPI 記法や使い方を覚えなくて済むため、実務でも運用負荷が減りそうなイメージです*3

簡単・便利・手厚いドキュメントということで、今後利用がますます増えていくであろう FastAPI について、環境構築からテストまでを紹介しました。

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) 研究開発部 研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。キーワード抽出など自然言語処理を生かした研究開発に取り組む。

▼本連載のほかの記事はこちら

buildersbox.corp-sansan.com

*1:wsgi 系から FastAPI のインスタンスを参照することで動作します。

*2:動作中のコードのスキーマが確実にわかるのはありがたいです。

*3:Flask だと OpenAPI で書いて、JSONSchema ライブラリで読み込める形式に変換し、バリデータを準備していました。正直大変です。

© Sansan, Inc.