目的
WindowsPCでVscodeを使ってFAST APIを少し触ってみました。
公式のチュートリアルを読解して、一部は動作確認をしました。
自分用メモとしてまとめました。
間違っている可能性があることをご了承ください。
実施内容
Python環境構築
公式からインストールする。
https://www.python.org/downloads/
適当なフォルダを作成してVscodeで開く。
Vscode の拡張機能からPythonをインストールする。
適当なtest.pyを作成して実行してみる。
単純なファイルは右上の▶で実行できる。
FAST APIを構築
公式:https://fastapi.tiangolo.com/ja/
公式のトップページを参照して下記二つのコマンドを実行する。
$ pip install fastapi
$ pip install uvicorn
main.pyを公式の指示通り作成して$ uvicorn main:app --reload
を実行する。
http://127.0.0.1:8000/items/10?q=somequery
などでAPIを実行したり、
http://127.0.0.1:8000/docs
からAPI定義がswaggerで閲覧できる。
おまけ:Python Types Intro
tuple、dict、Union、Optional、Pydantic、Annotatedとか記載してあります。
https://fastapi.tiangolo.com/ja/python-types/#__tabbed_4_2
FAST APIのチュートリアル
最初のステップ
FastAPIの使い方、APIの基本が記載されている。
パスパラメータ
型チェック、パスの評価順序、列挙型を用いたAPIの挙動の説明。
パラメータにPathを含んでも対応できる。
例えば下記のAPIを実行するとき、
http://127.0.0.1:8000/files/aaa
などは通常通り機能するが、
http://127.0.0.1:8000/files/C:/Users/test/aaa.txt
でも機能する。
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
C:/Users/test/aaa.txt
を1つのgetパラメータとして処理してくれる。
クエリパラメータ
パスパラメータやクエリパラメータの基本的な使い方の説明。
許容する型の指定、必須の設定など。
boolean方のクエリパラメータであるshortを指定するとき、
short=true
やshort=1
でtrueはもちろん、
short=yes
やshort=on
でもshor=true
として解釈してくれるのは意外でしてた。
リクエストボディ
pydanticを用いたリクエストボディの説明。
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
app = FastAPI()
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Union[str, None] = None):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
クエリパラメータと文字列の検証
Queryを用いたクエリパラメータのバリデーション(最大文字数、初期値、必須など)。
またQueryを用いれば、titleやdescriptionやaliasなどの情報を付加することもできる。
from typing import Union
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Union[str, None] = Query(
default=None,
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
alias="item-query"
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
Path Parameters and Numeric Validations
AnnotatedとPathを用いれば、引数に対して数値のバリデーションなどができる。
下記の例だとge=1
で1以上の値という制約をつけている。
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
Body - Multiple Parameters
1つのリクエストに対して2つのオブジェクト(Item
とUser
)を含む場合は下記のように記載する。
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
class User(BaseModel):
username: str
full_name: str | None = None
@app.put("/items/{item_id}")
async def update_item(
*,
item_id: int,
item: Item,
user: User,
importance: Annotated[int, Body(gt=0)],
q: str | None = None,
):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
if q:
results.update({"q": q})
return results
リクエストは下記のように記載する。
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
},
"importance": 5
}
また、1つのリクエストに対してオブジェクトが1つの場合でもBody(embed=True)
を指定することでオブジェクトのキーをリクエストに要求することができる。
↓Body(embed=True)あり
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
}
↓Body(embed=True)なし
{
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
}
Body - Fields
Annotated
とField
を用いれば、クラスに対して数値のバリデーションなどができる。
class Item(BaseModel):
name: str
description: str | None = Field(
default=None, title="The description of the item", max_length=300
)
price: float = Field(gt=0, description="The price must be greater than zero")
tax: float | None = None
Body - Nested Models
入れ子になっているクラスやリストの説明。オブジェクト指向がわかれば問題ない。
pydanticのhttpurl
の説明。
Declare Request Example Data
Path
やField
などのexamplesを用いることでサンプル値を設定することができる。
defaultよりも優先的に適用される。
複数のexamplesについて、Swagger UI上にチュートリアル通り表示されない。
OpenAPI 3.1.0が原因かも。
Extra Data Types
str
やint
以外にもUUID
やdatetime
などの方も使用できるという説明。
クッキーのパラメータ
Query
と同様の使用方法でCookie
を用いれば、クッキーのパラメータもバリデーションなどが可能。
from typing import Union
from fastapi import Cookie, FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(ads_id: Union[str, None] = Cookie(default=None)):
return {"ads_id": ads_id}
ヘッダーのパラメータ
Cookie
と同様の使用方法でHeader
を用いれば、ヘッダーのパラメータもバリデーションなどが可能。
from typing import List, Union
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
async def read_items(x_token: Union[List[str], None] = Header(default=None)):
return {"X-Token values": x_token}
デフォルトの設定ではアンダーバーはハイフンに自動変換される。
この変換を無効にする場合はconvert_underscores
をFalse
に設定する。
Response Model - Return Type
型アノテーションやresponse_model
により戻り値の型が指定できるという説明。
型アノテーションではAny
、response_model
ではUserOut
クラスを指定した場合、
①POSTのリクエストとしてはUserIn
オブジェクトが入力される。
②user
にUserIn
オブジェクトが代入される
③return user
でuser
が返される。
④response_model=UserOut
からuser
はUserOut
に変換される。
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user
クラスの継承を用いればresponse_model
なしでも同じようなことができるが、レスポンスのデータ量が増えるためresponse_model
を使うことが推奨されている。
下記の例だとresponse_model=None
がないと失敗する。
response_model=None
と指定することでFastAPIデフォルトのデータ検証を無効にしている。
from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/portal", response_model=None)
async def get_portal(teleport: bool = False) -> Response | dict:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return {"message": "Here's your interdimensional portal."}
response_model_exclude_unset=True
を指定すればレスポンスに初期値を含まないように設定できる。
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float = 10.5
tags: list[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
response_model_include
とresponse_model_exclude
を使えばレスポンスの項目を制御できる。(nameはレスポンスに含ませるけどpassは除外するなど)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]
Extra Models
オブジェクトを辞書型に変換、辞書型をもとにオブジェクト生成などの説明。
response_model=Union[PlaneItem, CarItem]
やresponse_model=List[Item]
やresponse_model=Dict[str, float]
を使えばレスポンスが複数ある場合の型指定などができる。
Response Status Code
status_code=status.HTTP_201_CREATED
をなどによりレスポンスのステータスコードを指定できる。
フォームデータ
Query
と同様の使用方法でForm
を用いれば、フォームのパラメータもバリデーションなどが可能。
例が少ないため未検証。
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(), password: str = Form()):
return {"username": username}
Request Files
Query
と同様の使用方法でFile
を用いれば、ファイルに関するバリデーションなどが可能。
UploadFile
を用いれば、filename
によるファイル名の取得やwrite(data)
によるファイル書き込みが可能。
また、Listを用いれば複数ファイルのアップロードに対応可能。
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(
files: Annotated[list[bytes], File(description="Multiple files as bytes")],
):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(
files: Annotated[
list[UploadFile], File(description="Multiple files as UploadFile")
],
):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
Request Forms and Files
Query
と同様の使用方法でFile
やForm
を用いれば、ファイルやフォームに関するバリデーションなどが可能。
from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(
file: Annotated[bytes, File()],
fileb: Annotated[UploadFile, File()],
token: Annotated[str, Form()],
):
return {
"file_size": len(file),
"token": token,
"fileb_content_type": fileb.content_type,
}
Handling Errors
raise HTTPException()
によりHTTPステータスコードを指定した例外処理を発生させることができる。
@app.exception_handler()
により例外時の処理を記載して、JSONResponse
でレスポンスを作成可能。
下記コードについて
①http://localhost:8000/items/3
にアクセス
②HTTPException()
が発生
③http_exception_handler()
が処理される
または
①http://localhost:8000/items/a
を入力
②RequestValidationError
が発生
③validation_exception_handler()
が処理される
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
FastAPIのhttp_exception_handler
を再利用することで例外発生時に独自の処理を入れ込むことが可能。
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {repr(exc)}")
return await http_exception_handler(request, exc)
Path Operation Configuration
status
用いればHTTP_201_CREATED
などを用いることができる。
tags
を用いればSwagger UIを下記のような表記ができる。
@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(item: Item):
return item
@app.get("/items/", tags=["items"])
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get("/users/", tags=["users"])
async def read_users():
return [{"username": "johndoe"}]
summary
やdescription
やresponse_description
を用いてパスオペレーションの説明が可能。
@app.post(
"/items/",
response_model=Item,
summary="Create an item",
description="Create an item with all the information, name, description, price, tax and a set of unique tags",
response_description="The created item",
)
async def create_item(item: Item):
return item
また下記のようにmarkdown形式でコメントを書くことも可能。
@app.post("/items/", response_model=Item, summary="Create an item")
async def create_item(item: Item):
"""
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 item
deprecated=True
とすることで非推奨なパスとしてSwagger UI上に表記できる。
JSON Compatible Encoder
jsonable_encoder()
がオブジェクトをJSON型に変換してくれる。
ボディ - 更新
PATCHにより既存の辞書型変数を部分的な更新ができる。
from typing import List, Union
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: Union[str, None] = None
description: Union[str, None] = None
price: Union[float, None] = None
tax: float = 10.5
tags: List[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.patch("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.dict(exclude_unset=True)
updated_item = stored_item_model.copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item
Dependencies
下記の例だと2つのAPIからCommonsDep
→Depends(common_parameters)
と呼び出され依存関係(≒共通処理?)がある。
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
CommonsDep = Annotated[dict, Depends(common_parameters)]
@app.get("/items/")
async def read_items(commons: CommonsDep):
return commons
@app.get("/users/")
async def read_users(commons: CommonsDep):
return commons
Security
OAuth2.0などに関する説明。
FastAPIのHTTPException
にはヘッダーが存在するため、OAuth 2.0などに対応可能。
ミドルウェア
ミドルウェアを用いたコーディング。
@app.middleware("http")
とすればhttpの共通処理が定義できる。
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
CORS (オリジン間リソース共有)
CORSに関する制約を追加することができる。
リソース共有を許可するオリジンを定義したりできる。
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def main():
return {"message": "Hello World"}
SQL (Relational) Databases
長くなりそうなので別記事で作成予定。
Bigger Applications - Multiple Files
長くなりそうなので別記事で作成予定。
Background Tasks
BackgroundTasks
を用いることで応答後の処理を設定できる。
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_notification(email: str, message=""):
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_notification, email, message="some notification")
return {"message": "Notification sent in the background"}
Metadata and Docs URLs
FastAPIのメタデータの使い方が記載されている。
description
やtags_metadata
やlicense_info
などの説明。
静的ファイル
こちらの記事のほうがわかりやすい。
https://qiita.com/rubberduck/items/3734057d92a5ee7a2e83
静的なファイルをマウントできる。
下記の2ファイルを用意する。
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
@app.get("/static/{item_id}")
async def read_item(item_id):
return {"item_id": item_id}
<h1>test</h1>
http://localhost:8000/static/
にアクセスすると、
index.htmlの内容が表示される。
テスト
$ pip install pytest
してテスト可能な環境が完成。
from .main import app
ではなくfrom main import app
としないと動作しないため注意。
これで$ pytest
も問題なく動作する。
VScodeなら関数単位でテスト可能。
テスト関数をデバックする場合▶を右クリックして、デバックを選択する。
デバッグ
下記のようにimport uvicorn
とif __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
が記載してあればデバックできる。
VScodeの場合は下記から「実行とデバック」を選択して、Pythonを選択する。
http://localhost:8000/docs
からAPIを実行することでデバックが可能となる。
直接URLを叩いてもデバックできない。
おわり
チュートリアルだけでも幅広く、半分程度しか理解できませんでした。
実際にFastAPIを使うときは必要に応じて「高度なユーザーガイド」も参照する必要がありそうです。
気が向いたらこの記事をまた整理しようと思います。