👋

PostgreSQLとFastAPIとStreamlitを使ってTodoリストを作ろう(後編)

2023/03/19に公開

本記事の目的

最近、Todoリストの作り方を学んだので、知識を深化させるために言語化する。プログラミング初心者の筆者でも作れたので、プログラミング初心者のモチベーションを上げるためにも、できるだけ分かりやすく書いていく。今回は後編のフロントエンド編。前編のバックエンドはコチラ。
https://zenn.dev/tirimen/articles/7b5861c40e8a77

Streamlitとは

PythonでWebアプリケーションを作成するためのフレームワークの一つ。フロントエンドは正直デザイン性が大事なので、筆者は苦手な部類なのだが、Streamlitは短いプログラムそれなりに仕上げてくれる。個人でのデータ処理やお試しアプリ開発にはとても向いていると思う。また、多くの方が記事を書いている。欲しい機能がある時に「○○ streamlit」と調べれば色々な記事が出てくるが、まとめて基礎的なコードが纏められている下記のサイトは頻繁に頼らせていただいた。
https://zenn.dev/alivelimb/books/python-web-frontend
https://data-analytics.fun/category/data-visualization/streamlit入門/

手順1:登録フレームワークの作成

今回のTodoリストの要件定義だが、「Todoリストの新規登録機能」「未完了のTodoリストの表示機能」「既存のTodoを完了もしくは削除できる機能」「完了済みのTodoリストの表示機能」を目標に作る。まずは「Todoリストの新規登録機能」を行う。コードは下記の通り。

FRONTEND/front.py
import streamlit as st
import requests
import datetime

page=st.sidebar.selectbox('ページ名',['新規登録','未完了リスト','完了リスト'])

if page=='新規登録':
    st.title('新規登録')
    with st.form(key='新規登録'):
        deadline:datetime.date=st.date_input('期日').strftime('%Y-%m-%d')
        todo:str=st.text_input('Todo',max_chars=100)
        priority:int=st.number_input('優先度',0)
        genre:str=st.text_input('カテゴリ',max_chars=15)
        submit_button=st.form_submit_button(label='登録')

        if submit_button:
            url='http://127.0.0.1:8000/router?deadline=%s&todo=%s&priority=%s&genre=%s'\
                %(repr(deadline)[1:-1],repr(todo)[1:-1],repr(priority),repr(genre)[1:-1])
            res=requests.post(url=url)
            st.success('登録完了')

  • st.sidebar.selectbox~で何をやっているか
    下図の様に、左にプルダウン形式のサイドバーを作るためのコード。第一引数はサイドバー名、第二引数は各ページ名。リスト形式で書くことに注意。


  • with st.form~で何をやっているか
    枠を作り、そこに機能を付けていく。自分の環境ではformの引数keyは設定しないといけないが、他記事では無くてもいけるみたい。何故かは分からないので引き続き調査する。新規登録ということで、前編のバックエンドで登場したPOSTパスパラメータ関数を呼ぶ。各変数に型定義をし、deadlineはカレンダーウィジェットから入力するためのst.date_input()、todoとgenreはテキスト入力するためのst.text_input()、priorityは数字を整数で入力するためst.text_input()の第二引数を0としているを用いる。


  • submit button~で何をやっているか
    form文内には必ずst.form_submit_buttonを書き込まないとエラーが発生する。この文はその名の通りボタンであり、クリックされると変数(今回はsubmit button)にTrueを返す。そして、Trueが帰った時の挙動をif文以降で定義する。


  • if submit button~で何をやっているか
    パスパラメータを呼ぶ(HTTP通信みたいなこと)ために、バックエンドのパスパラメータを見ながら、urlを定義していく。レスポンスの文字列はシングルクォーテーションで囲まれてしまうので、文字型変数を代入する際は[1:-1]を付ける。そして、requestsモジュールを使いPOSTリクエストを行う。


  • st.success('登録完了'):で何をやっているか
    リクエストとレスポンスが共に成功した(res.status_code==200)時の挙動を表記している。備忘録のため、HTTPステータスコードの意味を下に記すが、新しく見つけたコードについても随時追記していく。
HTTPステータスコード
200:リクエストとレスポンスともに成功。
400:リクエストに関わるエラー。
422:主に入力値のエラー(バリデーションエラー)で表示されるが、この原因以外にも表示される場合があるので、注意。
500:サーバーの動きに関わるエラー。

手順2:未完了リストの表示と完了・削除機能の追加

「未完了のTodoリストの表示機能」「既存のTodoを完了もしくは削除できる機能」を次にやっていく。個人で使うのでデザインにこだわらない。とにかく試行回数を多くしたいので、必要機能をバックエンドのDELETEとPATCHパスパラメータを用いて、最低限実装していく。コードは上記のコードに追記してください。

FRONTEND/front.py
elif page=='未完了リスト':
    st.title('未完了リスト')
    url='http://127.0.0.1:8000/router'
    res=requests.get(url=url)
    records=res.json()
    i=0
    for record in records:
        i+=1
        if record.get('completed_flag')==False:
            with st.form(key=str(i)):
                st.subheader('・'+record.get('deadline'))
                st.write(record.get('todo'))
                submit_button_1=st.form_submit_button(label='完了')
                submit_button_2=st.form_submit_button(label='削除')
                if submit_button_1:
                    deadline:datetime.date=record.get('deadline')
                    todo:str=record.get('todo')
                    priority:int=record.get('priority')
                    genre:str=record.get('genre')
                    nowdate:datetime.date=record.get('nowdate')
                    url='http://127.0.0.1:8000/router?deadline=%s&todo=%s&priority=%s&genre=%s&nowdate=%s'\
                        %(repr(deadline)[1:-1],repr(todo)[1:-1],repr(priority),repr(genre)[1:-1],repr(nowdate)[1:-1])
                    res=requests.patch(url=url)
                    st.success('完了')

                if submit_button_2:
                    deadline:datetime.date=record.get('deadline')
                    todo:str=record.get('todo')
                    priority:int=record.get('priority')
                    genre:str=record.get('genre')
                    completed_flag:bool=record.get('completed_flag')
                    nowdate:datetime.date=record.get('nowdate')
                    url='http://127.0.0.1:8000/router?deadline=%s&todo=%s&priority=%s&genre=%s&completed_flag=%s&nowdate=%s'\
                        %(repr(deadline)[1:-1],repr(todo)[1:-1],repr(priority),repr(genre)[1:-1],repr(completed_flag),repr(nowdate)[1:-1])
                    res=requests.delete(url=url)
                    st.success('削除')

  • records=res.json()で何をやっているか
    変数resはjsonファイルであるので、json()でpython上にjsonファイルを読み込む。

https://qiita.com/shungiku/items/0e87fbd144319926475c



  • for record in records:~で何をやっているか
    今回は複数のデータが返ってくるので、FastAPIのレスポンスボディは、[{"・・・":"・・・","・・・":"・・・",・・・}・・・]と[]で囲まれている。そのため、recordsはリスト型(=リストの中に辞書)として認識される。そのため、for文で一つずつ取り出し、辞書検索が可能


  • with st.form(key=str(i)):で何をやっているか
    一つ一つに完了・削除ボタンを実装したいので、form文も一つ一つ作っていく。そのため、key(formの名前みたいなもの)も被りなく命名する必要がある?ので、str(i)を使っていく。
    ※もっと良いやり方がありそうなので、もう少し調査


  • record.get('??')で何をやっているか
    recordは辞書型なので、record['??']で良いんじゃないかと安直に考えていたが、これだとキーがない場合、エラーを吐き出してしまう。get関数だとキーが無くてもNoneを返してくれるので、こちらの方が良い。前編のos.environ.get()も同じ。

手順3:完了リストの表示

完了フラグを参照し、完了リストに割り振る。組み合わせるのはGETパスパラメータ。ほぼ手順2のコピペなので、コードだけ載せておく。以下のコードを上記と同じく追記し、コマンドでStreamlitを動かす。

FRONTEND/front.py
elif page=='完了リスト':
    st.title('完了リスト')
    url='http://127.0.0.1:8000/router'
    res=requests.get(url=url)
    records=res.json()
    for record in records:
        if record.get('completed_flag')==True:
            st.subheader('・'+record.get('deadline'))
            st.write(record.get('todo'))
cmd
streamlit run front.py

まとめ

非常に簡素だが、必要最低限のTodoリストは作れたと思う。genreやpriorityを定義しているので、もっと本格的にしたいが取り敢えずここまで。言語化することで意味を理解せず使っていたコードにも気づくことができたので、これからも不定期に記載していく。

Discussion