FastAPI Tutorialメモ その2 Path Parameters

## はじめに
FastAPI Souce: https://github.com/tiangolo/fastapi
FastAPI Document: https://fastapi.tiangolo.com

対象: Path Parameters ( https://fastapi.tiangolo.com/tutorial/path-params/ )

FastAPIチュートリアルのメモ。

[FastAPI Tutorialメモ その1](https://qiita.com/kida_kun_/items/bcbaf3b7c97c5102af4a)の続きになります。
FastAPIインストールやサーバの起動は上記のものと同様となりますので、上記記事をご確認ください。

基本的にはFastAPIの公式チュートリアルを参考にしていますが、自身の学習のため一部分を省略したり順番を前後させています。
正しい詳細な情報は公式ドキュメントを参考いただければと思います。

Web & Python 初心者かつ翻訳はGoogleとDeepL頼りのため、間違い等ありましたらご指摘いただけますと幸いです。

## 開発環境
Ubuntu 20.04 LTS
Python 3.8.2
pipenv 2018.11.26

## 目標

- FastAPIの**Pathパラメータ**の理解

## 手順

#### Intro

FastAPIではパスに対するパラメータや変数をPythonの書式と同じ構文で宣言することができます。
以下のコードを`main.py`として保存してください。

```python:main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}
```

この時、デコレータ関数`@app.get("/items/{item_id}")`の`item_id`が**Pathパラメータ**であり、引数`item_id`として関数の引数に渡されます。
例としてこのファイルを保存した後にサーバを起動(`uvicorn main:app --reload`)し、 http://127.0.0.1:8000/items/foo をブラウザで開いてみてください。下記の通り、変数`item_id`に文字列`foo`が代入されたJSON形式のテキストが返ってくるはずです。

```sh
{"item_id":"foo"}
```

実際の画面

![qiita_2_1 2020-07-12 22-53-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607514/2bd94739-6892-6994-f89d-dabfc1f353b3.png)

これがFastAPIにおける**Pathパラメータ**の基本となります。


#### 型付きのPathパラメータ

FastAPIではPython標準の型アノテーションを使用して、関数内で**Pathパラメータ**の型を宣言することができます。

```python:main
~
# 前略
~

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}
```

関数を宣言している`async def read_item(item_id: int):`の行に注目してください。ここでは、引数`item_id`を`int`(整数型)として宣言しています。
型宣言によって、エラーチェックや単語の補完などのエディタサポートを受けられるようになるでしょう。(上記のコードでは、`foo = item_id + "bar"`のように数字と文字列を足す処理をすれば私の環境(PyCharm)ではType Checkerによるエラーが表示されました。)

main.pyの関数部分を上記のコードに変更した状態で http://127.0.0.1:8000/items/3 をブラウザで開くと、次のように表示されます。

```sh
{"item_id":3}
```

ここで、関数`read_item`が受け取り、返り値として表示した値が文字列ではなく整数の`3`であることに注意する必要があります。
URLへの入力はデフォルトではすべて文字列として解釈されますが、Pythonの型宣言によってFastAPIは自動的にリクエストを**parsing**(解析)し、整数型へ変換したのです。

> ためしに型を宣言している部分を取り除いたコード(`async def read_item(item_id: int):` の部分を`async def read_item(item_id):`に戻す)で http://127.0.0.1:8000/items/3 にアクセスると文字列`"3"`として表示されるでしょう。
> 
> ```sh
> {"item_id":"3"}
> ```

また、`int`型として宣言したPathパラメータに対して、  http://127.0.0.1:8000/items/foo のようなURLでアクセスした場合は下記のようなHttpエラーが返されます

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

与えられたパラメータ(`str`型"foo")が`int`型に変換することができないため、FastAPIの**Data validation**(データの検証)機能が働いたためです。
上記の`str`型同様に、浮動少数点数`float`型を与えた場合( http://127.0.0.1:8000/items/4.2 など)も同様のエラーになります。

> なお、`float`型で宣言したパラメータに`int`型の値を与えた場合は変換されることが確認できました

上記から、FastAPIはPythonの型宣言と全く同じバリデーションを行っていることがわかります。

エラーメッセージにはバリデーションが通過しなかったポイント(今回なら`"path"`パラメータの変数`"item_id"`)も明記されていることに注目できます。APIと相互作用するコードの開発やデバッグに非常に役立つでしょう。

型チェックによるバグの防止は、FastAPIの重要な特徴の一つです。




#### ドキュメントの確認
上記のコードを確認したら、ブラウザで http://127.0.0.1:8000/docs を確認してください。以下のように、インタラクティブ(対話的)なAPIのドキュメントが自動的に生成されています。(画面中央付近の`default`の下の`GET`と表記された青いタブをクリックすれば展開されます。)

![qiita_2_2 2020-07-13 19-25-40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607514/704e3bf0-c418-672a-cb96-b4649df9f387.png)

`Parameters`のName`item_id`の下に`integer`とありますね。Pythonの型宣言が(Swagger UIに統合された)インタラクティブなドキュメントにも反映されていることがわかるかと思います。
このドキュメントではAPIの様々な情報を得ることができ、また、デザイナーやフロントサイドの人々と共有することが可能です。

また、このドキュメントはFastAPIが生成したスキーマを元にしたものですが、生成されたスキーマはOpenAPIに準拠しており、様々な互換性のあるツールが開発されています。
[メモその1](https://qiita.com/kida_kun_/items/bcbaf3b7c97c5102af4a)でも確認したましたが、FastAPIではReDocを使用した代替ドキュメントも存在しています。
http://127.0.0.1:8000/redoc にアクセスしてみてください。

![qiita_2_3 2020-07-13 19-40-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607514/76998854-4c4e-f72c-1451-49c5196b8113.png)

ざっくりとした個人的な印象ですが、Swagger UI は実際にリクエストを生成して結果の確認までできるインタラクティブ(対話的)なドキュメント、一方のReDocは静的でシンプルなドキュメントという認識です。おそらくもっと色々な使い方や別のツールがあるのでしょうが、勉強不足でまったくわかりません。すみません。学習次第、更新していきたいと思います。

今のところ、リクエストを生成して結果も見れるSwagger UIが便利だなぁという気持ちです。


#### Pydantic
本題に戻りまして、このようなドキュメント生成にも役立つ型アノテーションは、すべて**Pydantic**というライブラリによって行われています。
Pydanticについては下記の記事が参考になりました。
[Pydantic 入門](https://qiita.com/0622okakyo/items/d1dcb896621907f9002b) @0622okakyo

**Pydantic**により、`str`、`float`、`bool`など多くのデータ型で、すべて同様に型アノテーションを行うことができます。


#### Path Operationにおける順序問題

Path Operationを作成する場合、パスが固定されてしまう状況になることがあります。

例として、ユーザが自身のデータを取得するための`/users/me`というパスがあるとします。
この時、同時にある別のユーザに関するデータを`user_id`を使用して取得する`/users/{user_id}`というパスも存在するとしましょう。

FastAPIにおけるPath Operationは順番に評価されていきます。
よって、下記のようにパス`/users/me`はパス`/users/{user_id}`よりも前に宣言される必要があります。

```python:main
from fastapi import FastAPI

app = FastAPI()


@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}


@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}
```

もし下記のようにパス`/users/me`がパス`/users/{user_id}`の後に宣言された場合、`me`という値が`user_id`というパラメータとしてFastAPIに渡されてしまいます。

```python:main_miss
from fastapi import FastAPI

app = FastAPI()


@app.get("/users/{user_id}")  # <- "/users/me"がGETメソッドできた場合、"me"という値が渡される
async def read_user(user_id: str):
    return {"user_id": user_id}


@app.get("/users/me")   # <- "/users/me"はすでに評価されたため、この関数は呼び出されない
async def read_user_me():
    return {"user_id": "the current user"}
```

#### Predefined values (定義済みの値)

パスパラメータを受けるPath Operaterがあり、そのパラメータの設定可能な有効値を事前に定義しておきたい場合、Pythonの標準モジュール`Enum`の使用をおすすめします。

まずは`Enum`クラスを作成してみましょう。

`Enum`をインポートし、`str`と`Enum`を継承したサブクラスを作成します。
`str`を継承することで、APIドキュメントは設定される値が文字列型であることを事前に知ることができ、正しくレンダリングを行われるようになります。

次に、有効な値となる固定値を持ったクラス属性を作成します。

```python:main
from enum import Enum   # <- Enumのインポート

from fastapi import FastAPI


class ModelName(str, Enum): # <- クラスの継承
    alexnet = "alexnet"     # <- クラス属性(固定値)の作成
    resnet = "resnet"
    lenet = "lenet"


app = FastAPI()
```

定義したクラスを用いてパスパラメータを定義していきます。

作成した列挙型クラス(`ModelName`)を使用し、型アノテーションを付与したパスパラメータを作成します。

```python:main
from enum import Enum

from fastapi import FastAPI


class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"


app = FastAPI()


@app.get("/model/{model_name}")
async def get_model(model_name: ModelName): # <- 型アノテーションを付与したパスパラメータ
    if model_name == ModelName.alexnet:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}

    if model_name.value == "lenet":
        return {"model_name": model_name, "message": "LeCNN all the images"}

    return {"model_name": model_name, "message": "Have some residuals"}
```

ドキュメント( http://127.0.0.1:8000/docs )を確認してください。
パスパラメータで使用できる値は事前に定義されているため、ドキュメントでそれらの値がインタラクティブに表示、選択されます。

1. `GET`タブをクリックし、タブ上部の`Try it out`をクリックしてください。
2. `model_name`が選択可能になります。
3. 値を選択して`Execte`をクリックすることでリクエストを発行できます。

(下記のスクリーンショットは[公式ドキュメト](https://fastapi.tiangolo.com/tutorial/path-params/)からの引用になります。
私の環境では選択画面のスクリーンショットが上手く取得できませんでした。)

![qiita_2_4 2020-07-13 23-00-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607514/8d07e813-73e4-4b12-3004-3e1ed974429c.png)


#### 列挙型について

```python:main
from enum import Enum

from fastapi import FastAPI


class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"


app = FastAPI()


@app.get("/model/{model_name}")
async def get_model(model_name: ModelName):
    if model_name == ModelName.alexnet: # <- ①
        return {"model_name": model_name, "message": "Deep Learning FTW!"}  # <- ③

    if model_name.value == "lenet":　# <- ②
        return {"model_name": model_name, "message": "LeCNN all the images"}    # <- ③ 

    return {"model_name": model_name, "message": "Have some residuals"}     # <- ③ 
```

詳細は[Python公式ドキュメント enum --- 列挙型のサポート](https://docs.python.org/ja/3/library/enum.html#module-enum)を参考にしてください。

ここでは、作成した`ModelName`の列挙型メンバーとの比較(①)、`.value`による列挙型の値の取得とその比較(②)、列挙型メンバーを返り値として使用(③)を行っています。
辞書型`dict`のようにネストになったJSONボディの場合でも、Path Operaterの値をenumメンバーとして返り値にすることが可能です。この場合、対応する値(上記では`str`)に変換されてクライアントに返されます。

インタラクティブドキュメントやブラウザでGETした場合、例えばこのようなJSONの返り値を取得できるでしょう。

```sh
{
  "model_name": "alexnet",
  "message": "Deep Learning FTW!"
}
```

#### パスを含むパスパラメータ

例えば、`/files/{file_path}`というパスを持つPath Operationがあるとします。
しかし、`file_path`自体が値としてパスを持っている場合(`file_path` = `home/johndoe/myfile.txt`のような場合)、
URLは次のようになるでしょう。

```sh
http://127.0.0.1:8000/files/home/johndoe/myfile.txt
```

OpenAPIでは、テストの定義と困難なシナリオの発生につながるという理由から、上記のようなパスを値として持つパスパラメータの宣言をサポートしていません。

しかしながら、Starletteの内部ツールを使用することで、FastAPIではそのようなパスパラメータを宣言することができます。
(ただし、OpenAPIに準拠するAPIドキュメントでパスパラメータ自体にパスが含まれていることを確認することはできません。※ドキュメント自体は動作します)

次のような URL を使用して、パスを含むパスパラメータを宣言します。

```python
/files/{file_path:path}
```

上記では、パスパラメータは`file_path`であり、`:path`の部分がパスパラメータにパスが含まれていることを明示しています。

使用例としては以下のようになります。

```python:main
from fastapi import FastAPI

app = FastAPI()


@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}
```



## 終わりに

次回はクエリパラメータになります。