1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Sharepoint上でPython製プログラムを動かすためのWebテンプレート

Last updated at Posted at 2024-10-23

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関数で)呼び出せるように繋いであります。

本テンプレートのメリット・デメリット

メリット

  • データ分析ライブラリが豊富な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で公開しますので、ライセンスの範囲内でご自由にお使いください。使ったら報告いただけると、私がたいへん喜びます。

参考資料

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?