StreamlitとFastAPIを使ってメモアプリを作ってみた

## はじめに

今回Pythonのライブラリ`Streamlit`とWebフレームワーク`FastAPI`を使って簡単なメモアプリを作ってみました。即席でWebアプリケーションを作るのに有用かつ、お手軽に作成できたので紹介させてください。

### 目標
メモ登録・一覧画面を`Streamlit`、`FastAPI`を使用して作成していきます。
イメージとして下記2画面です。
**メモ登録画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/c96eba2c-4317-2d38-ddc5-19c342fb835c.png)
**メモ一覧画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/04749070-db46-e126-9eb4-7fe40df76bd1.png)


## Streamlit

[Streamlit](https://streamlit.io/)はデータサイエンス領域などで WebUI 付きのアプリケーションを手早く作るためのソフトウェアです。こちらを使ってフロントエンドのアプリケーションを作成していきます。

## FastAPI

[FastAPI](https://fastapi.tiangolo.com/ja/)は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで高速なWeb フレームワークです。

>- **高速**: **NodeJS** や **Go** 並みのとても高いパフォーマンス (Starlette と pydantic  のおかげです)。 [最も高速な Python フレームワークの一つです](https://fastapi.tiangolo.com/ja/#_10).
>- **高速なコーディング**: 開発速度を約 200%~300%向上させます。
>- **少ないバグ**: 開発者起因のヒューマンエラーを約 40％削減します。
>- **直感的**: 素晴らしいエディタのサポートや  デバッグ時間を削減します。
>- **簡単**: 簡単に利用、習得できるようにデザインされています。ドキュメントを読む時間を削減します。
>- **短い**: コードの重複を最小限にしています。各パラメータからの複数の機能。少ないバグ。
>- **堅牢性**: 自動対話ドキュメントを使用して、本番環境で使用できるコードを取得します。
>- **Standards-based**: API のオープンスタンダードに基づいており、完全に互換性があります: [OpenAPI](https://github.com/OAI/OpenAPI-Specification) (以前は Swagger として知られていました) , [JSON スキーマ](https://json-schema.org/)

`Flask`より使い勝手が良くて、`Django`よりも学習コストが少ない、ドキュメントを自動で作ってくれる良さげなイメージですね。

## 環境構築

### 使用したバージョン

- Python・・・`3.9.7`
- Streamlit・・・`1.4.0`
- FastAPI・・・`0.71.0`

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

まずはPythonがインストールされている環境でライブラリをインストールしていきましょう。`FastAPI`の実行環境として ASGI サーバーも必要になりますので、[uvicorn](https://www.uvicorn.org/)も併せてインストールします。

```bash
pip install streamlit fastapi uvicorn
```

### Streamlit動作確認

`front.py`に下記コードを記載していきましょう。

```python:front.py
import streamlit as st

st.title('Hello World')
```

`Hello World`を表示するのに必要なコードはこれだけです。コマンドを実行してサーバーを立ち上げてみましょう。

```bash
streamlit run front.py
```

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/50f306f3-7eb1-5346-cea0-30d79fc2b0d5.png)
`Hello World`が表示されたら成功です！
### FastAPI動作確認

`main.py`に下記コードを記載します。

```python:main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def get():
    return 'Hello World'
```

コードが書けたらこちらも実行してみましょう。

```
uvicorn main:app --reload
```

![Untitled 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/32c6fc95-09a1-3aee-8eca-5df5f97c6300.png)

`Hello World`が出力されたらOKです!
どちらとも動作確認出来たら本格的に実装を進めていきます。

## ディレクトリ構成


```bash
.
├── main.py          Streamlitメイン関数
├── memo.db          メモデータ用DB
└── memoback
   ├── __init__.py   
   ├── crud.py       CRUD処理を記載
   ├── database.py   DB接続処理を記載
   ├── main.py       バックエンドメイン関数
   ├── model.py      DBモデル定義
   └── schema.py     型情報
```

## バックエンド実装

まずはバックエンドから実装を進めていきます。

### DB準備

今回はお手軽さ重視で、DBは`SQLite`、O/Rマッパーは`SQLAlchemy`を採用して進めていきます。必要なモジュールをインストールします。

```bash
pip install sqlalchemy
```

モジュールをインストールしたらDBの設定を`database.py`に記載していきます。

```python:database.py
from sqlalchemy import create_engine, engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = 'sqlite:///./memo.db'

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
```

### モデル定義

メモアプリに必要なDB定義を`model.py`に記載していきます。
必要最小限のカラムとして、主キーに`memo_id`、内容の`content`だけテーブルに持ちます。

```python:model.py
from sqlalchemy import Column, Integer, String
from .database import Base

class Memo(Base):
    __tablename__ = 'memos'

    memo_id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    content = Column(String, index=True)
```

### pydanticによる型定義
`pydantic`はPython の型アノテーションを利用して、実行時における型ヒントを提供したり、データ検証時のエラー設定を簡単に提供してくれるためのライブラリです。（`FastAPI`インストール時に同時にインストールされています）
この`pydantic`を活用して、メモ参照時と登録時の定義を下記のように`schema.py`に記載していきます。

```python:schema.py
from pydantic import BaseModel, Field

# メモ参照定義
class MemoSchema (BaseModel):
    memo_id: int = Field()
    content: str = Field(max_length=100)

    class Config:
        orm_mode = True

# メモ登録定義
class MemoCreatingSchema (BaseModel):
    content: str = Field(max_length=100)
    
    class Config:
        orm_mode = True
```

### DB参照・登録

次に実際に`memos`テーブルに参照・登録するコードを`crud.py`に記載していきます。

```python:crud.py
from sqlalchemy.orm import Session
from . import model, schema

# メモ一覧取得
def get_memos(db: Session, skip: int = 0, limit: int = 100):
    return db.query(model.Memo).offset(skip).limit(limit).all()

# メモ登録
def create_memo(db: Session, memo: schema.MemoCreatingSchema):
    db_memo = model.Memo(content = memo.content)
    db.add(db_memo)
    db.commit()
    db.refresh(db_memo)
    return db_memo
```

関数`create_memo`の第2引数では`pydantic`で定義した型を指定することで、リクエストで渡ってきたmemoテーブルのmodelが適切かどうか検証することができます。
誤ったリクエストを送った場合は`StatusCode422(Unprocessable Entity)`が返却され、不備内容についても下記のような詳細が返却されます。

```json:エラーメッセージ
{
    "detail": [
        {
            "loc": [
                "body",
                "content"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}
```

### ルーティング

最後にルーティング処理を`main.py`に記載してバックエンドの実装は完了です。

```python:main.py
from typing import List
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session

from . import model, schema, crud
from .database import SessionLocal, engine

model.Base.metadata.create_all(bind=engine)

app = FastAPI()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# メモ参照
@app.get("/memos", response_model=List[schema.MemoSchema])
async def read_memos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    memos = crud.get_memos(db, skip=skip, limit=limit)
    return memos

#メモ登録
@app.post("/memos", response_model=schema.MemoSchema)
async def create_memo(memo: schema.MemoCreatingSchema, db: Session = Depends(get_db)):
    return crud.create_memo(db=db, memo=memo)
```
ここで注目なのが、`@app.get,@app.post`デコレータの第2引数です。`pydantic`を使った型情報を引数で渡すことでデータ検証が実行され、定義にそぐわないレスポンスを実装しようとした場合にエラーで気づくことができます。また自動ドキュメント作成、JSONレスポンスへの変換などにも第2引数の型情報が使われています。
第2引数を指定せずレスポンスを返す方法も[公式ドキュメント](https://fastapi.tiangolo.com/ja/advanced/response-directly/)に記載があるので参照いただければと思います。

### 自動ドキュメント作成・動作確認

これでバックエンドの実装は完了です！試しにサーバーを立ち上げて動作確認してみましょう！
`FastAPI`の機能である**自動ドキュメント作成**について紹介していきます。サーバー立ち上げ後に下記URLにアクセスすると自動でドキュメントが生成されたページを確認できます。

`http://{host名}/docs`

![Untitled 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/84a73ab0-1c2f-e42d-d307-a79437c2a674.png)

`Swagger UI`が表示され、実際にリクエストを送ってテストすることもできます。
今回は紹介だけに留めてメモ参照・登録のリクエストを実行していきます。

メモ登録(`POST:http://127.0.0.1:8000/memos`)
```json:Body
{
  "content": "testです"
}
```
```json:実行結果
{
  "memo_id": 12,
  "content": "testです"
}
```

メモ参照(`GET:http://127.0.0.1:8000/memos`)
```json:実行結果
[
    {
        "memo_id": 1,
        "content": "テストです"
    },
    {
        "memo_id": 2,
        "content": "テストです"
    },
    {
        "memo_id": 3,
        "content": "テストです"
    },
    {
        "memo_id": 4,
        "content": "メモを登録してみます！！"
    },
    {
        "memo_id": 5,
        "content": "次のメモを確認"
    },
    {
        "memo_id": 6,
        "content": "メモを登録してみます！！"
    },
	{
		"memo_id": 12,
		"content": "testです"
	},
]
```

## フロントエンド実装

`main.py`にフロントエンドの処理を記載していきます。今回はメモ登録・参照できる2画面を作成していきます。画面の切り替えは`Streamlit`の機能`sidebar`を使って実装していきます。
また、メモ登録結果のレスポンスを表示するために、`st.json(res.json())`で確認できるようにします。

```python:main.py
import streamlit as st
import requests
import json

page = st.sidebar.selectbox('Choose your page', ['registration', 'list'])

if page == 'registration':
    st.title('メモ登録画面')
    with st.form(key='registration'):
        content: str = st.text_input('メモ内容', max_chars=100)
        data = {
                'content': content
        }
        submit_button = st.form_submit_button(label='メモ登録')

        if submit_button:
            url = 'http://127.0.0.1:8000/memos'
            res = requests.post(
                url,
                data=json.dumps(data)
            )
            if res.status_code == 200:
                st.success('メモ登録完了')
            st.json(res.json())

elif page == 'list':
    st.title('メモ一覧画面')
    res = requests.get('http://127.0.0.1:8000/memos')
    records = res.json()
    for record in records:
        st.subheader('・' + record.get('content'))
```

### 動作確認

メモ登録・参照してみましょう！

**メモ登録画面**

![Untitled 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/a1541d0c-d17e-c065-1d53-21f886905378.png)

成功した場合、レスポンスを画面上で確認できます。

**メモ一覧画面**

![Untitled 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178305/cbd13e6d-81b4-7c1d-7576-e8702594a049.png)

メモ一覧および先ほど登録したメモを確認出来たら実装完了です！

## おわりに

`FastAPI`と`Streamlit`はいかがでしたでしょうか？どちらとも思った以上に簡単に処理を記載できるかと思います。簡単なWebサービスを作成する際に役立つフレームワークかと思いますので、この記事が少しでも参考になったら幸いです。