LoginSignup
6
8

More than 1 year has passed since last update.

はじめての FastAPI(前編)

Last updated at Posted at 2021-04-22

後編はこちら
https://qiita.com/uturned0/items/16eeaf0bcb84d5c88853

FastAPIを使ってみる

ref
https://fastapi.tiangolo.com/tutorial/

install

pip install fastapi[all]

run

pycharmで動くように、こうする

main.py
from fastapi import FastAPI
import uvicorn

app = FastAPI()


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

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")

PyCharmの Run 設定は
普通の Python で
script path -> Module name にして uvicorn を選ぶ
main:app --reload --port 8000

しかし、これだと auto reload されなかった。

どうしてもappとrunが別ファイルじゃないとダメみたいなので、仕方ない
debug用のfileを作る。

debug.py
import uvicorn

# Importing app here makes the syntax cleaner as it will be picked up by refactors
from main import app

if __name__ == "__main__":
    uvicorn.run("debug:app", host="0.0.0.0", port=8000, reload=True)

これをpythonで走らせれば、 auto reload もok. uvicornをする必要はない。

イマイチだけど公式にもpycharm用のmanualがあった
https://fastapi.tiangolo.com/tutorial/debugging/

さあ見てみよう

API
http://0.0.0.0:8000/

Doc
http://0.0.0.0:8000/docs

Redoc
http://127.0.0.1:8000/redoc

Open API json scheme
http://127.0.0.1:8000/openapi.json

見れた!

Path Parameters¶

add an endpoint

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

http://0.0.0.0:8000/items/1

型 validation

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

http://0.0.0.0:8000/items/strings
{
detail: [
{
loc: [
"path",
"item_id"
],
msg: "value is not a valid integer",
type: "type_error.integer"
}
]
}

Use ENUM

from enum import Enum

from fastapi import FastAPI


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


@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    return {"model_name": model_name, "message": "Have some residuals"}

このとき注意点として、 model_name に入るのはstringsではなく、ModelName object.

model_name = {ModelName} ModelName.resnet
model_name.value = 'resnet'

ただし不思議な力で、 model_name をそのまま return すると model_name.value の strings が返る。

エラーの中にちゃんと使えるENUMが。優しい。

{
detail: [
{
loc: [
"path",
"model_name"
],
msg: "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
type: "type_error.enum",
ctx: {
enum_values: [
"alexnet",
"resnet",
"lenet"
]
}
}
]
}

PyDantic

validationは pydantic を使ってるので困ったらここを調べる。
https://pydantic-docs.helpmanual.io/

Query Parameters¶

ref
https://fastapi.tiangolo.com/tutorial/query-params/#query-parameters

本題だ。

pagination

fake_items_db = [{"name": "a"}, {"name": "b"}, {"name": "c"},{"name": "d"}, {"name": "e"}, {"name": "f"},{"name": "g"}, {"name": "h"}, {"name": "i"}]
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip : skip + limit]

[
{
name: "a"
},
{
name: "b"
},
{
name: "c"
}
]

[
{
name: "f"
},
{
name: "g"
}
]

optional parameters

types の Optional を使って、 deafult = None にするのがpoint

from typing import Optional

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

{
item_id: "foo"
}

{
item_id: "foo",
q: "1"
}

List だけ、 keyなし の model

Custom Root Types🔗
Pydantic models can be defined with a custom root type by declaring the root field.
The root type can be any type supported by pydantic, and is specified by the type hint on the root field. The root value can be passed to the model init via the root keyword argument, or as the first and only argument to parse_obj.

こういうとき

pets : Pets = Pets(["cat", "dog"])

__root__ を使う。

from pydantic import BaseModel

class Pets(BaseModel):
    __root__: List[str]

これだと loop できないので iter をつける

from pydantic import BaseModel

class Pets(BaseModel):
    __root__: List[str] 

    def __iter__(self):
        return iter(self.__root__)

    def __getitem__(self, item):
        return self.__root__[item]

    # len() が使えなかったので足してみた
    def __len__(self):
        return len(self.__root__)

呼び出し方法

print(Pets(__root__=['dog', 'cat']))

return に含める query parameter を変更する方法

最初に item を宣言して、後から必要に応じて item.update() する。


@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Optional[str] = None, short: bool = False):
    item = {"item_id": item_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

bool queryが true になるとき

これ全部 true

http://127.0.0.1:8000/items/foo?short=1
or


http://127.0.0.1:8000/items/foo?short=True
or


http://127.0.0.1:8000/items/foo?short=true
or


http://127.0.0.1:8000/items/foo?short=on
or


http://127.0.0.1:8000/items/foo?short=yes

query param を必須にするときは default 値をつけない

必ず 値が帰ってきそうに見えるが ↓

@app.get("/items/")
async def read_user_item(needy: str):
    return {"needy": 1}

これはokだが
http://127.0.0.1:8000/needy/?needy=1

これはNG
http://127.0.0.1:8000/needy/

detail: [
{
loc: [
"query",
"needy"
],
msg: "field required",
type: "value_error.missing"
}
]
}

Request Body¶

POST を受け取る

POST はまず ojbect を定義する。継承するのは pydantic の BaseModel.
keyに対して型を定義できる。

from pydantic import BaseModel

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

POSTのテストは /docs を使うといい。

このとき、post内容に無駄なkey:valueを入れておくと、自然にomitされる。

curl -X 'POST' \
  'http://0.0.0.0:8000/items/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "string",
  "description": "string",
  "price": 0,
  "tax": 0,
  "unnecessary" : "value"
}'


Response body
{
  "name": "string",
  "description": "string",
  "price": 0,
  "tax": 0
}

required な nameがないと、当然優しいエラー

curl -X 'POST' \
  'http://0.0.0.0:8000/items/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "description": "string",     # <------
  "price": 0,
  "tax": 0
}'

Response body
{
  "detail": [
    {
      "loc": [
        "body",
        "name"
      ],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

POST にアクセス

POST data は model object で受け取るので、パラメータはmodel経由で受け取る。

item.price とか item.tax とか。


@app.post("/items/")
async def create_item(item: Item):
    item_dict = item.dict()
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

POST をそのまま return したいときは **item.dict()


@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()} <-------

key なしの POST body を受け取る Body(...)

これびっくりした。何だ ... って。
すっかり nodejs みたいになってるな・・

@app.put("/body")
async def update_item(
    any_name: int = Body(...)
):
    results = {"any_name": any_name}
    return results

つまり↓の、keyなしvalueだけをpostしたときは

{
    1
}

それを Body(...) で受け取れる。

curl -X 'PUT' \
  'http://0.0.0.0:8000/body' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '111'

Response body
{
  "any_name": 111
}

これは 他のbodyと混ざっててもok.

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

まあ・・こんなことしないよね・・・

ちなみにvalidationさせることもできて、gt=100 で greater than 100になる

    any_name: int = Body(..., gt=100)

99投げるとエラー

{
  "detail": [
    {
      "loc": [
        "body"
      ],
      "msg": "ensure this value is greater than 100",
      "type": "value_error.number.not_gt",
      "ctx": {
        "limit_value": 100
      }
    }
  ]
}

Query Parameters and String Validations¶

パラメータに validation

関数の引数に Query() をつけると、その中で条件をかけれる

from fastapi import FastAPI, Body, Query
from typing import Optional

@app.get("/max_length/")
async def read_items(q: str = Query(None, max_length=50)):
    return {"q": q}

regex validation

    q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$")

Query(None) の None は default value

こうすると、q を指定しないで投げると勝手に 'this-is-default-value' が返ってくる。

# q is required
@app.get("/default-value/")
async def default-value(q: str = Query('this-is-default-value', min_length=3)):
    return {"q": q}

result

curl -X 'GET' \
  'http://0.0.0.0:8000/default-value/' \
  -H 'accept: application/json'

Response body
{
  "q": "this-is-default-value"
}

Make it required¶

  • Query() を使っているときの Required は Query(...)
    • この ... は省略記号じゃないよ!このままピリオド3つを書くんだよ!
  • Not requiredは Query(None) または Query(<何かデフォルト値>)

わっかりづらー! よくわからんのやけど、Query()を使うときは Optional があまり意味をなさない。
それはこの例でわかる。

# q is NOT required
@app.get("/required4/")
async def required4(q: str = Query(None, min_length=3)):
    return {"q": q}

q: Optional[str] じゃないのに、これ NOT required なんです。それは Query(None) だから。

いろいろ試しました。↓


# q is required
@app.get("/required1/")
async def required1(q: str):
    return {"q": q}

# q is NOT required
@app.get("/required2/")
async def required2(q: Optional[str] = Query(None, min_length=3)):
    return {"q": q}

# q is required
@app.get("/required3/")
async def required3(q: Optional[str] = Query(..., min_length=3)):
    return {"q": q}

# q is NOT required
@app.get("/required4/")
async def required4(q: str = Query(None, min_length=3)):
    return {"q": q}

# q is required
@app.get("/required5/")
async def required5(q: str = Query(..., min_length=3)):
    return {"q": q}

# q is required
@app.get("/required6/")
async def required6(q: str = Query('this-is-default-value', min_length=3)):
    return {"q": q}

結論

Query(...) だけが required になる。 Optionalは関係ない。 Quiery(None) または Query(default-value) は NOT required.

query param を重複しても list で受け取れる スゴイ

from typing import List, Optional

@app.get("/duplicated/")
async def read_items(q: Optional[List[str]] = Query(None)):
    query_items = {"q": q}
    return query_items

こんな無茶なurl投げると
http://0.0.0.0:8000/duplicated/?q=string1&q=string2

{
  "q": [
    "string1",
    "string2"
  ]
}

えっ昨今のAPIってこれ当たり前なの!?!?! おっさん死にそう・・・

parameterの説明はコメントじゃなくて descriptionに書こう

/docs, /redoc で説明が見えるようになる。title/description は api responseには影響がない。

@app.get("/items/")
async def read_items(
    q: Optional[str] = Query(
        None,
        title="Query string",
        description="Query string for the items to search in the database that have a good match",
        min_length=3,
    )
):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results

query key にハイフンが付く場合は Query(, alias)

そもそも、これで body.q が取れるのが不思議だよね

async def required5(q: str = Query(...)):

これは引数の名前を使って fastapiが勝手にbodyから同じ 名前の q を探してくれてる。
でも、もしurlが /?key-foo=1 の場合、pythonで body.key-foo というオブジェクトは作れないので
alias を設定する。

async def read_items(q: Optional[str] = Query(None, alias="item-query")):

こうすると、item-query が q に代入される!

deprecated は /docs に出るだけ

response に注意分が出るかと思ったけど、変わらなかった。

@app.get("/deprecated/")
async def deprecated(q: Optional[str] = Query(None, deprecated=True)):
    return q

Path Parameters and Numeric Validations¶

明日はここから
https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/

あと読むの必須 memo

Error handling
https://fastapi.tiangolo.com/tutorial/handling-errors/

SQL (Relational) Databases¶
https://fastapi.tiangolo.com/tutorial/sql-databases/

Bigger Applications - Multiple Files¶
https://fastapi.tiangolo.com/tutorial/bigger-applications/

Testing
https://fastapi.tiangolo.com/tutorial/testing/

けっこうあるなー。一日掛かりそう

グダグダな後編はこちら
https://qiita.com/uturned0/items/16eeaf0bcb84d5c88853

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8