StreamlitとPyScriptの連携を考える

## 概要

PyScript(とDOM)でUIを作るのはきついので、既存のブラウザ向けUI(Dashboard系)ライブラリと連携できないかと思い、ひとまず最も有名なStreamlitと連携してみようと考えました。

結論から言うとStreamlitとの連携は個人的に要件に合わなかったのと代替になりそうなライブラリを見つけたのでこれ以上何かすることはないですが、もったいないのでこれまでの悪あがきの結果を残しておきます。

## Streamlitの制約

Streamlitを動かすためのサーバーはStreamlit以外の用途の利用が厳しく制限されています、かつては画像ファイルをStreamlitのUIに表示するといったケースでさえ画像ファイルを返す別のサーバーを立てる必要があったほどです。これはさすがに改善されたようですが、相変わらずHTMLファイルは別サーバーからホストする必要があるなど基本的にそういうポリシーなので連携させようと思うとなかなか面倒です。

また、カスタムコンポーネント内でPyScriptを動かそうと思えば動かせるのでしょうが、ドキュメントによればどうやらカスタムコンポーネントではそのままでは単純に他のコンポーネントとの連携が取れないようになっているらしいのでだったらもう別サーバー立ててIFrame経由で表示ということでいいんじゃないかという結論に達しました。


## StreamlitとPyScript(IFrame)の値のやりとり

Streamlitコンポーネントの値をPyScriptに渡すのはそんなに難しくありません、結局IFrame経由なので、IFrameのURLにクエリ文字列をつけてIFrame内でロードされているPyScriptでパースするだけです。

```py
def Iframe(self, st, value=None, width=500, height=500, margin=20):
    json_str = json.dumps(value) if isinstance(value, dict) else "{}"
    query = f"?data={urllib.parse.quote(json_str)}&t={int(time.time())}"

    return st.components.v1.html(
        f'<iframe id="pyscript_iframe" src="http://localhost:{self.port}/{query}" width="{width}" height="{height}"></iframe>',
        width=width + margin,
        height=height + margin,
    )
```

どっちでもいいと思いますが、JSONに変換して渡すことにしました、またクエリの末尾に`t=[時間]`をつけていますが、これはキャッシュ対策です。キャッシュ制御するヘッダを出力するなら不要です。

```py
import js
import json

value_data = json.loads(js.decodeURIComponent(js.location.search).split("&")[0].split("=")[1])
```

読み込む側はJSONにするとループを回す必要がなく1行で読み込めます。

PyScript(IFrame)側からStreamlitコンポーネントに値を渡すためにはAPIを経由させるしかないようです。

## StreamlitとHTTPサーバーの連携

公開する際は個別にホストするしかないんですが、ローカルでデバッグするときのために簡易なHTTPサーバーをでっちあげてみました。

```py
from http.server import SimpleHTTPRequestHandler, HTTPServer
import threading
import json
import urllib.parse
import time


class PyScriptServer:
    def __init__(self, port):
        self.port = port

    @classmethod
    def run_threaded_server(cls, pys_port):
        server = HTTPServer(("", pys_port), SimpleHTTPRequestHandler)
        server.serve_forever()

    def run(self):
        pys_server = threading.Thread(
            target=PyScriptServer.run_threaded_server, args=(8080,), daemon=True
        )
        pys_server.start()

    def Iframe(self, st, value=None, width=500, height=500, margin=20):
        json_str = json.dumps(value) if isinstance(value, dict) else "{}"
        query = f"?data={urllib.parse.quote(json_str)}&t={int(time.time())}"

        return st.components.v1.html(
            f'<iframe id="pyscript_iframe" src="http://localhost:{self.port}/{query}" width="{width}" height="{height}"></iframe>',
            width=width + margin,
            height=height + margin,
        )
```

Streamlitで読み込むスクリプトの中で上のHTTPサーバーを呼び出すとあたかも連携してるっぽく感じなくもない雰囲気にはなります…？

```py
import streamlit as st
from pysserver import PyScriptServer


pys_server = PyScriptServer(port=8080)
pys_server.run()

st.title("埋め込みデモ")

st.write("以下にiframeで埋め込んだPyScriptのHTMLを表示します。")

st.write("スライダーを動かすと、PyScriptのIframeで表示されている値も変わります。")

value = st.slider("リロード確認用スライダー", min_value=0, max_value=100, value=50)
st.write(f"現在のスライダー値: {value}")

pys_server.Iframe(st, value={"value": value})
```

本格的にやろうとしたらこんな感じになります、めんどくさい。

https://github.com/uniras/PyScriptAndStreamlit/

こんな感じなので他に使えそうなライブラリがないか探してみたところ、マイナーそうだけど面白そうなライブラリがあったのでStreamlitとPyScriptの連携についてこれ以上探ることはおそらくもうないでしょう。