PostgreSQLとFastAPIとStreamlitを使ってTodoリストを作ろう(後編)
本記事の目的
最近、Todoリストの作り方を学んだので、知識を深化させるために言語化する。プログラミング初心者の筆者でも作れたので、プログラミング初心者のモチベーションを上げるためにも、できるだけ分かりやすく書いていく。今回は後編のフロントエンド編。前編のバックエンドはコチラ。
Streamlitとは
PythonでWebアプリケーションを作成するためのフレームワークの一つ。フロントエンドは正直デザイン性が大事なので、筆者は苦手な部類なのだが、Streamlitは短いプログラムでそれなりに仕上げてくれる。個人でのデータ処理やお試しアプリ開発にはとても向いていると思う。また、多くの方が記事を書いている。欲しい機能がある時に「○○ streamlit」と調べれば色々な記事が出てくるが、まとめて基礎的なコードが纏められている下記のサイトは頻繁に頼らせていただいた。
手順1:登録フレームワークの作成
今回のTodoリストの要件定義だが、「Todoリストの新規登録機能」「未完了のTodoリストの表示機能」「既存のTodoを完了もしくは削除できる機能」「完了済みのTodoリストの表示機能」を目標に作る。まずは「Todoリストの新規登録機能」を行う。コードは下記の通り。
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ステータスコードの意味を下に記すが、新しく見つけたコードについても随時追記していく。
200:リクエストとレスポンスともに成功。
400:リクエストに関わるエラー。
422:主に入力値のエラー(バリデーションエラー)で表示されるが、この原因以外にも表示される場合があるので、注意。
500:サーバーの動きに関わるエラー。
手順2:未完了リストの表示と完了・削除機能の追加
「未完了のTodoリストの表示機能」「既存のTodoを完了もしくは削除できる機能」を次にやっていく。個人で使うのでデザインにこだわらない。とにかく試行回数を多くしたいので、必要機能をバックエンドのDELETEとPATCHパスパラメータを用いて、最低限実装していく。コードは上記のコードに追記してください。
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ファイルを読み込む。
-
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を動かす。
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'))
streamlit run front.py
まとめ
非常に簡素だが、必要最低限のTodoリストは作れたと思う。genreやpriorityを定義しているので、もっと本格的にしたいが取り敢えずここまで。言語化することで意味を理解せず使っていたコードにも気づくことができたので、これからも不定期に記載していく。
Discussion