PowerToysを使おう

開発用のPCにWindowsを使っているのであれば、PowerToysを入れておくことを改めておすすめしたい。

learn.microsoft.com

Windows標準機能だけでは足りない部分や、便利な機能を足すツール。Microsoftが公式にリリースしています。

私がよく使っている機能だけ紹介しておきます。

Hosts ファイル エディター

hostsファイル のエディター。コメントアウトのつけ外しもできるし、らくちん。

カラーピッカー

画面上の色コードを取得するツール。

Always On Top

任意のウィンドウをショートカットキーで最前面表示にできる。

ここに書いたもの以外にも色々機能があるので、Windowsユーザーの人で使ったことなければ、ぜひ試してみてほしい。

Dockerでrunserverを起動する際にPIDを1にする

よくありがちなことなのですが、Dockerでアプリを動かす場合は、PID 1 のプロセスにしておいた方が良いという話です。

背景

Dockerコンテナ内でDjangoアプリを開発するときに、 migraterunserver を実行するために、以下のようなDockerfileを作成することがあります。

Dockerfile:

FROM python:3.13

# 環境変数設定
ENV PYTHONUNBUFFERED=1 \
    WORKDIR=/app

# 作業ディレクトリの作成
RUN mkdir -p $WORKDIR

# 作業ディレクトリの設定
WORKDIR $WORKDIR

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Djangoのmigrate実行後にrunserverを起動
CMD python manage.py migrate && python manage.py runserver

このDockerfileを使用してコンテナを起動すると、 migrate が実行された後に runserver が起動します。

これを以下のような compose.yaml で実行し、PIDの状態を確認してみます。

compose.yaml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - .:/app
docker compose up -d
docker compose exec app ps -ef

結果は以下のようになりました。

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  1 16:50 ?        00:00:00 /bin/sh -c python manage.py migrate && python manage.py runserver
root           8       1 18 16:50 ?        00:00:00 python manage.py runserver
root           9       8 41 16:50 ?        00:00:00 /usr/local/bin/python manage.py runserver
root          11       0 80 16:50 pts/0    00:00:00 ps -ef

さて、ここでPID 1 のプロセスは /bin/sh になっていて、shからrunserverを起動しています。 runserverを実行しているPythonのPIDは 8 です。runserverは内部でDjangoのアプリをスレッドで実行しているので、PID 9Pythonのプロセスです。

この状態で、 docker compose down を実行すると、コンテナが停止して破棄されるのですが、コマンドを実行してから停止するまでに時間がかかります。10秒ぐらい。

なぜか?

この時間がかかっているのは、PID 1にSIGTERMシグナルを送信してもプロセスが停止しないからです。 Docker composeのFAQに記載があります。

docs.docker.com

デフォルトではタイムアウトが10秒になっていて、10秒待ってもPID 1 のプロセスが終了しない場合は、強制終了されます。

少し時間が経った後に終了するのは、この強制終了によって終了されているからですね。

解決策

これをきれいに解決するには、 runserver をPID 1 で実行するとよいです。

Dockerfileを以下のように変更します。

FROM python:3.13

# 環境変数設定
ENV PYTHONUNBUFFERED=1 \
    WORKDIR=/app

# 作業ディレクトリの作成
RUN mkdir -p $WORKDIR

# 作業ディレクトリの設定
WORKDIR $WORKDIR

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Djangoのmigrate実行後にrunserverをexecで起動
CMD python manage.py migrate && exec python manage.py runserver

変更したのは最後のCMDの部分です。execコマンドでrunserverを実行するようにしました。 こうすると、PID 1 のプロセスがrunserverになります。

docker compose buildでイメージをビルドしなおし、composeを起動して、PIDを確認してみます。

docker compose build
docker compose up -d
docker compose exec app ps -ef

結果は以下のようになりました。

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0 20 17:10 ?        00:00:00 python manage.py runserver
root           8       1 67 17:10 ?        00:00:00 /usr/local/bin/python manage.py runserver
root          10       0 66 17:10 pts/0    00:00:00 ps -ef

PID 1 のプロセスが python manage.py runserver になっています。

これで、 docker compose down を実行すると、すぐにコンテナが停止、破棄されます。

まとめ

Dockerでアプリを動かすときは、PID 1 のプロセスにしておくと、コンテナの停止が早くなります。

exec コマンドを使うことで、起動したプロセスのPIDを起動元のPIDにできます。

補足

本番環境だとgunicornなどでアプリを起動することが多いですが、gunicornの場合も同様にしてPID 1 で起動しておかないと、graceful shutdownにならなかったりするので、注意が必要です。

参考

tokibito.hatenablog.com

Gunicornのシグナルハンドリングを試す

Python用のWSGIアプリケーションサーバーであるGunicornは、プロセス管理機能を持つマスタープロセスと、アプリケーションを動作させるワーカープロセスで構成されています。 親プロセス側となるマスタープロセスにシグナルを送ることでワーカープロセスを制御できます。

docs.gunicorn.org

シグナルはkillコマンドを使って送信できます。

Graceful Shutdownなどの挙動を見てみます。

試したPythonのバージョンは 3.13, Gunicornは23.0.0

WSGIアプリケーションを準備

main.py:

import time

def slow_response(data: bytes, interval: int) -> iter:
    """
    指定された時間だけスリープして1バイトずつデータを返すジェネレータ
    """
    for byte in data:
        time.sleep(interval)
        yield byte.to_bytes()

def application(environ, start_response):
    """
    WSGIアプリケーション
    """
    status = '200 OK'
    headers = [
        ('Content-Type', 'text/plain'),
    ]
    start_response(status, headers)
    return slow_response(b'Hello\n', 1)

このアプリケーションは、1バイトずつデータを返すジェネレータを使用しています。 「Hello」という文字列を1バイトずつレスポンスし、1バイト返すごとに1秒待機します。

Gunicornを起動

Gunicornを起動します。

gunicorn main

main.pyに application というWSGIアプリケーションが定義されているので、Gunicornはそれを起動します。

起動するとGunicornのログにはプロセスIDがログで表示されます。 kill コマンドでプロセスを終了できることを確認しておきます。

kill <プロセスID>

curlでリクエス

Gunicornが起動したら、curlでリクエストを送ります。

curl -N http://localhost:8000/

-N オプションは、バッファリングを無効にするオプションです。 これにより、サーバーからのレスポンスをリアルタイムで受け取ることができます。

SIGTERMによるGraceful Shutdownの挙動を確認

Gunicornは、SIGTERMシグナルを受け取ると、Graceful Shutdownを行います。

killコマンドでSIGTERMシグナルを送信して、Gunicornの挙動を確認します。

killコマンドは、明示的にシグナルを指定しない場合、デフォルトでSIGTERMシグナルを送信します。

linuxjm.sourceforge.io

curlでリクエストを送信した状態で、別のターミナルでkillコマンドを実行します。

GunicornはSIGTERMを受信したタイミングで、ログに「Handling signal: term」と表示しています。

しかし、curlのリクエストが完了するまで、Gunicornはプロセスを終了しません。

curlのリクエストが完了すると、Gunicornは「Worker exiting: (pid: <プロセスID)」と表示し、プロセスを終了します。

このように、GunicornはSIGTERMシグナルを受信した後、現在処理中のリクエストが完了するまでプロセスを終了しないことがわかります。このような動作をGraceful Shutdownと呼びます。

SIGQUITによる強制終了の挙動を確認

SIGQUITシグナルを送信すると、Gunicornは現在処理中のリクエストがあっても強制終了します。

kill -s SIGQUIT <プロセスID>

同様にcurlでリクエストを送信した状態で、別のターミナルでkillコマンドを実行します。

GunicornはSIGQUITを受信したタイミングで、ログに「Handling signal: quit」と表示しています。

curlのリクエストは強制終了されました。 リクエストを処理中のアプリケーションではSystemExit例外で強制終了されました。

SIGHUPによるワーカーの再起動の挙動を確認

SIGHUPシグナルを送信すると、ワーカーを再起動します。

kill -s SIGHUP <プロセスID>

SIGHUPシグナルでは、Gunicornは古いWorkerをGraceful Shutdownし、新しいWorkerを起動します。

SIGTTINとSIGTTOUによるワーカー数の増減を確認

SIGTTINシグナルを送信すると、Gunicornはワーカー数を増やします。

SIGTTOUシグナルを送信すると、Gunicornはワーカー数を減らします。

kill -s SIGTTIN <プロセスID>
kill -s SIGTTOU <プロセスID>

curlで並列リクエストを送信して、処理中にGunicornのワーカー数を増やしてみます。

curl -N http://localhost:8000/ & curl -N http://localhost:8000/ & wait

1つ目の結果はワーカーが1つの場合です。1つのリクエストが完了するまで、次のリクエストは待機します。

2つ目の結果は途中でSIGTTINシグナルを送信して、ワーカーを2つに増やした場合です。 途中から2つのリクエストが並列で処理されているのがわかります。

その後、SIGTTOUシグナルを送信して、ワーカーを1つに減らすのを試しました。

感想

Gunicornのシグナルハンドリングを試してみました。

シグナル操作でGunicornのワーカーを制御できるのは便利ですね。

適切にシャットダウンする操作は、アプリケーションの安定性のために知っておくと良さそうです。

コード

https://github.com/tokibito/sample_nullpobug/tree/main/python/gunicorn_signal

DjangoCongress JP 2025を開催しました

2025/2/22(土)にDjangoCongress JP 2025を開催しました。 djangocongress.jp

私はスタッフとして参加しました。

オンライン配信の会でした

今回は会場見つからずとか、スタッフのライフイベントとかで、時期をずらしてのオンライン配信のイベントとなりました。経緯などは hirokiky さんの記事にも書かれているので、ぜひ読んでもらえればと思います。

blog.hirokiky.org

DjangoCongressJPでオンライン配信のみの会は今回が初めてでしたが、OBSを使った配信に慣れた人が複数いたのもあり、トラブルなくできて本当によかったです。

配信アーカイブ

YouTubeライブで配信していたので、アーカイブがあります。

ROOM1

ROOM1の司会・進行はhirokikyさん

www.youtube.com

ROOM2

ROOM2の司会・進行は私(tokibito)でした。

www.youtube.com

アンケートのお願い

当日閲覧された方、アーカイブで見た方でも、ぜひイベント後アンケートへの回答をよろしくお願いします。

DjangoCongressJP 2025イベント後アンケート

フィードバックがあるとスタッフのやる気にもつながるので!ぜひ!

海外からの登壇者

今回は国内からのCfP応募が少なめでしたが、DSFの方がフォーラムで登壇者募集をアナウンスしてくれたりして、

  • 日本国内の登壇者7人
  • 海外からの登壇者7人

となりました。

登壇者のタイムゾーンUTC+0UTC+10 でしたので、海外の方はなるべく無理のない時間帯で登壇してもらったり、タイムテーブルを調整しました。

ライブでの字幕と同時通訳(翻訳)

配信を視ていただいた方はわかると思いますが、今回、ライブ配信でリアルタイムの文字起こしと翻訳(日本語→英語、英語→日本語)を配信画面に表示していました。

この文字起こしと翻訳部分は私が所属するObotAIで開発している Minutz というサービスを提供しました。

obot-ai.com

今回、海外からの登壇で英語発表が半分あるということもあり、どうしても日本の参加者が盛り上がるには言語の壁があって、何とかできるとといいなと思っていました。 会社で販売している製品の文字起こし精度が、たまたま最近大幅に改善したのもあり、hirokikyさんに無償提供を提案して試してもらったところ、満足いく精度で大丈夫そう、となったので採用してもらうことになりました。

私個人としても、英語のリスニングはまだまだなので、文字起こしと翻訳があることで、かなりリアルタイムで発表内容を理解することができました。 これはすごい体験だなと思いました。

また、海外の方(日本語がわからない方)が、日本語から翻訳された英語字幕で配信を視てくれていて、字幕・翻訳なしの従来の配信よりも良い体験ができたみたいで、これもすごいことだなと思いました。

オンラインの国際カンファレンス(国際学会)のようなことをできたので、すごくいい事例になったと思います。提案を受け入れていただき感謝しています。

Minutzの開発にもDjangoをたくさん使っている(サーバー側は全部DjangoDjango channels)ので、Djangoを使ったサービスでDjangoコミュニティに貢献できたのもうれしかったです。

おわりに

スタッフの皆さん、登壇者の皆さん、視聴者のみなさん、ありがとうございました。 予定は未定ですが、DjangoCongressJPは次回もあると思うので、その際には、ぜひCfPに応募してみてほしいです。

DjangoCongress JP 2025がオンラインで開催されます

2025/2/22(土)にDjangoCongress JP 2025が開催されます。 djangocongress.jp 今回はオンラインイベントで、登壇者の発表はYouTubeで配信されます。 参加は無料で、誰でも視聴可能です。ぜひご参加ください。

パブリックビューイング会場

東京では配信をプロジェクターで映して、みんなでワイワイ盛り上がりながら見れる会場が設置されます。 django.connpass.com イベント後はスタッフも合流して、有志で懇親会をやるので、参加できる方は是非どうぞ。

PythonのCUIアプリでrichを使って進捗状況表示

Pythonでターミナル(コマンドライン)から利用するCUIのアプリケーションを作る際、処理の進捗状況を表示するときに少しリッチにしたい。

richというライブラリを使うと簡単にできました。このライブラリは、PythonCUIアプリを作る際にリッチなUIを簡単に作れる機能を提供してくれます。

GitHub - Textualize/rich: Rich is a Python library for rich text and beautiful formatting in the terminal.

インストール

richはpipでインストールできます。

(venv)$ pip install rich

進捗状況表示

ドキュメントに進捗状況表示の実装方法についてページがあります。

Progress Display — Rich 13.6.0 documentation

ドキュメントのコードを参考に、簡単な計算処理の進捗状況表示を試してみます。

main.py:

import time
from rich.progress import track

values = list(range(10))  # 0~9 の数値のリストを用意
result = 0

for value in track(values, description="処理中..."):
    result += value  # 値を足し合わせ
    time.sleep(0.5)  # 意図的に遅延させる

print(result)  # 結果表示

実行結果

Cygwinからsshで接続したUbuntu Linuxbash上:

Windows11のPowerShell 7上:

クロスプラットフォームで問題なく動作しています。便利でした。

Django 5.2で追加される複合主キーサポートを試す

Django 5.2 alpha 1がリリースされています。 Djangoのalphaリリースは、まだ開発中の扱いです。alpha, beta, rc, stable(=無印) の順でだいたいリリースされます。

Django 5.2 alpha 1 released | Weblog | Django

Django 5.2のリリースノートを見ると、 "Composite Primary Keys" (複合主キー)というのがあり、オッ、と思ったので試していきたいと思います。

Django 5.2 release notes - UNDER DEVELOPMENT | Django documentation | Django

ドキュメントにトピックとしてページが追加されています。

Composite primary keys | Django documentation | Django

これまでDjangoのORMは複合主キーをサポートしていなかったため、単一の主キー用のフィールドを追加するとか、ワークアラウンドで何とかやってきましたが、対応が追加されるのはうれしいですね。

試した環境は Python 3.13, Django 5.2a1, MySQL 8.0 です。

Django 5.2a1はpipでPyPIからインストールできます。

(venv)$ pip install Django==5.2a1

複合主キーを持つモデルを作成

ドキュメントに記載のあった Product, Order, OrderLineItem で試してみます。

myapp/models.py:

from django.db import models

class Product(models.Model):
    """製品"""
    name = models.CharField("名称", max_length=100)


class Order(models.Model):
    """注文"""
    reference = models.CharField("注文番号", max_length=20, primary_key=True)


class OrderLineItem(models.Model):
    """注文明細"""
    pk = models.CompositePrimaryKey("product_id", "order_id")
    product = models.ForeignKey(Product, verbose_name="製品", on_delete=models.CASCADE)
    order = models.ForeignKey(Order, verbose_name="注文", on_delete=models.CASCADE)
    quantity = models.IntegerField(verbose_name="数量")

スキーマを確認

makemigrations コマンドで 0001_initial.pyマイグレーションファイルを作成後、 sqlmigrate コマンドでcreate tableの内容を見てみます。

$ python manage.py sqlmigrate myapp 0001
--
-- Create model Order
--
CREATE TABLE `myapp_order` (`reference` varchar(20) NOT NULL PRIMARY KEY);
--
-- Create model Product
--
CREATE TABLE `myapp_product` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(100) NOT NULL);
--
-- Create model OrderLineItem
--
CREATE TABLE `myapp_orderlineitem` (`quantity` integer NOT NULL, `order_id` varchar(20) NOT NULL, `product_id` bigint NOT NULL, PRIMARY KEY (`product_id`, `order_id`));
ALTER TABLE `myapp_orderlineitem` ADD CONSTRAINT `myapp_orderlineitem_order_id_8feda485_fk_myapp_order_reference` FOREIGN KEY (`order_id`) REFERENCES `myapp_order` (`reference`);
ALTER TABLE `myapp_orderlineitem` ADD CONSTRAINT `myapp_orderlineitem_product_id_f0c8cf26_fk_myapp_product_id` FOREIGN KEY (`product_id`) REFERENCES `myapp_product` (`id`);

myapp_orderlineitem テーブルは複合主キーで作られるようになっていますね。 migrate コマンドでテーブル作成後にMySQLのほうでも describe コマンドでスキーマ定義を見てみましょう。

mysql> describe myapp_orderlineitem;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| quantity   | int         | NO   |     | NULL    |       |
| order_id   | varchar(20) | NO   | PRI | NULL    |       |
| product_id | bigint      | NO   | PRI | NULL    |       |
+------------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

order_idproduct_id が主キーになっています。

ORMを試す

ドキュメントにあるサンプルコードをDjango shellで試してみます。

Django 5.2からは、Django shellを起動する際にモデルが自動インポートされた状態になります。 -v 2 オプションを指定すると、インポートされたモデルの詳細が表示されます。 django-extensionsの shell_plus にあった機能ですね。

$ python manage.py shell -v 2
9 objects imported automatically, including:

  from myapp.models import OrderLineItem, Order, Product
  from django.contrib.sessions.models import Session
  from django.contrib.contenttypes.models import ContentType
  from django.contrib.auth.models import User, Group, Permission
  from django.contrib.admin.models import LogEntry

Python 3.13.1 (main, Dec  4 2024, 08:54:14) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> Order
<class 'myapp.models.Order'>

ProductとOrderを1件作成し、作成したproductとorderを指定したOrderLineItemを1件作成します。

>>> product = Product.objects.create(name="apple")
>>> order = Order.objects.create(reference="A755H")
>>> item = OrderLineItem.objects.create(product=product, order=order, quantity=1)
>>> item.pk
(1, 'A755H')

複合主キーの pk はタプルになりました。

ドキュメント通りですが、 pk 引数をタプルで指定することもできるようです。

>>> item = OrderLineItem(pk=(2, "B142C"))
>>> item.pk
(2, 'B142C')
>>> item.product_id
2
>>> item.order_id
'B142C'

filterメソッドにpkを指定する際もタプルでの指定となるようです。

>>> OrderLineItem.objects.filter(pk=(1, "A755H"))
<QuerySet [<OrderLineItem: OrderLineItem object ((1, 'A755H'))>]>

ORMを試すのはこのぐらいにします。

Django admin

現時点では複合主キーのモデルは Django admin には非対応とドキュメントに記載があります。

試してみます。

myapp/admin.py:

from django.contrib import admin

from .models import Product, Order, OrderLineItem

admin.site.register(Product)
admin.site.register(Order)
admin.site.register(OrderLineItem)

checkを実行してみると、エラーになりました。

$ python manage.py check
# ... 中略
    admin.site.register(OrderLineItem)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/home/tokibito/sample_nullpobug/django/composite_pk/venv/lib/python3.13/site-packages/django/contrib/admin/sites.py", line 117, in register
    raise ImproperlyConfigured(
    ...<2 lines>...
    )
django.core.exceptions.ImproperlyConfigured: The model OrderLineItem has a composite primary key, so it cannot be registered with admin.

やはりダメなようです。

admin組み込みのワークアラウンドチャレンジ

ここからはワークアラウンドに少しチャレンジしてみます。

正式対応か、サードパーティのモジュールなどが出るのを待ったほうがよいですが、無理やりadminに組み込みを試します。

本番環境で使うべきではないコードです。参考程度に。

model._meta.is_composite_pk をチェックしてエラーを出しているので、無理やり外してみます。 is_composite_pk プロパティはreadonlyなので、クラス側から無理やり書き換えます。

myapp/admin.py(抜粋):

OrderLineItem._meta.__class__.is_composite_pk = False
admin.site.register(OrderLineItem)
OrderLineItem._meta.__class__.is_composite_pk = True

これで runserver はエラーがでずに起動できます。

一覧まではいけました。しかし、詳細ページはURLがタプルを文字列化したものになってしまっていたりして動かないです。

この辺はModelAdminやChangeListクラスをカスタマイズする必要がありそうですね。手間がかかるのでここまでにしておきます。

感想

業務システムなどで複合主キーが使われることがありますが、Djangoでは今まで扱づらかったです。

今回のDjangoの標準機能として複合主キーがサポートされるのはいいですね。

ドキュメントにはmodels.ForeignObjectを使って複合主キーに対してリレーションを設定する方法も記載されていました。

Django adminの対応は今後期待したいです。