🙆

fastapi + pydantic + devcontainer でサーバーを建てる

2024/06/05に公開

この記事は何

https://zenn.dev/mizchi/articles/setup-python-20240604

この記事は 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