『現場で使えるDjango管理サイトのつくり方』を読みました

『現場で使えるDjango管理サイトのつくり方』 著者のakiyokoさんから献本いただきました。

Djangoの管理サイト(Django Admin)にフォーカスした内容の本です。 www.amazon.co.jp

対象の読者

この本はDjangoの管理サイトについての本なので、ある程度Djangoフレームワークの使い方を知っていることが前提となっています。

対象の読者はこんな感じでした。

オススメポイント

  • 管理サイトの基本的な操作方法、使い方の説明がある
  • ModelAdminの拡張方法について丁寧な説明がある
  • 業務の現場で必要になりそうな、よくあるカスタマイズのパターンが紹介されている
    • 管理サイトのラベルを変更する、入力画面のカスタマイズ、バリデーションなど
  • Actionの追加など、拡張方法について丁寧な説明がある
  • 管理サイトのデザインをカスタマイズする方法について丁寧な説明がある

Django管理サイトをカスタマイズする必要がありそうなら、先にこの本に目を通しておくとスムーズに作業できると思うのでオススメです。

私はDjangoの管理サイトを10年以上前からカスタマイズしてきているので構造を把握していますが、もし管理サイトのソースコードをこれから読み込むのであれば、本書を先に読んでおくと楽になるかなーと思いました。

1つのVMインスタンスに複数のアプリを置くような構成を考える

VPSなどの環境で、1つのVMインスタンスにたくさんDjangoアプリをホストしたいなー、という場合の構成を考えたりしてました。

自分で運用しやすい形にまとまったので、メモを残しておきます。

要件

  • 1台のVMで複数種類のDjangoアプリを動かす
  • VMのメモリは2~4GBぐらい(月2000~4000円以下ぐらいのVPS
  • OSはUbuntu
  • DBはMySQL
  • WebサーバーはNginx
  • HTTPSはLet's Encryptを使う
  • MySQLはWorkbenchとかですぐ見れるようにはしておきたい
  • メール送信はSendgridを使う(なのでローカルにSMTPサーバーは不要)

要するにサーバー費用を抑えつつ、アプリをいくつも動かしたい。

負荷が上がった場合は、ミドルウェアを別のものに変えなくても、他のインフラに容易に移設できるような感じにしておきたい。

構成

f:id:nullpobug:20200630224433p:plain
構成図

結局これで一旦落ち着きました。

  • Nginx
    • Docker化するとCertbotが面倒な感じだったので、ホスト側Ubuntuにインストールして利用
    • アプリとの通信はTCP使わずにUnixドメインソケット
  • MySQL
    • アプリごとにDocker化するとメモリ食い過ぎる
    • 運用時にすぐmysqlコマンドやmysqldumpを使いたかったのと、Workbenchでの接続も簡単にしたかったので、ホスト側Ubuntuにインストール
  • アプリ
    • Pythonの環境は分離したかったので、Dockerで動かす
    • アプリごとにdocker-compose.ymlを作って自動起動するように設定してる
    • gunicornはワーカープロセスを1にしてメモリ使用量減らす、代わりにワーカースレッド数を増やす
    • NginxからはUnixドメインソケットで接続するので、Gunicornはソケットファイルにバインド
    • MySQLへの接続はUnixドメインソケット経由
  • cron
    • 定期実行は docker-compose exec でアプリのmanage.pyのコマンドを起動する

以上。小さいDjangoアプリなら1台に10個ぐらいホストしても大丈夫だとおもう。

FlaskのBlueprintのフックポイント(before_requestとbefore_app_request)について

FlaskのBlueprintには before_requestbefore_app_request のフックポイントがあり、どう呼ばれるのか検証していた。

API — Flask Documentation (1.1.x)

ドキュメントを読んでも、実行される順番や条件がわかりづらい。

検証コード

Flaskのバージョンは1.1.2

app.py

from flask import Flask, Blueprint

# Flask==1.1.2
app = Flask(__name__)

bp1 = Blueprint('bp1', __name__)

@bp1.before_app_request
def bp1_before_app_request():
    """before_app_requestはすべてのリクエストでviewの前に呼ばれる
    """
    print("bp1 before_app_request")

@bp1.before_request
def bp1_before_request():
    """before_requestは対象のBlueprintへルーティングされるリクエストでviewの前に呼ばれる
    """
    print("bp1 before_request")

@bp1.route("/bp1")
def bp1_view():
    print("bp1 view")
    return "/bp1"

bp2 = Blueprint('bp2', __name__)

@bp2.before_app_request
def bp2_before_app_request():
    print("bp2 before_app_request")

@bp2.before_request
def bp2_before_request():
    print("bp2 before_request")

@bp2.route("/bp2")
def bp2_view():
    print("bp2 view")
    return "/bp2"


@app.before_request
def app_before_request1():
    """Flask.before_requestはすべてのリクエストでviewの前に呼ばれる
    """
    print("Flask before_request1")

app.register_blueprint(bp1)
app.register_blueprint(bp2)

@app.before_request
def app_before_request2():
    """登録順で実行されるので、こちらはbp2.before_app_requestよりも後になる
    """
    print("Flask before_request2")

app.run(host="0.0.0.0")

実行結果

  • Flask.before_request で登録した関数は、すべてのリクエストでviewの前に呼ばれる
  • Blueprint.before_app_request で登録した関数は、 Flask.before_request と同様にすべてのリクエストでviewの前に呼ばれる
    • Flask.before_request と同じ扱いで、登録した順番で実行される
  • Blueprint.before_request で登録した関数は、対象のBlueprintにルーティングされるリクエストでviewの前に呼ばれる
  • 内部的には、 Flask.before_request_funcs に登録されたものを呼び出しているようだ
$ python app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Flask before_request1  # /bp1へアクセスしたとき
bp1 before_app_request
bp2 before_app_request
Flask before_request2
bp1 before_request
bp1 view
127.0.0.1 - - [14/May/2020 01:34:13] "GET /bp1 HTTP/1.1" 200 -
Flask before_request1  # /bp2へアクセスしたとき
bp1 before_app_request
bp2 before_app_request
Flask before_request2
bp2 before_request
bp2 view
127.0.0.1 - - [14/May/2020 01:34:17] "GET /bp2 HTTP/1.1" 200 -

Pythonの入門者向け動画教材2020年4月開講分

2019年にJMOOCで公開していたPythonの入門者向け動画教材ですが、2020年の4月開講分が受講できるようになりました。

去年は1~4章まででしたが、2章分追加して6章構成になりました。追加部分はオブジェクト指向プログラミングについてです。

JMOOCというオンライン講座の1つとして公開されています。 www.fisdom.org

講座はFisdomというプラットフォームで配信されているため、利用には登録が必要ですが、受講料等はかかりません。無料です。

CMSコミュニケーションズの寺田さんと2人で半分ずつ分担して作成したものになります。

Pythonを学習するための手助けになればいいなと思います。

去年の分

python-socketioとFastAPIを組み合わせて動かす

python-socketioのsioインスタンスを操作するWebAPIを作りたくて調べていました。

試した環境はUbuntu 18.04、Python 3.8、python-socketio 4.4.0、python-engineio 3.11.2、FastAPI 0.52.0です。

python-socketioのASGIAppクラスに other_asgi_app という引数があり、ここにASGIアプリケーションを指定すれば、socketio以外のトラフィックを指定したASGIアプリケーションに流してくれるようです。

python-socketio.readthedocs.io

コード

server.py:

import socketio
from fastapi import FastAPI

# setup fastapi
app_fastapi = FastAPI()
# setup socketio
sio = socketio.AsyncServer(async_mode='asgi')
app_socketio = socketio.ASGIApp(sio, other_asgi_app=app_fastapi)


@app_fastapi.get("/")
async def index():
    """fastapiのAPI実装(socketioに関係ない)
    """
    return {"result": "Index"}


@app_fastapi.get("/ping/{sid}")
async def ping(sid: str):
    """指定されたsidにemitするエンドポイント
    """
    sio.start_background_task(
        sio.emit,
        "ping", {"message": "ping from server"}, room=sid)
    return {"result": "OK"}


@sio.event
async def connect(sid, environ):
    """socketioのconnectイベント
    """
    print('connect ', sid)


@sio.event
async def disconnect(sid):
    """socketioのdisconnectイベント
    """
    print('disconnect ', sid)

client.py:

import asyncio
import socketio

sio = socketio.AsyncClient(reconnection=False)


@sio.on('ping')
async def on_ping(data):
    print('on_ping: ', data)


@sio.event
async def connect():
    print('connected.')


@sio.event
async def disconnect():
    print('disconnected.')


async def main():
    await sio.connect('http://localhost:8000')
    await sio.wait()

asyncio.run(main())

requirements.txt:

fastapi
uvicorn
python-socketio
aiohttp

実行結果

server:

# uvicornでサーバーアプリケーションを実行
$ uvicorn server:app_socketio
INFO:     Server initialized for asgi.
INFO:     Started server process [14975]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     db0b541370c040bea135855bbd0384eb: Sending packet OPEN data {'sid': 'db0b541370c040bea135855bbd0384eb', 'upgrades': ['websocket'], 'pingTimeout': 60000, 'pingInterval': 25000}
connect  db0b541370c040bea135855bbd0384eb
INFO:     db0b541370c040bea135855bbd0384eb: Sending packet MESSAGE data 0
INFO:     127.0.0.1:50052 - "GET /socket.io/?transport=polling&EIO=3&t=1583778107.0980704 HTTP/1.1" 200 OK
INFO:     ('127.0.0.1', 50052) - "WebSocket /socket.io/" [accepted]
INFO:     db0b541370c040bea135855bbd0384eb: Received request to upgrade to websocket
INFO:     db0b541370c040bea135855bbd0384eb: Upgrade to websocket successful
INFO:     db0b541370c040bea135855bbd0384eb: Received packet PING data None
INFO:     db0b541370c040bea135855bbd0384eb: Sending packet PONG data None
INFO:     127.0.0.1:50054 - "GET /ping/db0b541370c040bea135855bbd0384eb HTTP/1.1" 200 OK
INFO:     emitting event "ping" to db0b541370c040bea135855bbd0384eb [/]
INFO:     db0b541370c040bea135855bbd0384eb: Sending packet MESSAGE data 2["ping",{"message":"ping from server"}]

client:

# サーバー起動後にクライアントを起動
$ python client.py
connected.
on_ping:  {'message': 'ping from server'}
disconnected.

API呼び出し(curl):

# sidを指定してcurlでAPI呼び出し
$ curl -w '\n' http://localhost:8000/ping/db0b541370c040bea135855bbd0384eb
{"result":"OK"}

Pythonについてインタビューされた記事がエンジニアHubに掲載されました

私へのインタビュー記事がエンジニアHubというメディアに掲載されています。 employment.en-japan.com Pythonの学習のためにどういったコードを読むと良いかなど話しました。

VagrantのUbuntu環境をアップグレードする

作業環境としてVagrant(VirtualBox)でUbuntu16.04を2017年ごろから使ってます。 そろそろ20.04も近くなってきたので、今更ですが既存の環境をUbuntu18.04に上げておこうかと思い、色々調べていたのでメモを残します。

使っているboxは ubuntu/xenial64ubuntu/bionic64

Vagrant box ubuntu/bionic64 - Vagrant Cloud

Q. Vagrantの仕組みで既存のUbuntu環境のメジャーアップデートはできるのか?

A.できない。Ubuntuなら通常通りdo-release-upgradeコマンドを使おう。新規にVMを作り直してもよいなら、そちらのほうが手軽。

Q. Vagrantfileのboxを変更したら既存のVMは使えない?

A.そのまま使える。boxの指定を ubuntu/xenial64 から ubuntu/bionic64 に変更しましたが、特に問題はなかった。

他に気をつけた点

  • アップグレードするとadd-apt-repositoryで追加したaptリポジトリが無効化されるので、 vagrant provision を再度実行すれば有効になるようにしとくのがよい。
  • ホスト名はアップグレードで変わらないので、気になるなら変更すればいい。
  • Vagrantfileに書いていたパッケージのインストールが、パッケージの名称変更により一部失敗していたので修正した。

実際にやった手順

  1. Vagrantfileの更新(boxの変更、パッケージ名の変更)
  2. VMsshで入って sudo do-release-upgrade
  3. ホスト名の変更 sudo hostnamectl set-hostname ubuntu-bionic
  4. sudo rm /etc/apt/sources.list.d/* (provisionでパッケージを入れ直すので)
  5. vagrant provision でaptのパッケージなど入れ直し。
  6. Pythonのvenvの作り直し