[メモ]FastAPIの構造を理解する

【目標】

定義(pyproject.toml や models.py)から、メインのAPI処理(app/main.py)まで書いてみる。



【構成】

project/
├── pyproject.toml        # ライブラリの定義
├── alembic.ini           # DBマイグレーション設定
├── migrations/           # マイグレーションファイル(自動生成)
├── src/                  # 自作ライブラリ(コアロジック)
│   └── core/
│       ├── __init__.py
│       ├── models.py      # DBテーブル定義(静的な処理)
│       └── engine.py      # SP計算ロジック(動的な処理)
└── app/                  # FastAPI本体
    └── main.py

ライブラリの定義

project.toml

[build-system]
requires = ["setuptools>=61.0"] #setuptoolsのversion61.0を使う
build-backend = "setuptools.build_meta" #バッグエンド作業担当

[project]
name = "core"
version = "0.1.0"
description = "コアエンジン"
dependencies = [
    "fastapi",
    "uvicorn",
    "sqlalchemy",
    "alembic",
    "pydantic",
    "pydantic-settings"
]

[tool.setuptools.packages.find]
where = ["src"] # パッケージはsrc配下

DBの設計図

models.py

from sqlalchemy import Column,Integer,String,Float
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class Player(Base):
    __tablename__ = "players" # DB内でのテーブル名

    id = Column(Integer,primary_key=True,index=True)
    name = Column(String,unique=True)
    sp = Column(Float,default=100.0)
    level = Column(Integer,default=1)

ライブラリのインストール

pip install -e .

※pyproject.tomlと同じ階層にsrcが存在しないとエラーになる。


DBマイグレーションの自動化(Alembic) の準備

alembicの初期化

alembic init migrations

alembic.iniの修正

SQLiteを使ってmyproject.dbという名前のDBファイルを作る

;sqlalchemy.url = driver://user:pass@localhost/dbname
sqlalchemy.url = sqlite:///./myproject.db

env.py

import sys
from os.path import abspath,dirname
sys.path.insert(0,abspath(dirname(dirname(__file__)))) #パスを通す

# 自作ライブラリからBaseをインポート
from core.models import Base

#書き換える
# target_metadata = None
target_metadata = Base.metadata

__file__:今いるところ

dirname:そのファイルが入っているフォルダ

abspath:相対的な場所

sys.path:絶対的な場所


”--autogenerate”は、models.pyにはテーブルが設定されているのに

実際のデータベースにはまだないという差分を見つけ出します。

.bash

alembic revision --autogenerate -m "create player table"

migrations/versions/の中にテーブルを作る

マイグレーションに関する台本(スクリプト)は migrations というフォルダに入れる

alembic.ini

script_location = %(here)s/migrations

migrations/env.py

config = context.config

migrationsとversionsをくっつけている。

alembic.script.base.ScriptDirectory

https://github.com/sqlalchemy/alembic/blob/7b510dc52c7e931f393b6387f183bf888a08dee9/alembic/script/base.py

env.py 内の context オブジェクトに、保存先のフォルダ情報が渡される。

env.py

    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

設計図をDBに適用(=マイグレート)

.bash

alembic upgrade head

メインファイル作成

自作ライブラリ(core)をインポートして,DBと連携する。

app/main.py

from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from asagaya_core.models import Player  # 自作ライブラリから呼び出し!

# 1. DB接続の設定
SQLALCHEMY_DATABASE_URL = "sqlite:///./myproject.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

app = FastAPI()

# 2. DBセッションの管理(Djangoの connection みたいなもの)
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 3. エンドポイント(APIの窓口)
@app.get("/")
def read_root():
    return {"message": "ようこそ"}

@app.get("/players")
def get_players(db: Session = Depends(get_db)):
    # DBから全プレイヤーを取得
    players = db.query(Player).all()
    return players

動作確認(pyproject.toml配下で)

.bash

uvicorn app.main:app --reload

ブラウザ

http://127.0.0.1:8000/

ブラウザ

http://127.0.0.1:8000/players

データなし

データを追加したい

app/main.py 

from fastapi import Body 

#既存コードの下に追加
@app.post("/players")
def create_player(name: str = Body(...), db: Session = Depends(get_db)):
    new_player = Player(name=name, sp=100.0, level=1)
    db.add(new_player)
    db.commit()          
    db.refresh(new_player) # DBで発行されたIDなどを読み込み直す
    
    return {"message": "プレイヤーを登録しました", "player": new_player}

ブラウザ

http://127.0.0.1:8000/docs

こんな画面でた!

「Try it out」をクリック。

ブラウザ

http://127.0.0.1:8000/players

データが増えたよ

連続して入力するとどうなるかな。。。

422エラーでた。

中のデータが定義と合わないから、処理できないんだって。

1つずつデータを入れて再実施。


動的な処理を作る

engine.py

from .models import Player

class Engine:
    @staticmethod
    def walk_around(player:Player) -> Player:
        consumption = 5.0
        player.sp = max(0.0,player.sp - consumption)
        return player

    @staticmethod
    def eat_and_drink(player:Player) -> Player:
        recovery = 20.0
        player.sp = min(100.0,player.sp + recovery)
        return player

    @staticmethod
    def level_up_check(player:Player) -> bool:
        if player.sp >= 100.0:
            player.level += 1
            return True
        return False

main.py

from core.engine import Engine

#既存の処理の下に追加
@app.post("("/players/{player_id}/walk")")
def walk_around(player_id:int,db:Session = Depends(get_db)):
    player = db.query(Player).filter(Player.id == player_id).first()
    
    if not player:
        raise HTTPException(status_code=404,detail="プレイヤーが見つかりません")

    Engine.walk_around(player)

    db.commit()
    db.refresh(player)

    return {"message":f"{player.name}は体力を消耗した","current_sp":player.sp}

.bash

http://127.0.0.1:8000/docs

なんかできてる!

player_idを記載してExecuteボタンを押す

またまた422エラー出た。

Request URL「http://127.0.0.1:8000/players?player_id=1」とある。

パスパラメータで送りたいのに、クエリパラメータで送られてるなあ。

main.py

@app.post("/players/{player_id}/walk") #修正
def walk_around(player_id:int,db:Session = Depends(get_db)):

再実施

422エラーが消えた。良さそう。

.bash

http://127.0.0.1:8000/players?player_id=1

ちゃんと反映されてた。

残りの処理も追加。

engine.py

from .models import Player

# 既存の処理の下に追加
@app.post("/players/{player_id}/eat_and_drink")
def eat_and_drink(player_id:int,db:Session = Depends(get_db)):
    player = db.query(Player).filter(Player.id == player_id).first()
    if not player:
        raise HTTPException(status_code=404,detail="プレイヤーが見つかりません")

    Engine.eat_and_drink(player)

    db.commit()
    db.refresh(player)

    return {"message":f"{player.name}は回復した","current_sp":player.sp}


@app.get("/players/{player_id}/level_up_check")
def level_up_check(player_id: int, db: Session = Depends(get_db)):
    player = db.query(Player).filter(Player.id == player_id).first()
    if not player:
        raise HTTPException(status_code=404, detail="プレイヤーが見つかりません")
    
    is_leveled_up = Engine.level_up_check(player)
    
    if is_leveled_up:
        db.commit()
        db.refresh(player)
        return {"message": "レベルアップしました!", "new_level": player.level}
    
    return {"message": "まだレベルアップの条件を満たしていません", "current_level": player.level}

ブラウザで確認

http://127.0.0.1:8000/docs

なんかできてる。

Eat And Drink(回復)から動作確認。

player_id=1を設定して、Executeボタンを押下。

.bash

http://127.0.0.1:8000/players?player_id=1

player_id=1(阿佐ヶ谷太郎)のspは80→100に回復したよ。

level_up_check(レベル管理)も動作確認。

コード200(正常)が出てて、うまくいってそう。

.bash

http://127.0.0.1:8000/players/1/level_up_check

ちなみにpostをgetに変えると・・・

#@app.post("/players/{player_id}/level_up_check")
@app.get("/players/{player_id}/level_up_check")
def level_up_check(player_id: int, db: Session = Depends(get_db)):

エラーが出たよ。

レベル確認するだけの処理なのに、GETじゃなくてPOSTを使ってるのが原因っぽい。
GET:情報を見るだけ
POST:何かを変化させる

タイトルとURLをコピーしました