Site Overlay

簡単なWebサービスをPythonのみで作ってみた1

1)背景

最近pythonを勉強している中で、pythonのみで簡単なウェブアプリが作れるということで、勉強がてら作成してみました。使用した技術は

  1. フロント:Streamlit
  2. バック:FastAPI
  3. データベース:MongoDB

です。それぞれ少し説明します。

1-1)Streamlit

Streamlitはpythonのフレームワークで、html/css/javascriptを一切触ることなく画面をつくることができます。もちろん細かい見せ方の設定はできないものの、pythonを知っていればよいので、データサイエンス領域で広く使われています。
インストールの仕方含めStreamlitの詳細は以下を参照してみてください。

https://docs.streamlit.io/ (公式ページ)
https://camp.trainocate.co.jp/magazine/streamlit-web/

1-2)FastAPI

FastAPIはpythonのフレームワークであり、バックエンドで使われます。DjangoやFlaskに代表されるpythonのウェブフレームワークの中でもかなり高速な部類に入り、急速に広く使われて初めています。
FastApiについては、以下の記事が詳しいので、参考にしてみてください。

https://fastapi.tiangolo.com/ja/ (公式ページ)
https://kinsta.com/jp/blog/fastapi/

1-3)MongoDB

MongoDBですが、NoSQL型のドキュメント指向型データベースで、ビッグデータの処理などによく使われます。データはJSON形式で保存されているので、データの拡張などに強いです。
MongoDBについては以下が参考になります。

https://www.kagoya.jp/howto/it-glossary/develop/mongodb/

python上でMongoDBをあつかうにはpymongoというライブラリをインストールする必要があります。こちらについても詳しい記事がすでにありますので、詳細は例えば以下をご確認ください。

https://pymongo.readthedocs.io/en/stable/ (公式)
https://python-work.com/pymongo/

2)今回作成したアプリ

今回作成したアプリは、

ユーザー情報を画面から照会/登録/更新/削除する

というとてもシンプルなWebアプリです。認証機能も実装していません。

構成についてもかなりシンプルで、以下の通りです。

完成した画面は以下の通りです。

これらのキャプチャー以外も「update」や「deleteAll」のページもありますが、まぁ同じような画面なので、割愛します。

こうした画面をhtml/css/javascriptを一切使わずに作れるのはすごいですね。

3)実装コード Streamlit側

さて、ここからStreamlitで実装したコードについて、触れていきます。

なお、Streamlitのインストールなどについては、例えば 1)背景で紹介したページなどを参照してください。「Streamlit インストール」などでググればたくさん出てきますので、ここでは割愛します。

Streamlitを以下のコマンドで立ち上げます。

Streamlit run user_viewer.py

実行対象のモジュールuser_viewer.pyは以下です。

import json
import streamlit as st
from constants import *
from view_selector import ViewSelector


with open(Constants.prop_path, mode="r", encoding="UTF-8") as f:
    s = f.read()

menu_list = list(json.loads(s).keys())

st.title("Streamlit サンプル")

page = st.sidebar.selectbox('メニューを選択せよ', menu_list)

view_selector: ViewSelector = ViewSelector(page)
view_selector.perform_selected_logic()

class Constants():
    prop_path: str = "./config/menu.properties"
{  
    "home": "ViewHome", 
    "inquire": "ViewUser",
    "inquireAll": "ViewAllUsers",
    "register": "ViewRegister",
    "update": "ViewUpdate",
    "deleteAll": "ViewDeleteAll"
}

user.viewer.pyで実施していることは、menu.propertiesからmenu一覧をリストで取得します。これをst.sidebar.selectboxの引数に渡すことで、サイドバーからプルダウンでメニューを選べるようになります。

user.viewer.pyの最後の2行(view_selector)についてです。通常pageの値によって、画面を実装するコードを書いていくのですが、これだとpageが増えるたびに if & elifが増えることになります。if & elifが大量にあるのは保守性の観点があまり好ましくないので、今回ストラテジーパターンを使って、pageの値で画面の実装をスイッチさせるようにしました。

pageの値によってインスタンスを取得する処理が以下の「view_selector.py」になります。

import json
from constants import Constants
from view_strategy import IView


class ViewSelector():

    __module_name = 'view_strategy'
    __class_name_set: dict = {}

    def __init__(self, page) -> None:
        if not any(__class__.__class_name_set):
            with open(Constants.prop_path, mode="r", encoding="UTF-8") as f:
                s = f.read()
            __class__.__class_name_set = json.loads(s)

        self.strategy: IView = self.__get_instance(page)


    def __get_instance(self, page) -> IView:
        clazz_name = __class__.__class_name_set[page]
        module = __import__(__class__.__module_name, clazz_name)
        clazz = getattr(module, clazz_name)
        return clazz()


    def perform_selected_logic(self) -> None:
        self.strategy.show_screen()

ViewSelectorはストラテジーパターンにおける「Context」にあたります。ViewSelectorの最初のインスタンス化のときだけ、menu.propertiesを読み込み、クラス変数「__class_name_set」にセットします。そして、pageに紐づくクラス名を__class_name_setから取り出し、リフレクションを用いてpageに応じたインスタンスを取得してます。

リフレクションは動的にインスタンスを取得する技術ですが、コード上追えないやシングルトンクラスでも無理やりインスタンス生成できてしまうなど一長一短な気分もありますが、if文を使わずにpageの値に応じたインスタンスを取得したいため、今回はリフレクションを使ってます。

strategyを記述したモジュールが「view_strategy.py」になります。

from abc import ABCMeta, abstractmethod
import json
import requests
import streamlit as st
import pandas as pd


class IView(metaclass=ABCMeta):
    _url: str = 'http://localhost:8000/api/v2/users/'

    @abstractmethod
    def show_screen(self) -> None:
        pass


class ViewHome(IView):

    def show_screen(self) -> None:
        st.title("ホーム")


class ViewAllUsers(IView):

    def show_screen(self) -> None:
        st.title("全ユーザ照会")
        with st.form(key='inquireAll'):
            submit_bottun = st.form_submit_button(label='照会')
            if submit_bottun:
                res = requests.get(self._url)
                decorded_text = res.content.decode('utf-8')
                list_data= json.loads(decorded_text)                
                st.table(list_data)
                


class ViewUser(IView):

    def show_screen(self) -> None:
        st.title("ユーザ照会")
        with st.form(key='inquire'):
            seq_nbr: str = st.text_input('シーケンス番号',key='seq')
            submit_bottun = st.form_submit_button(label='照会')
            if submit_bottun:
                res = requests.get(self._url+seq_nbr)
                decorded_text = res.content.decode('utf-8')
                list_data= json.loads(decorded_text)
                if not isinstance(list_data, list):
                    list_data = [list_data]
                st.table(list_data)


class ViewRegister(IView):

    def show_screen(self) -> None:
        st.title("ユーザ登録")
        with st.form(key='register'):
            seq_nbr: str = st.text_input('シーケンス番号')
            first_name: str = st.text_input('First Name')
            last_name: str = st.text_input('Last Name')
            gender: str = st.radio(label='性別を選択せよ',
                                   options=('female', 'male'),
                                   index=0,
                                   horizontal=True,)
            roles: str = st.radio(label='ロールを選択せよ',
                                  options=('admin', 'user'))

            payload = {
                "sequence_nbr": seq_nbr,
                "first_name": first_name,
                "last_name": last_name,
                "gender": gender,
                "roles": roles
            }
            submit_bottun: bool = st.form_submit_button(label='登録')
            if submit_bottun:
                res = requests.post(url=self._url, json=json.dumps(payload))
                if res.status_code == 200:
                    st.success('登録完了')
                st.markdown(res.text)


class ViewUpdate(IView):

    def show_screen(self) -> None:
        st.title('ユーザ情報更新')
        with st.form(key='update'):
            seq_nbr: str = st.text_input('シーケンス番号')
            first_name: str = st.text_input('First Name')
            last_name: str = st.text_input('Last Name')

            payload = {
                "sequence_nbr": seq_nbr,
                "first_name": first_name,
                "last_name": last_name
            }
            submit_bottun: bool = st.form_submit_button(label='更新')
            if submit_bottun:
                res = requests.put(url=self._url+seq_nbr,
                                   json=json.dumps(payload))
                if res.status_code == 200:
                    st.success('更新完了')
                st.markdown(res.text)


class ViewDeleteAll(IView):

    def show_screen(self) -> None:
        st.title("全ユーザ削除")
        with st.form(key='deleteAll'):
            submit_bottun: bool = st.form_submit_button(label='削除')
            if submit_bottun:
                res = requests.delete(self._url)
                if res.status_code == 200:
                    st.success('削除完了')
                st.markdown(res.text)

デザインパターンのStrategyクラスがIViewクラスに相当し、ConcreteStrategyクラスたちがViewHomeやViewAllUsers、ViewRegisterに相当します。

ストラテジーパターンとリフレクションを使うことで、もし新しいページを追加したい場合、既存の実装に一切手を入れることなく、menu.propertiesへの定義追加とconcreteStrategyクラスを実装するだけで、実現することができます。

各ConcreteStrategyクラスのshow_screenメソッドがStreamlitで画面を表させる実装です。フォームを作るには「st.form」でつくり、テキスト入力欄はst.text_inputで簡単に作れます。ラジオボタンはst.radioで作ることができます。これらの配置はStreamlitが自動で設定してくれますが、原則記載順から表示されるようになってます。

ボタンは「st.form_submit_button」に相当し、フォーム上のボタンが作成されます。ボタンをクリックしたときの動作が「if submit_bottun」配下の処理で、ここにバックエンドにリクエストを投げレスポンスの整形を実装してます。

基本的にbackendとのやりとりはjson形式で行う方針で実装してます。jsonであれば仮にバック側が別のプログラミング言語であっても、大体の言語ではjson形式をいい感じに変換してくれるライブラリが用意されているためです。実際pythonではjsonライブラリやjson_utilライブラリなどが用意されています。

以上がStreamlit側のコード全容です。html/css/javascriptを一切触らずに画面を構築することができました。なお、「html/css/javascriptを一切触らずに」という意味は開発者が触らないという意味で、実際にhtml/css/javascriptを使っていないわけではありません。Streamlitがいい感じにhtml/css/javascriptに生成/変換している、という意味合いになります。

ちょっと長くなってきたので、、FastAPI側やMongoDBあたりは別の記事でふれたいと思います。

最後まで読んでいただきありがとうございます。
質問等はコメント欄かお問合せにてよろしくおねがいいたします。

簡単なWebサービスをPythonのみで作ってみた1」への5件のフィードバック

  1. marimo さんの発言:

    まさしく探していた内容でとても参考になります
    ただ記事を拝見する限りmodels.pyファイルの中身が記載されていないので、良ければソースを見せていただくことは可能でしょうか?

    1. コメントありがとうございます。
      ご指摘のとおりmodels.pyを記載してないですね。。。
      とりいそぎ以下に記載いたします。
      →すみません、読みづらいので削除しました。下記本編の方に追記してますので、そちらを参照ください。

      また簡単なWebサービスをPythonのみで作ってみたの本編にも追記しておきます。
      →こちらに追記いたしました。
      https://mathnatsnc.com/2023/03/07/%e7%b0%a1%e5%8d%98%e3%81%aaweb%e3%82%b5%e3%83%bc%e3%83%93%e3%82%b9%e3%82%92python%e3%81%ae%e3%81%bf%e3%81%a7%e4%bd%9c%e3%81%a3%e3%81%a6%e3%81%bf%e3%81%9f%ef%bc%92/

      よろしくおねがいいたします。

  2. Marimo さんの発言:

    上記のコメントに対応いただきありがとうございました
    (その1ではなくその2の方にコメントするべきでしたね。。。)
    無事こちらでも動作確認できました
    これからもっとWebサービスを使いこなせるよう勉強していきます😊

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です