CLOVER🍀

That was when it all began.

PythonのWeb APIフレームワーク、FastAPIを試す

これは、なにをしたくて書いたもの?

とあるPythonを扱うチュートリアルを見ているのですが、サンプルとしてFastAPIが使われるようなので先に独立して触っておこうかなと
思いまして。

なかなか人気もありそうですし。

FastAPI

FastAPIのWebサイトはこちら。

FastAPI

GitHubリポジトリーはこちら。

GitHub - tiangolo/fastapi: FastAPI framework, high performance, easy to learn, fast to code, ready for production

登場は2018年と新しいフレームワークのようですが、かなり人気のようですね。調べてみたところ、GitHubリポジトリーのStarの伸びが
凄まじいです。

Webサイトのトップページによると、以下が特徴なようです。

  • 高速 … Node.jsやGoと同等のパフォーマンス(StarletteとPydanticの恩恵が大きい)で、最も高速なPythonフレームワークのひとつ
  • 高速なコーディングとバグの削減
  • 優れたエディターのサポート
  • 使いやすく、学びやすい
  • 少ないコードでの実装が可能
  • プロダクションレディなコードをドキュメントからインタラクティブに、自動で得ることができる
  • OpenAPIの基づいている

これらのより詳しい説明はこちらですね。

Features - FastAPI

チュートリアルやドキュメント、デプロイ向けのガイドなどはこちら。

Learn - FastAPI

APIリファレンスはこちら。

Reference - Code API - FastAPI

Python 3.8以上であれば動作するようです。

FastAPI / Requirements

ひとまず、Exampleを試してみましょう。

FastAPI / Example

環境

今回の環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

FastAPIのExampleを試す

まずはインストールから。

FastAPI / Installation

FastAPI自身と

$ pip3 install fastapi

ASGIサーバーというものをインストールするようです。ASGIサーバーとしてはUvicornとHypercornが挙げられています。

Uvicorn

GitHub - pgjones/hypercorn: Hypercorn is an ASGI and WSGI Server based on Hyper libraries and inspired by Gunicorn.

今回はドキュメントに従って、Uvicornをインストールしておきます。

$ pip3 install uvicorn[standard]

インストールされたモジュールはこちら。

$ pip3 list
Package           Version
----------------- ------------
annotated-types   0.6.0
anyio             4.2.0
click             8.1.7
exceptiongroup    1.2.0
fastapi           0.109.2
h11               0.14.0
httptools         0.6.1
idna              3.6
pip               22.0.2
pydantic          2.6.0
pydantic_core     2.16.1
python-dotenv     1.0.1
PyYAML            6.0.1
setuptools        59.6.0
sniffio           1.3.0
starlette         0.36.3
typing_extensions 4.9.0
uvicorn           0.27.0.post1
uvloop            0.19.0
watchfiles        0.21.0
websockets        12.0

では、Exampleに従ってソースコードを作成してみます。

FastAPI / Example

main.py

from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

起動してみます。

$ uvicorn main:app --reload

ポート8000、変更を監視してリロードするモードで起動したようです。

INFO:     Will watch for changes in these directories: ['/path/to']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [12543] using WatchFiles
INFO:     Started server process [12545]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

アクセスして確認してみます。

$ curl localhost:8000
{"Hello":"World"}


$ curl localhost:8000/items/5
{"item_id":5,"q":null}


$ curl localhost:8000/items/5?q=test
{"item_id":5,"q":"test"}


$ curl localhost:8000/items/5?r=test
{"item_id":5,"q":null}

/へのアクセスはいいとして、/items/{item_id}に割り当てている関数定義を確認してみます。

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

{item_id}がパスパラメーター、q: Union[str, None]がQueryStringに対応しているみたいですね。

この状態でhttp://localhost:8000/docsにアクセスしてみます。

すでにOpenAPI定義があります、と…。

ここでアクセスするURLをhttp://localhost:8000/redocにすると、RedocでのOpenAPI定義を確認できます。

便利ですね。

次は、ドキュメントに習ってソースコードを修正してみます。

main.py

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    is_offer: Union[bool, None] = None

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

PUTメソッドを追加しました。

ソースコードを修正して保存すると、Uvicornを--reloadオプション付きで起動しているので、リロードが行われます。
その時のログはこんな感じです。

WARNING:  WatchFiles detected changes in 'main.py'. Reloading...
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [12862]
INFO:     Started server process [12865]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

確認。

$ curl -XPUT -H 'Content-Type: application/json' localhost:8000/items/3 -d '{"name": "sample", "price": 500}'
{"item_name":"sample","item_id":3}

OKそうですね。

確認結果は省略しますが、OpenAPI定義やRedocの方にも反映されます。

なにを書いたのか?

ここまでの内容の要約が、こちらに書かれています。

FastAPI / Recap

まず、Python 3.8以降で使える以下のような型宣言がポイントになっています。

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):

これでエディターのサポート、データのバリデーション(JSON検証)、入力データからPythonオブジェクトへの変換、
Pythonオブジェクトから出力データへの変換、OpenAPI定義(Redoc含む)という一連の結果が得られます。

エンドポイントを見てみます。

こちらはHTTP GETメソッドに対応していて、{item_id}はパスパラメーターです。

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):

qはqという名前のQueryStringパラメーターに対応していて、= Noneがあるためオプション扱いです。

HTTP PUTメソッドに対応しているこちらは、HTTPボディをItemクラスのインスタンスとして読み込みます。メディアタイプは
application/jsonになっているようです。

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):

HTTPボディで扱うクラスは以下の定義ですが、nameおよびpriceが必須項目、is_offerがオプションという扱いです。

class Item(BaseModel):
    name: str
    price: float
    is_offer: Union[bool, None] = None

この後はチュートリアルに進むのがよいみたいです。

Tutorial - User Guide - FastAPI

目次を見ると、だいたいこれを眺めていけばやりたいことはできそうな感じですね。

テストを書く

せっかくなので、テストまで書いてみましょう。

テストはチュートリアルのこちらを見ればよさそうです。

Testing - FastAPI

テスティングフレームワークとしてはpytest、HTTPリクエストにはhttpxを使います。

インストール。

$ pip3 install pytest httpx

結果はこうなりました。

$ pip3 list
Package           Version
----------------- ------------
annotated-types   0.6.0
anyio             4.2.0
certifi           2024.2.2
click             8.1.7
exceptiongroup    1.2.0
fastapi           0.109.2
h11               0.14.0
httpcore          1.0.2
httptools         0.6.1
httpx             0.26.0
idna              3.6
iniconfig         2.0.0
packaging         23.2
pip               22.0.2
pluggy            1.4.0
pydantic          2.6.0
pydantic_core     2.16.1
pytest            8.0.0
python-dotenv     1.0.1
PyYAML            6.0.1
setuptools        59.6.0
sniffio           1.3.0
starlette         0.36.3
tomli             2.0.1
typing_extensions 4.9.0
uvicorn           0.27.0.post1
uvloop            0.19.0
watchfiles        0.21.0
websockets        12.0

__init__.pyの作成。

$ touch __init__.py

作成したテストコードはこちら。

test_main.py

from fastapi.testclient import TestClient

from main import app

client = TestClient(app)

def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"Hello": "World"}

def test_read_item():
    response = client.get("/items/10")
    assert response.status_code == 200
    assert response.json() == {"item_id": 10, "q": None}

def test_update_item():
    response = client.put(
        "/items/5",
        json={"name": "test", "price": 300}
    )

    assert response.status_code == 200
    assert response.json() == {"item_name": "test", "item_id": 5}

先ほど起動したUvicornは、実行しておく必要はありません。

テストを実行。

$ pytest
================================================================================== test session starts ===================================================================================
platform linux -- Python 3.10.12, pytest-8.0.0, pluggy-1.4.0
rootdir: /path/to
plugins: anyio-4.2.0
collected 3 items

test_main.py ...                                                                                                                                                                   [100%]

=================================================================================== 3 passed in 0.69s ====================================================================================

こんな感じでしょうか。

おわりに

PythonのWeb APIフレームワーク、FastAPIを試してみました。

謳い文句どおり簡単に使えそうですし、ドキュメントも揃っている感じで良さそうですね。

あまりPythonでWeb APIを書くことはないのですが、それでもFlaskを使ったりしていたのでこれからはFastAPIでもいいかなと
思いました。