見出し画像

FastAPI勉強日記: #3. リクエストデータを受け付ける

前回まででFastAPIの開発環境が整ったので、FastAPIでリクエストを受け付ける部分を勉強することにした。

Pathパラメーターを受けつける

@app.get("/books/{book_id}")
def get_book(book_id: int):
    return {"book_id": book_id}

nodeと似ているので、すんなり頭に入っていく。ポイントとしては、Pythonでも型指定をしていて、上のレではintタイプで、book_idを受け付ける、というふうに関数を書くところだ。
python = 型がないというイメージのまま頭が何年もアップデートされていない自分にとっては新鮮に思った。
(型指定があるのはこのリクエストを受け付けるメカニズムにPydanticというものが動いているからだと後でわかった。)

早速上で作ったAPIを動かしてみる。

ブラウザにアクセシス、ローカルホストの8000番ポートの/books/15というパスにアクセスすると15の部分がint型のデータとして、book_idに入ることがレスポンスのJSONからも確認できた。


パスのパラメーターである15がbook_idとして入力される

次に、/15ではなく、/great_gatsbyというパスにアクセスしてみると、

↓のような返信が返ってくる。

{"detail":[{"loc":["path","book_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}

"great_gatsby"というデータをintに変換できなくてエラーとなったようだ。

実行中のFastAPIのログには下のようなエラーがでていることがわかる。

"GET /books/great_gatsby HTTP/1.1" 422 Unprocessable Entity


ネストされたPathパラメーターを取得する

先程の続き、で下のように複数のPathパラメーターをリクエストで受け取ることもためしてみた。

@app.get("/books/{book_type}/{book_id}")
def get_book(book_type: str, book_id: int):
    return {"book_type": book_id, "book_id": book_id}

http://127.0.0.1:8000/books/english/3
にアクセスすると、下のように返ってくる。
{"book_type":3,"book_id":3}

このあたりはnode.jsと同じなのでわかりやすい。

バリデーション

FastAPIでは、リクエストデータに対してバリデーションを手軽い付与できるのが驚いた。

@app.get("/books/{book_id}")
def get_book(book_id: int = Path(..., ge=100)):
    return {"book_id": book_id}

例えば↑のように、先程の例でintという型指定をしたところに、= Path(…, ge=1000)と追加すると、
下のようにbook_idが100未満の値を指定したときに、エラーを422返すようになる。

100未満の数字をパスにいれるとエラーとなる

Path(…, ge=100))のところは、本来デフォルト値を指定するところで、デフォルト値はないので、…がしていされている。

ge=100のgeはgreater than or equalの略で、この場合”100以上”を意味する。
つまり、Path(…, ge=100))は、book_idが100以上であるという成約を付け足していることになる。

ほかにも
gt 
lt
le
があり、それぞれ、”greater than(より大きい)”、"less than(未満)”、”less than or equal to(less than or equal to)”の条件を指定できるようだ。


文字列のバリデーション


@app.get("/search_book/{book_name}")
def search_book(book_name: str = Path(..., min_length=1, max_length=20)):
    return {"book_name": book_name}

book_nameに上のように1文字のものを指定すると、422エラーになりまる。

このように最小文字数や、最大文字数を追加することができる。

クエリパラメーター

パスパラメーターではなく、クエリパラメータは下のようにPathの定義(@app.get()の中)には入れずに、関数のパラメーターの方にだけ入れる。

@app.get("/search_book/{book_name}")
def search_book(book_name: str, offset: int = 0, limit: int = 10):
    return {"book_name": book_name, "offset": offset, "limit": limit}

実に簡素でわかりやすいなと思った。

クエリパラメータを設定せずにリクエストを送ると
http://127.0.0.1:8000/search_book/great

↑のように、関数のパラメーターのところで設定したoffsetとlimitにデフォルト値(0と10)が自動的にoffsetとlimit変数に設定される。

↓のようにクエリパラメータを設定するとhttp://127.0.0.1:8000/search_book/great?offset=100&limit=50

クエリパラメータで指定した値が設定される。

クエリパラメータにバリデーションを追加するには、↓のようにQueryクラスを使う。

from fastapi import FastAPI, Path, Query

@app.get("/search_book/{book_name}")
def search_book(book_name: str, offset: int = Query(0, ge=0), limit: int = Query(10, le=100)):
    return {"book_name": book_name, "offset": offset, "limit": limit}

Queryクラスのコンストラクタにわたす1つ目の値がデフォルト値、2つ目がValidationの条件になっている。

http://127.0.0.1:8000/search_book/great?offset=100&limit=500
のようなリクエストを送ると↓のように422エラーが返される。


Postリクエスト

PostリクエストのBodyの情報はBodyクラスで受け取る↓

@app.post("/photos")
async def create_photo(title: str = Body(...), filename: str = Body(...), price: int = Body(...)):
    return {"tilte": title, "filename": filename, "price": price}

注意点としては、@app.post()のように、@app.get()から変更しないといけないこと。

このPost APIを試すのに、Postmanもいいが、今回、httpieというツールを使ってみた。

httpie


httpieのインストール

 brew install httpie

下のようにパスの次に、key=valueのペアを連ねるだけで、このkey-valueペアを内包したJSONファイルがポストできるので非常に便利である。

% http POST http://127.0.0.1:8000/photos title="cat" filename="cat1.jpg" price="500"
HTTP/1.1 200 OK
content-length: 49
content-type: application/json
date: Fri, 08 Sep 2023 07:46:32 GMT
server: uvicorn

{
    "filename": "cat1.jpg",
    "price": 500,
    "title": "cat"
}

ポストされたデータがそのままリターンされるAPIなので、リクエストがちゃんと受け取られたことを確認できた。

ちなみに、-vオプションをつけるとリクエストの内容もログにでてくるので、デバッグ時に、ヘッダの中身やリクエストbodyの中身の確認に使える

% http -v POST http://127.0.0.1:8000/photos title="cat" filename="cat1.jpg" price="500"
POST /photos HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 56
Content-Type: application/json
Host: 127.0.0.1:8000
User-Agent: HTTPie/3.2.2

{
    "filename": "cat1.jpg",
    "price": "500",
    "title": "cat"
}


HTTP/1.1 200 OK
content-length: 49
content-type: application/json
date: Fri, 08 Sep 2023 07:45:55 GMT
server: uvicorn

{
    "filename": "cat1.jpg",
    "price": 500,
    "title": "cat"
}


Pydantic Model

Postするデータが大きくなってくると先の例のようにすべてのデータをパラメーターで定義するのが億劫になってくるし、視認性も悪くなる。

Pydanticのモデルを使うと、リクエストの内容からオブジェクトを生成できて非常に便利である。

# ↓を新たに追加
from pydantic import BaseModel

...



class Photo(BaseModel):
    title: str
    filename: str
    price: int


@app.post("/photos")
async def create_photo(photo: Photo):
    return photo

上の例は、先の例と全く同じJSONを受け取るAPIだが、Photoクラスのオブジェクトとしてリクエスト内のデータを受け取る例である。
受け取ったPhotoクラスのオブジェクト(photo)をそのままreturnしてクライアントに返しているが、かってにJSONに変換されるので、便利すぎて驚いたのであった。

このようにオブジェクトでリクエストを受け取るためにはPhotoクラスはPydanticのBaseModelを継承したクラスでなければならない。ここがすごく重要なポイントである。
BaseModelの小クラスとして自分の欲しいクラスを定義すると、あとはPydanticがデータ変換、バリデーション、変数への代入をやってくれるという仕組みになっているようである。

Pydanticについて知ったのは今回が初めてだったが、これを使うとリクエストデータのバリデーションももっといろいろ(上の例だとPhotoオブジェクトに必要なデータがリクエストに入っているかのチェックなど)できるようで、FastAPIを実装する上で中心的な存在になりそうである。

このあと、もう少しPydanticに慣れておこうと思った。

続く。

このブログに関する質問や、Webサービス、Android・iOSアプリの開発の相談はこちら↓↓↓からお願いします!

mizutori@goldrushcomputing.com
@mizutory


次回は↓↓↓





この記事が気に入ったらサポートをしてみませんか?