8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Stliteでの静的Streamlit開発環境を、できるだけ普通のPython開発に近づける

Posted at

StreamlitはインタラクティブなWebアプリがPythonだけで作成できるライブラリです。

本稿で取り扱う Stlite は、ブラウザだけで動作するStreamlitです。

CPythonをWebAssembly/Emscriptenに移植したPyodideがあり、そのPyodideで動作するStreamlitがStliteです。GitHub Pagesのような静的サイトホスティングで動かしたりできます。

【Geminiに聞いてみた】Streamlitを、Pyorideを使ってStliteで静的サイトとして構築することに、どのようなメリットがありますか?

  1. サーバーコストの大幅な削減(またはゼロ化)
  2. スケーラビリティの向上
  3. セキュリティとプライバシーの確保
  4. インタラクションのレスポンス向上(ロード後)
  5. デプロイと管理の簡便さ

ただ、StliteはもちろんPyodideを使ってブラウザ上でPythonを動かすことになるので、通常のCPython開発とは色々と異なります。本稿では、Stliteの開発において以下のような点を実現する、「できるだけ普通のPython開発と近い」Stliteの開発環境の構築を目指します。

具体的には、Stliteの開発において以下を実現します。

  • Dev Containerを使って環境構築
  • uvを使ってPythonの依存関係管理
  • VS Code等のエディタ上でPythonの静的解析による支援

Dev Container 環境構築

まずDev Containerを使って環境構築します。ベースにはNode.jsのコンテナを使います。本稿では後からカスタマイズしやすいようにDockerfile(Containerfile)を利用しますが、devcontainer.jsonにコンテナ名とfeaturesを使ってやってもいいと思います。

.devcontainer/devcontainer.json
{
  "name": "開発コンテナ- stlite",
  "build": {
    "dockerfile": "../Containerfile"
  }
}
Containerfile
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm

Git管理もしておきましょう

$ git init .
$ wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/refs/heads/main/Node.gitignore

準備できたらコンテナをビルドをします。VS Codeだったらコマンドパレットで>Dev Containers: Reopen in Containerです。

Stliteアプリの立ち上げ

とりあえずStliteでWebアプリが動くようにします。本稿ではRspackを使って適当なWebアプリを構築します。Viteでも何でも使い慣れたツールがあればそれで良いと思います。

$ npm init -y
$ npm install --save-dev @rspack/cli @rspack/core
rspack.config.js
const path = require('path');
const rspack = require('@rspack/core');

module.exports = {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
  entry: './src/index.js',

  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'auto',
    clean: true,
  },

  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    hot: true, // ホットリロードを有効化
    watchFiles: ['src/**/*', 'public/**/*'],
  },

  plugins: [
    new rspack.HtmlRspackPlugin({
      template: './public/index.html',
    }),
  ],
};

package.json(抜粋)
  "scripts": {
    "dev": "rspack serve"
  },
public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>stlite App</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@stlite/mountable@latest/build/stlite.css"
    />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

HTMLテンプレートファイルには、Stlite用のCSSが入っている点だけが特徴です。あとは通常の静的Webアプリの開発とだいたい同じで、srcにJavaScriptが、publicに静的ファイルが入る設定です。

Stiteを呼び出すためのJSファイルを記述します。

src/index.js
import { mount } from "https://cdn.jsdelivr.net/npm/@stlite/browser@0.85.1/build/stlite.js";

async function startStlite() {
  mount(
    {
      entrypoint: "streamlit_app.py",
      files: { "streamlit_app.py": `
import streamlit

streamlit.title("stliteアプリ!")

name = streamlit.text_input("名前を入力してね")
if name:
    streamlit.write(f"Hello, {name}!")
`,
      },
    },
    document.getElementById("root")
  );
}

startStlite();

JavaScript内に文字列としてPython(Streamlit)のソースコードを埋め込んでいます。

ここまでできたら、 $ npm run dev で起動します。

image.png

これで、Stliteのアプリが起動するようになりました。
見た目はStreamlitですがブラウザだけで動作しており、アプリケーションサーバーを必要としません。静的サイトにビルドして配布したりGitHub Pagesのような静的サイトホスティングにデプロイできます。

Python開発環境を整備して開発しやすくする

これでStliteは動作したのですが、Python部分を文字列として埋め込んでいたら開発しづらいので、Pythonファイルを分離して.pyにします。

Pythonファイルはsrcに入れてrspackでバンドルするような形でも良いのですが、ここではpublicに入れて配信します。public/src_pyにPythonファイルを書いていきます。

public/src_py/index.py
import streamlit

streamlit.title("stliteアプリ!")

name = streamlit.text_input("名前を入力してね")
if name:
    streamlit.write(f"Hello, {name}")

index.jsからPythonスクリプトの文字列を削除し、Pythonファイルを読み込むための設定を入れます。

src/index.js
import { mount } from "https://cdn.jsdelivr.net/npm/@stlite/browser@0.85.1/build/stlite.js";

async function startStlite() {
  const FILE = "index.py";
  mount(
    {
      entrypoint: FILE,
      files: { [FILE]: { url: `./src_py/${FILE}` } }
    },
    document.getElementById("root"),
  );
}

startStlite();

これでnpm run dev すれば動作します。
しかしDev Containerで構築した開発環境にPythonランタイムが無いので、静的解析が使えなかったりと色々不便です。

まずコンテナにuvを入れるようにします。

Containerfile
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm

# Install uv
COPY --from=ghcr.io/astral-sh/uv:0.0.9.18 /uv /uvx /bin/

お好みでVS CodeのPython拡張機能も設定します。

devcontainer/devcontainer.json
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python"
      ]
    }
  }

コンテナをリビルドしてuvを使える状態にして、$ uv init --bareでpyproject.tomlを作成します。

index.pyを開くと、「streamlit」が解決できなくてエラーが出るようになると思います。つまりPythonの静的解析が利くようになっており、一歩前進です。

image.png

これは単純にStreamlitを依存関係に追加することで解決します。devグループに追加している理由は後述します。

$ uv add --dev streamlit

これでエラーが消えると思います。普通のPython開発と同じように、お好みでruffとかも追加できます。

やっていることの見た目は普通のPython開発なのですが、uv addしたパッケージは(devグループであるかどうかを問わず)あくまで開発環境上で開発体験の向上のためだけにインストールしているものであり、Stlite側のPyodide環境には一切影響しないという点に注意してください。

Stliteで利用するPyodide側Pythonの依存関係の管理

StreamlitではPythonの様々なパッケージを組み合わせて開発することがほとんどだと思います。たとえば以下のように、pandas, numpy, scikit-learnを追加したStliteアプリを構築してみます。

$ uv add pandas numpy scikit-learn
public/src_py/index.py
import streamlit
import pandas
import numpy
from sklearn import linear_model

# モデルの準備
df = pandas.DataFrame([
    {"身長": 160, "体重": 57},
    {"身長": 169, "体重": 61},
    {"身長": 180, "体重": 70},
])

reg = linear_model.LinearRegression()
reg.fit(df[["身長"]].to_numpy(), df["体重"].to_numpy())

# Streamlitアプリ
height = streamlit.number_input("身長を入力してね")

streamlit.write(reg.predict(numpy.array([[height]]))[0])

当然ですがこれだけではStliteは動作しません。Stlite側にpandas, numpy, scikit-learnを依存関係として追加して、Pyodideのmicropipでインストールする必要があります。

ここではuvのpyproject.tomlのdependenciesを変換してStlite側にインストールします。

まず、pyproject.tomlのdependenciesをrequirements.txtとして書き出します。

$ uv pip compile pyproject.toml -o public/src_py/requirements.txt

お好みで、依存関係をrequirements.txtとして書き出すスクリプトを「コンパイル」としてpackage.jsonのscriptに含めたりしてもいいと思います。

index.jsを以下のように書き換えて、書き出したrequirements.txtをパースして依存関係を読み込む処理を追加します。

src/index.js
import { mount } from "https://cdn.jsdelivr.net/npm/@stlite/browser@0.85.1/build/stlite.js";

async function startStlite() {
  const FILE = "index.py";

  const reqsText = await fetch('./src_py/requirements.txt').then(res => res.text());
  const requirements = reqsText
    .split('\n')
    .map(line => line.trim().split('==')[0])
    .filter(line => line && !line.startsWith('#'));

  mount(
    {
      entrypoint: FILE,
      files: { [FILE]: { url: `./src_py/${FILE}` } },
      requirements: requirements,
    },
    document.getElementById("root"),
  );
}

startStlite();

これで$ npm run devでStliteアプリを起動すると、動作します。

image.png

なお何でも依存関係追加できるわけではなく、Pyodideで利用できるPythonパッケージの制約を受ける点に注意してください。

まとめ

以下のような準備をすることで、Stliteでのアプリ開発を普通のPythonを用いたStreamlitの開発に近づけることができました。

  • Dev Containerでのコンテナ構築は、Node.jsのコンテナにuvを追加
  • pyprojectのdevグループにstreamlitを追加
  • uvで管理している依存関係をPyodideに追加

こうして作ったStliteのアプリは、Ruffとかを入れて開発体験を向上したり、GitHub Pagesのような静的サイトにデプロイしたりできます。

補足: 複数ページは注意が必要

Stliteは複数ページのアプリケーションに対応しています。

src/index.js(抜粋)
async function startStlite() {
  const FILES = [
    "streamlit_app.py",
    "pages/char_count.py",
  ];

  const files = FILES.reduce((acc, file) => {
    acc[file] = { url: `./src_py/${file}` };
    return acc;
  }, ({}));

  mount(
    { entrypoint: FILES[0], files },
    document.getElementById("root"),
  );
}

こうすると複数のPythonファイルを用いた複数ページを構築できますが、トップページ以外のURLはパーマネントURLとして直接アクセスできません。1ページにまとめるか、別のStliteアプリとして分割した方が便利なことが多いのではないかと思います。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?