Pyodide上でdocutilsを動かしてみる

docutilsはreStructuredTextをPythonで扱うためのモジュール。

Pyodide上でdocutilsを動かせば、ブラウザ上だけでreStructuredTextを扱えるかなと思って試していました。

コード

pyodide-docutils.html:

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>docutils on Pyodide</title>
  </head>
  <body>
    <div class="container-fluid">
      <div class="row">
        <div class="col-md-6 vh-100 p-1">
          <form class="h-100">
            <textarea placeholder="You can write texts by reStructuredText." id="editarea" class="form-control h-100 font-monospace"></textarea>
          </form>
        </div>
        <div class="col-md-6 border border-2 h-auto" id="preview"></div>
      </div>
    </div>
    <script type="text/javascript">
      let pyodide;
      let previousText = "";
      let lock = false
      const editArea = document.getElementById("editarea");
      const preview = document.getElementById("preview");
      async function main() {
        // setup
        pyodide = await loadPyodide();
        await pyodide.loadPackage("micropip");
        const micropip = pyodide.pyimport("micropip");
        await micropip.install("docutils");
        // start interval
        setInterval(updatePreview, 500)
      }
      async function updatePreview() {
        const currentText = editarea.value;
        if (currentText == previousText || lock) { return }
        lock = true;
        await render(currentText);
        previousText = currentText;
        lock = false;
      }
      async function render(rst) {
        pyodide.globals.set("rst", rst)
        const output = pyodide.runPython(`
          from docutils.core import publish_parts
          try:
              result = publish_parts(rst, writer_name='html')['html_body']
          except Exception as e:
              print(e)
        `)
        preview.innerHTML = pyodide.globals.get("result");
      }
      main();
    </script>
  </body>
</html>

見た目はBootstrap5を使って簡単に整えつつ、テキストエリアに入力したreStructuredTextをdocutilsでHTMLに変換し、右側に表示をします。

特別なことは特になくて、普通に動きました。何かに使えると面白そうですね。

参考

PythonのcProfileモジュールを使ってプロファイル取得とSnakeVizによる可視化

先日のPyCon Kyushu Kagoshima 2024で登壇したcProfileの資料の内容から抜粋しつつ、cProifleの使い方について少し書いてみます。

tokibito.hatenablog.com

Pythonのプロファイルとは

  • Pythonの実行に関する統計情報
    • 関数がどれだけ呼ばれたか
    • 実行時間どのぐらいかかったか

Pythonでは標準モジュールのcProfileを使うと、プロファイルを取得できます。

プロファイルを取得する対象のコード

read_file.py:

import datetime
import timeit

DATA_FILE = "data.jsonl"


def main():
    """ファイルの行数をカウントするスクリプト"""
    print("start:", datetime.datetime.now())

    result = 0
    with open(DATA_FILE) as input_file:
        for line in input_file:
            result += 1

    print(f"result: {result} lines")

    print("end:", datetime.datetime.now())


if __name__ == "__main__":
    # 速すぎるのでtimeitで10回実行
    print(timeit.timeit(main, number=10))

プロファイルの取得

cProfileでプロファイルを取得します。

今回はスクリプト全体のプロファイルを取得するので、次のようにシェルから実行します。

$ python -m cProfile -o read_file.profile read_file.py

read_file.py を実行後、 read_file.profile にプロファイルデータが保存されます。

SnakeVizによる可視化

cProfileで取得したプロファイル情報は、標準モジュールのpstatsでテキストとして読むことができますが、SnakeVizを使うとウェブブラウザ上でグラフ表示、データのソートなどができて簡単なので、今回はこれを使ってみます。

jiffyclub.github.io

SnakeVizのインストール

$ pip install snakeviz

依存がTornadoのみなので、OSの追加パッケージなどが不要でハマりどころが少ないのが良かったです。

SnakeVizの実行

$ snakeviz read_file.profile

SnakeVizの画面(Chrome

実行すると、Tornadoによるウェブサーバーが起動し、可視化されたプロファイルをウェブブラウザで閲覧できます。

SnakeVizで可視化されたコールグラフ

pstatsと同様の呼び出し結果の一覧表示

一覧表示のほうはカラムのクリックでソートされるので、眺めるのが手軽だなと思いました。

PyCon Kyushu Kagoshima 2024に参加しました

2024/5/25にPyCon Kyushu Kagoshima 2024が鹿児島県で開催され、参加してきました。

kyushu.pycon.jp

私はPythonのプロファイラの使い方の紹介をしました。

speakerdeck.com

イベント参加者数が予想以上に多くて、賑わっていたように思います。

鹿児島はアーケードになっているエリアが広くて、路面電車もあり、中心街がかなり広い印象を受けました。 買い物にも遊ぶのにも困らなさそうで、興味深い~と思いながら散策してました。

機会があればまた行きたいです。

Djangoのプロジェクトにヘルスチェック用のエンドポイントを提供するdjango-health-check

ウェブアプリの外形監視をするときには、外形監視用のエンドポイントURLを用意することが多いと思います。

外形監視用のエンドポイントでは、アプリからデータベースなど各種ミドルウェアへの接続が正常であるかなどをチェックしますが、チェック項目はサービスによって異なることがほとんどです。

django-health-checkを使うと、汎用の外形監視用のエンドポイントをすぐに用意でき、設定によりチェック項目を変更できます。

django-health-check · PyPI

インストール

pip install django-health-check

試してみる

今回は health_check.db を使って、データベース接続のチェックを実施してみます。

アプリの有効化とURL追加

settings.py:

INSTALLED_APPS = [
    # ...
    'health_check',  # 必須
    'health_check.db',  # データベースのチェックをする場合に追加
]

urls.py:

from django.urls import path, include

urlpatterns = [
    # ...
    path('ht/', include('health_check.urls')),
]

DBマイグレーション

health_check.db はチェック用のテーブルを作成して操作するので、アプリを INSTALLED_APPS に追加したら、 migrate を実行します。

python manage.py migrate

実行する

runserverを起動して、 /ht/ にアクセスすると、チェック結果が表示されます。

デフォルトではHTMLで表示されます。

?format=json のようにクエリ文字列を指定すると、結果をJSON形式で返すこともできます。

何をチェックしているのか

health_check.db は実際にどのような操作をしてチェックしているのか、ソースコードを確認してみます。

https://github.com/revsys/django-health-check/blob/master/health_check/db/backends.py

  • TestModelというモデルのレコードを1件作成(=INSERT)
    • obj.save()
  • 作成したレコードを削除(=DELETE)
    • obj.delete()

このようなコードでした。「データベースに接続できること」「データベースに書き込みができること」をチェックしていますね。

データベース以外にもcache, storage, celery, boto, rabbitmq, redisなど、標準で用意されているバックエンドがあります。

カスタムのバックエンドを作成するのも簡単なので、使いやすそうです。

Djangoで動的にフォームクラスを作成する

Djangoのフォーム機能は通常、 django.forms.Form クラスを継承して定義をします。

フォームを使う | Django ドキュメント | Django

リクエスト時に動的にフォームクラスを生成したい場合もあるので、今回はそれをやってみます。

フォームクラスを動的に生成する関数

myapp/forms.py:

from django import forms


def create_form(form_schema_dict):
    """
    動的にフォームを作成する

    :form_schema_dict: フォームのスキーマを定義した辞書
    :return: フォームクラス

    例:
    form_class = create_form({
        "name": {"max_length": 100},
        "email": {"max_length": 100},
    })
    """
    form_schema = {}
    for field_name, options in form_schema_dict.items():
        form_schema[field_name] = forms.CharField(label=field_name, **options)
    return type("DynamicForm", (forms.Form,), form_schema)

この create_form 関数では、指定されたフィールド名とオプションでCharFieldを持つフォームクラスを生成して返します。

type関数は、Pythonの組み込み関数で、引数を3つ指定する場合は、新たな型を生成して返却します。

参考: 組み込み関数 - Python 3.12.3 ドキュメント

動作確認

Django shellからインポートして、フォームクラスが動作するか試します。

>>> from myapp import forms
>>> form_class = forms.create_form({
...     'name': {'max_length': 100},
...     'email': {'max_length': 100},
... })
>>> form = form_class()
>>> form
<DynamicForm bound=False, valid=Unknown, fields=(name;email)>

Djangoのビューで表示してみる

Djangoのビューから呼び出して、テンプレートファイル上でのレンダリングも試します。

myapp/views.py:

from django.shortcuts import render
from django import forms
from .forms import create_form


def index(request):
    form_class = create_form(
        {
            "name": {"max_length": 100},
            "email": {"max_length": 100},
            "message": {"widget": forms.Textarea},
        }
    )
    form = form_class()
    return render(request, "index.html", {"form": form})

このビュー関数では、単純にフォームを表示するだけにしていますが、やろうと思えば is_valid() を呼び出してフォーム内容を検証するようなコードにも変更できます。

myapp/templates/index.html:

<html>
  <head>
    <title>form test</title>
  </head>
  <body>
    <form>
      {{ form }}
    </form>
  </body>
</html>

サンプルコード全体は、GitHubに置いてます。

https://github.com/tokibito/sample_nullpobug/tree/main/django/dynamic-form

札幌で温泉合宿をやっていた 2024/04

4/19~4/21に札幌で合宿をやっていました。いつもの id:tomio2480 主催の会です。

前回: 札幌で温泉合宿をやっていた - 偏った言語信者の垂れ流し

合宿では、 Django Meetupの資料 を直しつつ、いつも通り飯を作っていました。

DjangoMeetupTokyo #13を開催しました

2024/4/13にDjangoMeetupTokyo #13を開催しました。

DjangoMeetupTokyo #13 - connpass

前回同様、もくもく会+ハンズオンの型式での開催をしました。

中級者向けハンズオン

前回同様、私が資料を準備して、中級者向けハンズオンをやりました。

DjangoMeetupTokyo #13 中級者向けハンズオン — django-meetup-tokyo-13 ドキュメント

今回はDjangoのユーザー認証と、ユーザーに紐づくデータの取り扱いを学ぶ内容でした。

資料の内容を順番に説明し、手元で動かしてもらいながら、Djangoの機能について詳しく見ていきました。

本日、資料の手順を再検証、うまく動かなかったところなどを修正、説明を追記しました。