はじめに
この記事はNTTテクノクロス Advent Calendar 2023のシリーズ2の14日目です。
こんにちは、NTTテクノクロスの原です。社内向けのブログを運営しておりましたが、最近業務都合でなかなか記事をかけておらず久しぶりにブログを書いています。
私は普段、社内で他組織の開発チームの技術支援を担当しています。また、社内へ研修もチームで開催しています。
来年度から、Docker、Git、自動テスト、自動ビルド、CI/CD、アジャイル開発やモブプロなどベーススキルとして必要になる技術や開発手法を学ぶための研修を社内向けに開催する予定でした。
その中でまずは研修で手を動かしながら開発してもらうために、Pythonで簡単なAPIを作ってもらうような内容を考え、準備していく中で昨今話題のFastAPIを中心としたライブラリを利用したのでその紹介をできればと思います。
Pythonを選んだ理由としては、近年のAI・機械学習ブームによってPython利用者が増えてきたことが主な理由です。また、なるべく選定した言語に詳しい必要性を低くしたかったのでPythonを研修では選定しています。
初期構成
はじめに以下の構成で開発環境を整えました。
Python関連のライブラリの管理、パッケージングにはryeを用いました。
-
rye
- ライブラリの管理からPythonの仮想環境、ビルドなどのエコシステムを一括してコントロールできるようにしたものです。
-
FastAPI
- WebサーバでAPIを作るために特化したライブラリです。OpenAPIも一緒に作ってくれます。
-
pydantic
- Pythonのデータ検証と設定管理のためのライブラリで、型ヒントを用いてデータの自動検証とシリアライズを行う機能を提供します。
-
SQLAlchemy
- PythonのためのデータベースORM(オブジェクト関係マッピング)ライブラリです。
ryeは「It's not yet production ready」であり、所々でバグがあったりしましたが、今回は研修向けであるためで採用しました。
しかし、Pythonのパッケージ管理、仮想環境の各種ツールをそろえる必要がなくryeを起点として各ツールを実行できるというのはそれだけでも開発体験の良さに気がつけるので、一度は触れることをおすすめします。
初期構成の環境作成
研修ではライブラリの使用方法や言語がメインテーマではないので、実装が簡単なTODOアプリを作成することになりました。
Pythonの準備
ryeでテンプレートを生成します。
rye init todo-project
テンプレート作成後のディレクトリ構成は以下のような形になります。
.
├── .python-version
├── pyproject.toml
├── README.md
└── src
└── todo_project
└── __init__.py
次にPythonのバージョンを固定します。
これによって、プロジェクト直下の.python-version
ファイルにバージョンが書き込まれ、プロジェクトで共通的にバージョンを固定することが出来ます。
rye pin 3.10.11
初回の同期を実施します。初回の同期では先ほど固定したPythonのvirtualenvの仮想環境の作成や、requirements.lock
、requirements-dev.lock
が作成されます。また、.venv
ディレクトリはpyproject.toml
と同じディレクトリに作成されます。
rye sync
ライブラリのインストール
開発に必要な各ライブラリを追加して、syncすることでローカルに追加されます。
また、追加したライブラリのバージョンなどはpyproject.toml
に記載され、requirements.lock
、requirements-dev.lock
にも記録されます。
rye add fastapi uvicorn pydantic sqlalchemy psycopg2-binary
rye sync
Webサーバの起動
先ほど追加したuvicornを使って、Webサーバを起動することができます。
rye run uvicorn main:app --host 0.0.0.0 --reload
上記で起動すると、8000番ポートで起動するため、以下にアクセスすることでOpenAPIを確認することができます。
http://localhost:8000/docs
実装
中身のアプリに関しては以下のように実装していきました。
- router.py
- FastAPIのエンドポイントを指定する。主にControllerの役割。
- models.py
- モデル(ドメイン)を定義する。
- schemas.py
- モデルの制約を定義する。
- db.py
- データベースの関連情報を定義する。
from typing import List
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy import select, Session
from .models import Todo, TodoRequestForCreateOrUpdate, TodoResponse
from db import get_session
router = APIRouter()
# TODOのリストを取得します。
@router.get("/todos/", response_model=List[TodoResponse])
def read_list_todos(session: Session = Depends(get_session)):
return session.exec(select(Todo)).all()
# TODOを取得します。
@router.get("/todos/{todo_id}", response_model=TodoResponse)
def read_todos(todo_id: int, session: Session = Depends(get_session)):
return session.exec(select(Todo).where(Todo.id == todo_id)).one()
# TODOを新規登録します。
@router.post("/todos/", response_model=TodoResponse)
def create_todo(request: TodoRequestForCreateOrUpdate, session: Session = Depends(get_session)):
todo = Todo.from_orm(request)
session.add(todo)
session.commit()
session.refresh(todo)
return todo
# TODOを更新します。
@router.put("/todos/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, request: TodoRequestForCreateOrUpdate, session: Session = Depends(get_session)):
todo = session.get(Todo, todo_id)
if not todo:
raise HTTPException(status_code=404, detail="TODO not found")
for key, value in request.dict(exclude_unset=True).items():
setattr(todo, key, value)
session.add(todo)
session.commit()
session.refresh(todo)
return todo
# TODOを削除します。
@router.delete("/todos/{todo_id}")
def delete_todo(todo_id: int, session: Session = Depends(get_session)):
db_todo = session.get(Todo, todo_id)
if not db_todo:
raise HTTPException(status_code=404, detail="TODO not found")
session.delete(db_todo)
session.commit()
return {"ok": True}
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Todo(Base):
__tablename = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
is_done: Column(Bool)
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
class TodoBase(BaseModel):
id: Optional[int]
title: str
is_done: bool
created_at = datetime
class TodoCreate(TodoBase):
pass
class TodoUpdate(TodoBase):
title = Optional[str]
is_done = Optional[bool]
import os
from sqlmodel import create_engine, Session
DATABASE = os.environ.get('DATABASE', 'postgresql')
USER = os.environ.get('DB_USER', 'user')
PASSWORD = os.environ.get('DB_PASSWORD', 'secret')
HOST = os.environ.get('DB_HOST', 'localhost')
PORT = os.environ.get('DB_PORT', '5432')
DB_NAME = os.environ.get('DB_NAME', 'unknown')
DATABASE_URL = f'{DATABASE}://{USER}:{PASSWORD}@{HOST}:{PORT}/{DB_NAME}'
ECHO_LOG = False
engine = create_engine(DATABASE_URL, echo=ECHO_LOG)
def get_session():
with Session(engine) as session:
yield session
SQLModelを使った実装
実装を進めていく中で、FastAPIの作者が作成したSQLModelを発見しました。
これはpydanticやSQLAlchemyで重複するコードをシンプルに記述するために開発されたライブラリで、このライブラリを使って、実装を変えてみると以下の通りとなりました。
この記事を書いた当初はSQLModelのバージョンが低かったのですが、昨年度からリリースが1年近くストップしていたのですが、11月から無事リリースが続いています!今後の継続的なリリースにも期待したいですね。
- router.py
- sqlmodelのselect, Sessionの置き換えで大きくは変わっていません。
- models.py
- モデルの定義と制約を記述しています。
from sqlmodel import select, Session
from typing import Optional
from sqlmodel import SQLModel, Field
from pydantic import BaseModel
class Todo(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(index=True)
is_done: bool
class TodoRequestForCreateOrUpdate(BaseModel):
id: Optional[int]
title: str
is_done: bool
class TodoResponse(BaseModel):
id: int
title: str
is_done: bool
このようにモデルの定義とリクエストとレスポンスデータの検証できるようにすることで、シンプルにコードを記述することができるようになりました。
まとめ
スタートからしばらく経って、月中頃となりました。
たいへんではありましたが最後までご覧頂きありがとうございました!
あと、今回はSQLModelがそこまで使われておらず日本語記事も多くなかったので執筆しました。
早すぎるライブラリかもしれませんが、今後バージョンアップしたときに役立つかと思ったので、ぜひpydantic、SQLAlchemyを利用していた方はSQLmodelも触ってみて頂けるとよいかと思います。
どんどん明日以降も面白い記事がもりだくさん!ぜひNTTテクノクロスアドベントカレンダーをご覧ください!