django-extensionsのrunscriptでスクリプトを実行する

django-extensionsには、スクリプトファイルを実行するためのrunscriptというコマンドが用意されている。

このコマンドを使うと、Djangoのコンテキスト(つまり、Djangoのsettings.pyが適用された、 django.setup() を実行済みの状態)で、Pythonスクリプトを実行できる。

RunScript — django-extensions 3.2.3 documentation

試したバージョンは、Python 3.10、Django 4.2、django-extensions 3.2.3。

scriptsフォルダにスクリプトファイルを用意する

django-extensionsをセットアップ済みのプロジェクトで作業する。

Djangoのプロジェクトディレクトリに、 scripts という名前のフォルダを作成し、スクリプトファイルはその中に作成する。

scriptsフォルダ内のスクリプトPythonモジュールとして再利用しないのであれば、scriptsフォルダには __init__.py を作成しなくてもよい。

今回作成したのは、 scripts/foo.py というファイル。

ディレクトリ構造とファイルの配置は次の通り。

.
├── db.sqlite3
├── manage.py
├── myproject
│     ├── __init__.py
│     ├── asgi.py
│     ├── settings.py
│     ├── urls.py
│     └── wsgi.py
└── scripts
    └── foo.py

scripts/foo.py:

from django.contrib.auth.models import Permission

def run(*args):
    """fooという名前のスクリプト
    """
    print(f"args: {args}")
    # ORMも使える
    print(Permission.objects.count())

今回作成したファイルでは、Permissionモデルのレコード件数を取得して画面に出力している。

このコードは、DjangoのORMが使われる。

関数名は run としておく。runscriptコマンドから渡される引数は、可変長引数で受け取れる。

runscriptコマンドでスクリプトを実行する

django-extensionsをセットアップ済みのプロジェクトでは、manage.pyコマンドのサブコマンドとして runscript コマンドが使えるようになっている。

python manage.py runscript <script_name> のように、スクリプトファイル名の .py を除いた名前で実行できる。

$ python manage.py runscript foo --script-args hoge fuga
args: ('hoge', 'fuga')
24

スクリプトには、 --script-args オプションでコマンドから追加パラメータの文字列を渡すことができる。スペース区切りで複数の値を指定できる。

実行結果では、ORMが動作して、正常にcount()の結果が表示されていることがわかる。

まとめ

  • django-extensionsには、 runscript コマンドがある
  • runscript コマンドでは、Djangoのコンテキストでスクリプトファイルを実行できる
  • Djangoのプロジェクトフォルダに scripts/<script name>.py のようなファイルでスクリプトを用意する
  • python manage.py runscript <script name> --script-args param1 param2 .. のように実行する

Djangoのcacheフレームワークで複数の値をまとめて取得、更新する

試したバージョンはPython3.10, Django 4.2.4, pymemcache 4.0.0

Djangoのキャッシュフレームワークを使うと、memcachedやRedisなどをキャッシュサーバーとして利用できる。 複数の値をキャッシュサーバーから読み書きする際、 cache.get() cache.set() を何度も呼ぶと、キャッシュサーバーとの通信のレイテンシーの影響を受けて遅くなりやすい。

同一ホスト内にキャッシュサーバーがいる場合はさほど問題にならず、クラウド上にデプロイしてサーバーを分けたときに問題に気づきやすい。

キャッシュ読み書きを複数回行う例

まず、 cache.set() cache.get() をキャッシュの読み書きする都度呼び出すコードを示す。

>>> from django.core.cache import cache
>>> cache.set("k1", "spam")
>>> cache.set("k2", "egg")
>>> cache.set("k3", "ham")
>>> cache.get("k1")
'spam'
>>> cache.get("k2")
'egg'
>>> cache.get("k3")
'ham'

このコードだと cache.set() を3回、 cache.get() を3回呼び出していて、たとえばキャッシュバックエンドがmemcachedの場合、サーバーとの通信が6回発生する。

仮に1回の通信に50ミリ秒かかるとすると、50ms * 6回=300ms程度かかってしまう。回数が増えると線形にパフォーマンスが劣化していく。

複数の値をまとめて読み書きする例

複数の値をまとめて読み書きする場合は、 cache.set_many()cache.get_many() を使う。

>>> from django.core.cache import cache
>>> cache.set_many({"k1": "spam", "k2": "egg", "k3": "ham"})
[]
>>> cache.get_many(["k1", "k2", "k3"])
{'k1': 'spam', 'k2': 'egg', 'k3': 'ham'}

このコードだと、memcachedの場合はサーバーとの通信は2回で済む。読み書きする値の数が増えても、レイテンシの影響は大きくならない。

複数の値の読み書きは、バックエンドクラスの実装にも依存するので、どの場合でもこのAPIを使うと速くなるとは言い切れないことに注意する。

キャッシュサーバーを使うときは、実装によっては「ローカルの開発時は速いのに本番環境ではレイテンシの影響で遅くなる」という可能性があることを考慮しておくとよい。

参考

ワイルドカードのサブドメイン名をnginxのルーティングに利用する

DNSのサービスで *.example.com のようにワイルドカードサブドメインを設定し、nginxでワイルドカード部分の文字列を見てルーティング先を切り換える、といった構成を行ったので、メモを残します。

ドメイン名の設定

ムームードメインムームDNSを利用していたので、CNAMEの設定でワイルドカード* の文字を使用できた。

*.test としたので、 *.test.example.com のようなワイルドカードの設定となり、 hoge.test.example.com などにマッチする。

nginxのVirtualHost設定

nginxでも server_name ディレクティブで、ワイルドカードの部分は * の文字を使用できる。

Module ngx_http_core_module

まずはすべてのホスト名にマッチする設定をする。

server {
  listen 80;
  root /home/vagrant/www;
  index index.html;
  server_name *.test.example.com;

  location / {
    try_files $uri $uri/ =404;
  }
}

example.com の部分は自分の利用したドメイン名に置き換える。 /home/vagrant/www には適当な動作確認用の index.html ファイルを作成しておく。

この設定で http://hoge.test.example.com/ にブラウザでアクセスすると、 index.html の内容が表示される。

正規表現サブドメインのホスト名部分を抜き出す

ドキュメントの通り、 server_name ディレクティブでは正規表現を使用できるので、 (?<server_host>.+) のようなパターンで一致した文字列を変数名で読めるようしてみる。

Module ngx_http_core_module

また、 if ディレクティブで抜き出したホスト名部分の文字列を使って分岐処理をしてみます。

server {
  listen 80;
  root /home/vagrant/www;
  index index.html;
  server_name ~^(?<server_host>.+).test.example.com$;

  location / {
    if ($server_host != "www") {
      add_header Content-Type text/plain;
      return 200 $server_host;
    }
    try_files $uri $uri/ =404;
  }
}

この設定では、 www.test.example.com 以外の場合は、ホスト名をレスポンスとして返しています。 www.test.example.com の場合は、 try_files ディレクティブにより、 index.html の内容が返されるようになります。

たとえば、 http://hoge.test.example.com/ にブラウザでアクセスした場合は、 hoge と表示されます。

ここまでできると、location以下でルーティングは自由にできるので、たとえばホスト名部分によって proxy_pass の先を変更したりもできます。

サブドメインごとに server ディレクティブを記述してルーティングすることもできますが、運用上この形のほうが嬉しい場合があったので、この形で今回はやってみました。

Let's Encryptでワイルドカード証明書を取得して設定する

ここまでの設定だとHTTPですが、Let's Encrypt(certbot)を使ってワイルドカード証明書を設定すると、HTTPSにすることもできます。

certbotコマンドで以下のようにワイルドカード証明書を取得します。

certbot certonly --server https://acme-v02.api.letsencrypt.org/directory --manual -d *.test.example.com --preferred-challenges dns

この場合、ドメインの所有確認のためにTXTレコードの追加が必要になります。画面に表示された指示にしたがって、TXTレコードをDNSに設定しておきます。

証明書を取得できたらnginxに設定します。HTTPの場合は強制でHTTPSにリダイレクトさせるような場合は以下のようになります。

server {
  listen 80;
  server_name *.test.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443;
  root /home/vagrant/www;
  index index.html;
  server_name ~^(?<server_host>.+).test.example.com$;

  ssl_certificate /etc/letsencrypt/live/test.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/test.example.com/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

  location / {
    if ($server_host != "www") {
      add_header Content-Type text/plain;
      return 200 $server_host;
    }
    try_files $uri $uri/ =404;
  }
}

参考

DjangoMeetupTokyo #11 と DjangoCongressJP 2023 CfP

8/6(日)に DjangoMeetupTokyo #11 を麹町でやりました。

DjangoCongressJP 2023 のCfPの応募のきっかけになれば、と思って開催した次第です。 久しぶりのDjangoのオフラインの会は楽しかったので、やってよかったです。

会場を提供は eLV の方からでした。ありがとうございます。

不定期でも、続けていけるといいなーと思います。

DjangoCongressJP 2023のトークは引き続き8/21(月)まで募集中です。

docs.google.com

早めに応募してもらえると、とても助かります!

Meetupの様子

togetter.com

Djangoで文字列をテンプレートとして使う

Djangoでテンプレートエンジンを使う場合、テンプレートはファイルとして用意する場合が多いですが、文字列を入力とすることもできます。

The Django template language: for Python programmers | Django documentation | Django

試したバージョンは、Python3.10, Django4.2.2

Templateクラスの使用例

django.template.Templatedjango.template.Context を利用します。

Templateクラスは抽象化されたテンプレート、Contextクラスはテンプレートに渡す変数などを格納した辞書に似たインターフェースのコンテキストです。

>>> from django.template import Template, Context
>>> template = Template("今日の日付: {% now 'Y-m-d' %}\nコンテキストで渡した値: {{ spam }}")
>>> context = Context({"spam": "egg"})
>>> template.render(context)
'今日の日付: 2023-07-02\nコンテキストで渡した値: egg'

template.render() の引数には Context クラスのインスタンスを指定する必要があります。dictを渡すとエラーになります。 Context クラスはコンストラクタに辞書を指定できます。

>>> template.render({"spam": "egg"})
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/vagrant/tmp/venv/lib/python3.10/site-packages/django/template/base.py", line 171, in render
    with context.render_context.push_state(self):
AttributeError: 'dict' object has no attribute 'render_context'

この template インスタンスのクラスは、 django.template.base.Template となっています。

>>> type(template)
<class 'django.template.base.Template'>

テンプレートエンジンを指定する場合

django.template.loader.engines: Templates | Django documentation

Djangoはテンプレートエンジンの切替に対応しており、複数のテンプレートエンジンを実行時に切り替えて使うこともできます。 こちらの場合は django.template.engines でエンジンを指定して利用可能です。

>>> from django.template import engines
>>> django_engine = engines["django"]
>>> template = django_engine.from_string("今日の日付: {% now 'Y-m-d' %}\nコンテキストで渡した値: {{ spam }}")
>>> template.render({"spam": "egg"})
'今日の日付: 2023-07-14\nコンテキストで渡した値: egg'

template.render() の引数にdictを渡す必要があります。 Context クラスのインスタンスを渡すとエラーになります。

>>> template.render(Context({"spam": "egg"}))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/vagrant/tmp/venv/lib/python3.10/site-packages/django/template/backends/django.py", line 57, in render
    context = make_context(
  File "/home/vagrant/tmp/venv/lib/python3.10/site-packages/django/template/context.py", line 278, in make_context
    raise TypeError(
TypeError: context must be a dict rather than Context.

こちらの template インスタンスのクラスは、 django.template.backends.django.Template となっています。

>>> type(template)
<class 'django.template.backends.django.Template'>

インタフェースの違いと、どちらを使うほうがよいか

Template: Templates | Django documentation

django.template.base.TemplateDjangoの初期のころからある実装です。こちらはコンパイルされたテンプレートオブジェクトを表しています。

テンプレートエンジン切替に対応した django.template.backends.django.TemplateDjango 1.8で実装されたものです。 django.template.Template の薄いラッパーであるとドキュメントに説明がありました。

ドキュメントの説明だと、現時点ではテンプレートのインスタンスget_templatefrom_string メソッドで取得する想定のようなので、上記に記載した方法だと後者のテンプレートエンジンを指定する方法にしておくのが無難そうです。

それぞれインターフェースに微妙な違いがあるので注意が必要です。