Open11

FastAPIで遊ぶ

not75743not75743

HelloWorld

main.py
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: str = None):
    return {"item_id": item_id, "q": q}

確認

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

$ curl localhost:8080/items/1
{"item_id":1,"q":null}
not75743not75743

パスパラメータ

単一

main.py
@app.get("/path/{path_id}")
def read_path(path_id: int):
    return {"path_id": path_id}
$ curl localhost:8080/user/1/items/1
{"user_id":1,"item_id":1}

複数

main.py
@app.get("/user/{user_id}/items/{item_id}")
def read_user_item(user_id: int, item_id: int):
    return {"user_id": user_id, "item_id": item_id}
$ curl localhost:8080/user/1/items/1
{"user_id":1,"item_id":1}

文字列

main.py
@app.get("/users/{user_name}")
def read_user(user_name: str):
    return {"user_name": user_name}
$ curl localhost:8080/users/not75743
{"user_name":"not75743"}
not75743not75743

クエリパラメータ

シンプル

main.py
@app.get("/items")
def read_items(int1: int = 1, int2: int = 2):
    return {"int1": int1, "int2": int2}
$ curl localhost:8080/items
{"int1":1,"int2":2}

$ curl localhost:8080/items?int1=100
{"int1":100,"int2":2}

$ curl localhost:8080/items?int1=100\&int2=1000
{"int1":100,"int2":1000}

オプショナル

main.py
from typing import Optional

@app.get("/items2")
def read_items2(int1: Optional[int] = None, int2: int = 10):
    if int1:
        return {"int1": int1, "int2": int2}
    return {"int2": int2}
$ curl localhost:8080/items2
{"int2":10}

$ curl localhost:8080/items2?int2=10000000
{"int2":10000000}

$ curl localhost:8080/items2?int1=50000\&int2=10000
{"int1":50000,"int2":10000}

バージョンによりいろいろな書き方が
https://fastapi.tiangolo.com/python-types/#union

パスパラメータとの併用

main.py
from typing import Optional

@app.get("/items3/{item_id}")
def read_items3(item_id: int, int1: Optional[int] = None, int2: int = 10):
    if int1:
        return {"item_id": item_id, "int1": int1, "int2": int2}
    return {"item_id": item_id, "int1": int1, "int2": int2}
$ curl localhost:8080/items3/777?int2=20
{"item_id":777,"int1":null,"int2":20}

$ curl localhost:8080/items3/777?int1=30\&int2=20
{"item_id":777,"int1":30,"int2":20}
not75743not75743

ドキュメント

/docsにアクセスすることで対話的なドキュメントが見れる
パス、クエリパラメータ、それが必須かどうか、などの様々な情報が見れる

not75743not75743

pydanticによるデータの検証

main.py
from typing import Optional

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

@app.post("/items/")
async def create_item(item: Item):
    return item
curlで確認
## 成功
$ curl -X POST "curl -X POST "http://localhost:8080/items/" \
     -H "Content-Type: application/json" \
     -d '{
           "name": "sample item",
           "description": "A sample item description",
           "price": 15.99,
           "tax": 1.5
         }'
{"name":"sample item","description":"A sample item description","price":15.99,"tax":1.5}

## nameが不正のためエラー
$ curl -s -X POST "http://localhost:8080/items/" \
     -H "Content-Type: application/json" \
     -d '{
           "name": 3,
           "description": "A sample item description",
           "price": 150,
           "tax": 1.5
         }' | jq
{
  "detail": [
    {
      "type": "string_type",
      "loc": [
        "body",
        "name"
      ],
      "msg": "Input should be a valid string",
      "input": 3,
      "url": "https://errors.pydantic.dev/2.5/v/string_type"
    }
  ]
}

## optionalのため成功
$ curl -X POST "curl -X POST "http://localhost:8080/items/" \
     -H "Content-Type: application/json" \
     -d '{
           "name": "sample item",
           "price": 15.99
         }'
{"name":"sample item","description":null,"price":15.99,"tax":null}

リクエストボディの表示

not75743not75743

バリデーションの追加

文字数を50に制限

main.py
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=20)):
    return {"query": q}
$ curl localhost:8080/items?q=aaaaaaaaaaaaaaaaaaaa
{"query":"aaaaaaaaaaaaaaaaaaaa"}

$ curl -s localhost:8080/items?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | jq
{
  "detail": [
    {
      "type": "string_too_long",
      "loc": [
        "query",
        "q"
      ],
      "msg": "String should have at most 20 characters",
      "input": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "ctx": {
        "max_length": 20
      },
      "url": "https://errors.pydantic.dev/2.5/v/string_too_long"
    }
  ]
}

その他いろいろなバリデーション
https://fastapi.tiangolo.com/ja/tutorial/query-params-str-validations/

not75743not75743

docstringでdescription

main.py
@app.get("/items")
async def read_items(q: Optional[str] = Query(None, max_length=20)):
    """
    Create an item with all the information:

    - **name**: each item must have a name
    - **description**: a long description
    - **price**: required
    - **tax**: if the item doesn't have tax, you can omit this
    - **tags**: a set of unique tag strings for this item
    """
    return {"query": q}

見た目

マークダウン使えるんですね

not75743not75743

パスのバリデーション

main.py
from fastapi import FastAPI, Path

app = FastAPI(
    title="fastapi test",
)


@app.get("/items/{item_id}")
async def read_items(
    item_id: int = Path(title="The ID of the item", ge=1, le=10)
):
    return {"item_id": item_id}
# 1文字のint、成功
$ curl -X 'GET' \
  'http://localhost:8080/items/1' \
  -H 'accept: application/json'
{"item_id":1}

# 11文字のint、失敗
$ curl -s -X 'GET' \
  'http://localhost:8080/items/11' \
  -H 'accept: application/json' | jq
{
  "detail": [
    {
      "type": "less_than_equal",
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "Input should be less than or equal to 10",
      "input": "11",
      "ctx": {
        "le": 10
      },
      "url": "https://errors.pydantic.dev/2.5/v/less_than_equal"
    }
  ]
}
not75743not75743

パスのバリデーション Annotated

Annotatedを使うのが望ましい
バージョンによっては使えないっぽい?

main.py
@app.get("/items-annotated/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=10)]
):
    return {"item_id": item_id}
not75743not75743

複数のモデルを受け取る

main.py
from typing import Optional

from fastapi import Body, FastAPI
from pydantic import BaseModel


class Item(BaseModel):
    name: str
    description: Optional[str] = None

class User(BaseModel):
    username: str
    full_name: Optional[str] = None

app = FastAPI()

@app.put("/items/{item_id}")
async def update_item(
    item_id: int,
    item: Item,
    user: User,
    importance: int = Body(..., gt=0),
):
    results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
    return results