Sharepoint上にReactフロントエンドつきのPythonツールを展開できるWebテンプレートを作成しました。実際には、Amazon S3のような静的Webホスティングサービスを含む任意のサーバ上で使用できますので、ぜひお安いサーバを借りてご使用ください。
https://github.com/narumichiaki/react-pyscript-template
特別なサーバを用意することなく、プログラミングに詳しくないユーザにもPythonツールを展開できます。コンパイルの必要もありません。企業内などでのPythonツール展開の方法にお困りの方、ぜひどうぞ。
概要
Reactをフロントエンド、PyScriptをバックエンドとして採用しています。
- フロントエンドは
renderer.tsx
に記述します。React + TypeScriptで書くことができます。- Babel Standaloneでtsxを読み込んで実行しています。したがって、事前にコンパイルする必要はありません。
- バックエンドは
backend.py
に記述します。Pythonで書くことができます。- PyScriptはWebAssembly上で動くPython実行環境であり、ユーザのブラウザ上でPythonを実行できるようにします。つまり、ここに書いたPython製プログラムはユーザのブラウザ上で実行されます。
- つなぎの部分が
main.tsx
に記述されていますが、特に触る必要はありません。- 本Webテンプレートでは、PyScript製バックエンドをReact製フロントエンドからAPIで(
callAPI
関数で)呼び出せるように繋いであります。
- 本Webテンプレートでは、PyScript製バックエンドをReact製フロントエンドからAPIで(
本テンプレートのメリット・デメリット
メリット
- データ分析ライブラリが豊富なPythonをバックエンドとして使える。
- Pythonは使える人が多いので引き継ぎがしやすい。
- プログラミング環境・スキルがない人にもPython製ツールを提供できる。
- バージョンをツール作成側でコントロールできる。旧バージョンが各ユーザの手元に残らない。
- ユーザによる意図しない改変を避けられる。
- セキュリティへの配慮がそれほど必要ない(すべてユーザのブラウザ上で実行されるため)
- Sharepoint環境のみ: Sharepoint上に設置して動作させられる(後述)。
デメリット
- サーバ側でのデータ保持はできない。
- 秘匿性の高いデータ(API Keyなど)がユーザから隠せない。
- ユーザ側のPCに計算負荷がかかる。
Sharepoint上への設置
本テンプレートは、カスタムスクリプトが有効化されているSharepoint上に設置することができます。index.html
の拡張子を.aspx
に変更してアップロードしてください。
2024年11月中旬までに行われたSharepointのセキュリティ強化により、カスタムスクリプトの作成(aspxのアップロードを含む)は、管理者の事前許可を要するようになりました。本テンプレートのアップロードにも管理者の事前許可が必要です。ただし、本テンプレートでは機能の変更に際してindex.aspx
を変更する必要がないので、一度アップロードしてしまえば、その後の更新は好きなように行うことができます。
コード例
以下のコード例はVersion 1.3.0に基づきます。
render.tsx
import React, { JSX, StrictMode, useState } from "https://esm.sh/react@17.0"
import ReactDOM from "https://esm.sh/react-dom@17.0"
import { z } from "https://esm.sh/zod@3.23.8"
// バックエンドAPI
type CallApiFunction = (
path: string,
json_param: Record<string, any>,
response_schema?: z.ZodType<any, any, any>
) => Promise<Record<string, any>>
let callAPI: CallApiFunction
// API Responce Validators (バグ予防のためにAPIから受け取る値をチェックする)
// for "/log"
const LogMessageSchema = z.object({
message: z.string()
})
function App(): JSX.Element {
const [count, setCount] = useState<number>(0)
const handleClick = React.useCallback(() => {
setCount((prevCount) => {
const newCount = prevCount + 1
// レスポンスのバリデーションあり版
callAPI("/log", { message: newCount }, LogMessageSchema).then(response => {
console.info(`Response: ${response.message}`)
}).catch(error => {
console.error("ログの記録に失敗しました。", {error})
})
// レスポンスのバリデーションなし版
callAPI("/log_unsafe", { message: newCount }).then(response => {
console.info(`Response: ${response.message}`)
}).catch(error => {
console.error("ログの記録に失敗しました。", {error})
})
return newCount
})
}, [])
return (
<div>
<div>Count: {count}</div>
<button onClick={handleClick}>Increment</button>
</div>
)
}
function render(call_api: CallApiFunction) {
callAPI = call_api
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById('root') as HTMLElement
)
}
// renderをwindowに登録(必須)
(window as any).render = render
backend.py
from typing import Any
import sys
import logging
import js
from pydantic import BaseModel, field_validator
from backend_helper import APP, Response, api_endpoint
logging.basicConfig(stream = sys.stdout)
LOGGER: logging.Logger = logging.getLogger(__name__)
LOGGER.setLevel(logging.DEBUG)
### MODEL ###
### CONTROLLER ###
### API ###
# サンプル: パラメータのバリデーションあり
class Message(BaseModel):
message: str
@field_validator('message', mode = 'before')
@classmethod
def convert_to_string(cls, v):
return str(v)
@APP.route("/log", Message)
def log(message: Message):
response_message: str = f"/log: Recieved log message: {message.message}"
LOGGER.info(response_message)
return Response.ok({"message": response_message})
# サンプル: パラメータのバリデーションなし
@APP.route("/log_unsafe")
def log_unsafe(message: dict[str, str]):
response_message: str = f"/log_unsafe: Recieved log message: {message["message"]}"
LOGGER.info(response_message)
return Response.ok({"message": response_message})
# APIエンドポイントをwindowに登録(必須)
js.window.api_endpoint = api_endpoint
ライセンス
Apache License 2.0で公開しますので、ライセンスの範囲内でご自由にお使いください。使ったら報告いただけると、私がたいへん喜びます。