はじめに
この記事では、python をつかって簡単なToDoアプリを作成します。
内容は、FastAPI を使用したAPIの作成にのみ焦点を当てますので、フロントエンドに関してはノータッチでいきます。また、今回データベースを扱うORM(Object Relational Mapping)は SQLModel を使用していきます。さらにデータベースのスキーマ管理は Alembic を用います。
以下、使用している Python は Ver 3.10 で、必要なライブラリは次の通りです。
fastapi==0.95.1
sqlmodel==0.0.8
alembic==1.10.4
この記事に記載しているプログラム一式は次の GitHub で利用することができます。
ToDoアプリのREST API一覧とフォルダ構成
今回作成するToDoアプリのREST APIは以下のとおりとします。
HTTPメソッド | パス | 説明 | リクエスト | レスポンス |
---|---|---|---|---|
GET | /api/tasks | タスクを全て取得する | – | タスクの一覧 |
GET | /api/tasks/{id} | 特定idのタスクを取得する | – | タスク |
POST | /api/tasks | タスクを新しく登録する | タスクの内容 | 登録したタスク |
PATCH | /api/tasks/{id} | タスクを更新する | タスクの状態 | 更新したタスク |
DELETE | /api/tasks/{id} | タスクを削除する | – | – |
なるべくシンプルにするため、ユーザー認証等は考えないことにします。
上記のAPIを FastAPI と SQLModel で実装するにあたって、ソースコードのフォルダ構成は以下のようにしました。今回のようなシンプルなAPIではこのような細分化した構造をとる意義は薄いかもしれませんが、今後アプリケーションが複雑になっても耐えられる構造の練習として細かくフォルダを分割しています。
.
├── alembic.ini
├── cruds/
│ └── task.py
├── models/
│ └── task.py
├── routers/
│ └── task.py
├── migrations/
│ ├── env.py
│ └── script.py.mako
├── db.py
├── main.py
└── settings.py
フォルダそれぞれの役割としては、
models/
: データベースのスキーマを定義するcruds/
: データベースとのやりとりをおこなう関数を定義するrouters/
: RestAPI のパスオペレーション関数を定義する
また、詳しくは後述しますが、 alembic.ini
と migrations/
は Alembic によって自動生成します。
DBの接続とスキーマを定義する
今回はユーザー認証等は考えないので、ToDoアプリのテーブル構造は以下の task テーブルのみとします。
このテーブルのスキーマを models/task.py
に実装しましょう。
from typing import Optional
from sqlmodel import SQLModel, Field
class TaskBase(SQLModel):
title: str = Field(description='task name', nullable=False)
done: bool = Field(default=False, description='done flag', nullable=False)
class Task(TaskBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class TaskCreate(TaskBase):
pass
class TaskRead(TaskBase):
id: int
class TaskUpdate(SQLModel):
title: str
done: bool
task テーブルのスキーマは、class Task(TaskBase, table=True)
がそれに当たります。ここで、3つのクラス: TaskCreate
, TaskRead
, TaskUpdate
がありますが、これらはAPIの リクエスト/レスポンス の構造として定義しています。routers/task.py
にて使用します。
次に、DBとの接続部分を実装していきましょう。
まずは、DB接続情報を settings.py
に記載しておきます。今回は簡単のため SQLite を使用しました。
SQLITE_NAME = 'db.sqlite3'
DB_URI = f'sqlite:///{SQLITE_NAME}'
また、DBとのセッションを確立する関数を db.py
に定義します。
from collections.abc import Generator
from sqlmodel import create_engine, Session
import settings
engine = create_engine(settings.DB_URI, echo=True)
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
DBを操作する関数の実装
DBの操作(CRUDs)をおこなう関数を cruds/task.py
に実装します。DB操作の部分を routers/
のコードから切り離して実装することで、 routers/
のコードをよりシンプルにする狙いがあります。
冒頭で述べた、実装する RestAPI の機能を満足するように関数を定義しました。以下の表はその実装したい RestAPI の再掲です。
HTTPメソッド | パス | 説明 | リクエスト | レスポンス |
---|---|---|---|---|
GET | /api/tasks | タスクを全て取得する | – | タスクの一覧 |
GET | /api/tasks/{id} | 特定idのタスクを取得する | – | タスク |
POST | /api/tasks | タスクを新しく登録する | タスクの内容 | 登録したタスク |
PATCH | /api/tasks/{id} | タスクを更新する | タスクの状態 | 更新したタスク |
DELETE | /api/tasks/{id} | タスクを削除する | – | – |
from sqlmodel import select, Session
from models.task import Task, TaskCreate, TaskUpdate
def read_task(session: Session, task_id: int) -> Task | None:
task: Task | None = session.get(Task, task_id)
return task
def read_tasks(session: Session, done: bool | None = None) -> list[Task]:
query = select(Task)
if done is not None:
query = query.where(Task.done == done)
tasks: list[Task] = session.exec(query).all()
return tasks
def create_task(session: Session, task: TaskCreate) -> Task:
db_task: Task = Task.from_orm(task)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
def update_task(session: Session, db_task: Task, task: TaskUpdate) -> Task:
task_data = task.dict(exclude_unset=True)
for key, value in task_data.items():
setattr(db_task, key, value)
session.add(db_task)
session.commit()
session.refresh(db_task)
return db_task
def delete_task(session: Session, db_task: Task) -> None:
session.delete(db_task)
session.commit()
return
上記の関数の全てに 引数 session
が用意されています。ここには、db.py
で定義した 関数 get_session()
の値を渡します。
ルーターの実装
routers/task.py
にRestAPI のパスオペレーション関数を実装します。
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
import cruds.task as task_crud
from models.task import Task, TaskRead, TaskCreate, TaskUpdate
from db import get_session
router = APIRouter(
prefix='/tasks',
tags=['Task'],
responses={404: {'message': 'Not found'}})
@router.get('/', response_model=list[TaskRead])
def read_tasks(
*,
session: Session = Depends(get_session),
done: bool | None = Query(None)
) -> list[TaskRead]:
"""
Read tasks from the database, filtered by the 'done' flag.
Parameters
----------
done : bool | None, optional, **[query parameter]**\n
The value of the 'done' flag to filter tasks by, by default None.
Returns
-------
list[TaskRead]\n
The list of tasks.
"""
return task_crud.read_tasks(session=session, done=done)
@router.get('/{task_id}', response_model=TaskRead)
def read_task(
*,
session: Session = Depends(get_session),
task_id: int
) -> TaskRead:
"""
Read a task from the database by its ID.
Parameters
----------
task_id : int, **[path parameter]**\n
The ID of the task to read.
Returns
-------
TaskRead\n
The requested task if found, otherwise raises a 404 HTTPException.
"""
task: Task | None = task_crud.read_task(session=session, task_id=task_id)
if task is None:
raise HTTPException(status_code=404, detail='Task not found')
return task
@router.post('/', response_model=TaskCreate)
def create_task(
*,
session: Session = Depends(get_session),
task: TaskCreate
) -> TaskCreate:
"""
Create a new task in the database.
Parameters
----------
task : TaskCreate, **[body parameter]**\n
The task creation object.
Returns
-------
TaskCreate\n
The created task.
"""
created_task = task_crud.create_task(session=session, task=task)
return created_task
@router.patch('/{task_id}', response_model=TaskRead)
def update_task(
*,
session: Session = Depends(get_session),
task_id: int,
task: TaskUpdate
) -> TaskRead:
"""
Update a task in the database.
Parameters
----------
task_id : int, **[path parameter]**\n
The ID of the task to update.
task : TaskUpdate, **[body parameter]**\n
The update object containing the new task data.
Returns
-------
TaskRead\n
The updated task.
"""
org_task: Task | None = task_crud.read_task(session=session, task_id=task_id)
if org_task is None:
raise HTTPException(status_code=404, detail='Task not found')
return task_crud.update_task(session=session, db_task=org_task, task=task)
@router.delete('/{task_id}')
def delete_task(
*,
session: Session = Depends(get_session),
task_id: int
) -> dict[str, bool]:
"""
Delete a task from the database.
Parameters
----------
task_id : int, **[path parameter]**\n
The ID of the task to delete.
Returns
-------
dict[str, bool]\n
A dictionary with a single key 'ok' and a value of True, indicating successful deletion.
"""
task_to_delete: Task | None = task_crud.read_task(session=session, task_id=task_id)
if task_to_delete is None:
raise HTTPException(status_code=404, detail='Task not found')
task_crud.delete_task(session=session, db_task=task_to_delete)
return {'ok': True}
そして、main.py でアプリケーションを作成します。
from fastapi import FastAPI
from routers import task
app = FastAPI()
app.include_router(task.router)
DBを作成する
今回、データベースの作成は Alembic を用います。ターミナルで以下のコマンドを実行すると、データベース管理をおこなうファイル群が自動生成されます。
$ alembic init migrations
上記のコマンドで生成されるものは、以下になります。
. ├── alembic.ini └── migrations/ ├── README ├── env.py ├── script.py.mako └── versions/
次に、このマイグレーションファイルでSQLModelで定義したテーブルを作成してもらうために、env.py
と script.py.mako
を次のように微修正します。
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from sqlmodel import SQLModel
from settings import DB_URI
from models.task import Task
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
conf = config.get_section(config.config_ini_section, {})
conf['sqlalchemy.url'] = DB_URI
connectable = engine_from_config(
conf,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
これで、マイグレーションファイルの準備は完了です。
以下のコマンドを実行することで、models/task.py
で定義したスキーマのテーブルが作成されます。今回の場合は db.sqlite3
ファイルが作成されます。
$ alembic revision --autogenerate -m "init"
$ alembic upgrade head
ToDoアプリの Rest API を立ち上げる
実装が完了したので、ToDoアプリの Rest API を立ち上げてみましょう。次のコマンドを実行します。
$ uvicorn main:app --reload
## 出力 INFO: Will watch for changes in these directories: ['xxxxx/'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: Started reloader process [70402] using StatReload INFO: Started server process [70405] INFO: Waiting for application startup. INFO: Application startup complete.
これでAPIが立ち上がりました。試しに http://localhost:8000/docs にアクセスしてみましょう。FastAPI によって生成された APIドキュメントにアクセスすることができます。
この画面で実装した機能を実際に試すことができます。 例えば、POSTメソッドを試すとDBにタスクが新規登録されることが確認できます。
以上で ToDoアプリの簡単な Rest API を実装することができました。
まとめ
- Fast API と SQLModel で ToDoアプリの Rest API を実装した
- データベースのスキーマ管理は Alembic でおこなった
今回実装したToDoアプリのテストコードは次の記事▼で実装しています。