【目標】
定義(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:何かを変化させる
