🙆
fastapi + pydantic + devcontainer でサーバーを建てる
この記事は何
この記事は python の学習を兼ねて色々書き散らしたので、 fastapi のサーバーを建てるという軸では理解しづらくなった。
上から順になぞるだけで devcontainer 上で fastapi の開発環境が構築できるようにする。
プロジェクトを作成
rye のインストールは略
$ rye init fastapi-example
$ cd fastapi-example
$ rm -r src # 作る対象がライブラリではないので一旦消す
$ rye sync
$ rye add fastapi fastapi-cli pydantic
$ rye add mypy pytest -d
.vscode/settings.json
{
"deno.enable": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
},
"python.analysis.typeCheckingMode": "strict",
"files.exclude": {
".venv": true,
"**/__pycache__": true,
"**/*.pyc": true,
".*cache": true,
"*.lock": true
}
}
FastAPI + Pydantic でサーバーを実装
main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
class Input(BaseModel):
name: str
age: int
class User(BaseModel):
id: int
name: str
age: int
app = FastAPI()
@app.get("/health")
async def health():
return dict(status="ok")
@app.post("/users", response_model=User)
async def create_user(input: Input) -> User:
return User.model_validate(dict(name=input.name, age=input.age, id=1))
@app.get("/users", response_model=List[User])
async def users() -> List[User]:
return [User.model_validate(dict(name="John", age=30, id=1))]
@app.get("/")
async def root():
return {"greeting": "Hello world"}
$ rye run fastapi dev
http://localhost:8000 で動作を確認
fastapi のユニットテストを追加
fastapi 自体が testclient を提供してくれているので、これを使う
main_test.py
from main import app
from fastapi.testclient import TestClient
client = TestClient(app)
def test_get_users():
response = client.get("/users")
assert response.status_code == 200
assert response.json() == [{ "id": 1, "name": "John", "age": 30,}]
rye 経由で pytest を叩く
$ rye test
rye.scripts でタスクを登録
pyproject.toml
[tool.rye.scripts]
dev = { cmd = ["fastapi", "dev"], env-file = ".env" }
test = { cmd = ["rye", "test"]}
rye run <task>
で実行できる
devcontainer でコンテナで実行
このファイルを .devcontainer/devcontainer.json
に置く
.devcontainer/devcontainer.json
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/schlich/devcontainer-features/rye:1": {},
},
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
"ms-python.mypy-type-checker",
"charliermarsh.ruff",
"ms-python.python"
]
}
},
"remoteUser": "vscode"
}
vscode のコマンドパレット(TODO: 名前忘れた)からコンテナに入る
コンテナ内で rye sync
して rye run dev
できればおk
デプロイ
TODO
あとで調べる。手軽なのは fly.io か cloudrun だろうか。
おまけ: openapi の spec を吐いて、 deno から型付きで叩く
OpenApi の JSON を吐いて、そこから型定義を生成して deno から叩く。
deno も devcontainer に含める。
[tool.rye.scripts]
dev = { cmd = ["fastapi", "dev"], env-file = ".env" }
test = { cmd = ["rye", "test"] }
gen = { cmd = ["python", "-c", "from main import app;from json import dump;dump(app.openapi(),open('gen/spec.json','w'))"]}
openai の定義を経由して spec.d.ts を生成する。
$ mkdir gen
$ rye run gen
$ deno run -A npm:openapi-typescript gen/spec.json -o gen/spec.d.ts
型付きで動く typed_test.ts
import type { paths } from "./gen/spec.d.ts";
import { expect } from "jsr:@std/expect";
import { Fetcher } from 'npm:openapi-typescript-fetch@2.0.0';
const fetcher = Fetcher.for<paths>()
fetcher.configure({
baseUrl: 'http://localhost:8000',
});
const getUsers = fetcher.path('/users').method('get').create();
Deno.test('getUsers', async () => {
const res = await getUsers({});
expect(res.data).toEqual([{ id: 1, name: "John", age: 30 }]);
});
テストを実行
⬢ [Docker] $ deno test -A .
running 1 test from ./typed_test.ts
getUsers ... ok (4ms)
ok | 1 passed | 0 failed (5ms)
.devcontainer
側でも deno を呼べるようにしておく。
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/schlich/devcontainer-features/rye:1": {},
"ghcr.io/devcontainers-community/features/deno": {}
},
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-docker",
"ms-python.mypy-type-checker",
"charliermarsh.ruff",
"ms-python.python",
"denoland.vscode-deno"
]
}
},
"remoteUser": "vscode"
}
settings.json でも deno.enable しておく
.vscode/settings.json
{
"deno.enable": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
},
"python.analysis.typeCheckingMode": "strict",
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
".*cache": true,
".venv": true,
"*.lock": true
}
}
Discussion