GitHubから自動でPyPIへパッケージ登録

久しぶりに公開しているパッケージ(django-compositepk-model)を更新する必要が出た。

色々新し方法に変わっているようで、パッケージ作成方法をhatchに変更し、GitHubからタグ付け時に自動でパッケージ作成し登録(公開)するようにした。

PyPIの管理画面から案内が出ていた新しいパッケージの作成方法については、以下ページ。

Python Packaging User Guide

日本語ページは以下。

https://packaging.python.org/ja/latest/guides/section-build-and-publish

色々書いてあるが、以下手順で行った。

1)pyproject.toml を書く

プロジェクト直下にpyproject.tomlファイルを作成する。説明通りに順に自分のプロジェクト用を作成した。

  • ビルドバックエンドはhatch用を設定
  • バージョンについては固定値にした※
  • 「実行可能なスクリプトを作成する」「先進的なプラグイン」は関係なし

※バージョンアップ時には、これを書き換えて、タグ付けを行う

自分のプロジェクト構成が特殊で、hatchの標準のディレクトリ構成に合っていないので、hatch用のターゲットファイル指定を追記した。

https://hatch.pypa.io/latest/config/build

今回のパッケージの例では、以下。

https://github.com/Arisophy/django-compositepk-model/blob/main/pyproject.toml

以前のパッケージと構成が合うように、以下を追加している。

[tool.hatch.build.targets.sdist]
include = [
  "compositepk-model/cpkmodel/*.py",
]

[tool.hatch.build.targets.wheel]
packages = ["cpkmodel/"]

[tool.hatch.build.targets.wheel.force-include]
"compositepk-model/cpkmodel" = "cpkmodel"

2)pyproject.tomlの確認

※本手順は、以下3-2)のGitHub上のワークフロー定義でbuildだけを動くようにして(タグを付けなければbuildのみ実行)、作成されたファイルをActionsの結果からダウンロードして確認でもOK

念のため、以前のパッケージと同じ構成でパーケージ作成されることを確認。

ローカルの開発環境で、hatchをインストールして、

pip install hatch

ビルドを行い

hatch build

pyproject.tomlの記述に問題がなく、正しい構成でパッケージが作成されることを確認した。

3)GitHub Actions CI/CD ワークフローを用いてパッケージ配布物のリリースを公開する

「TestPyPI へ公開」 と「配布パッケージに署名する」は必要ないので、以下を行う。

3-1)PyPI側でGitHubからの連携許可

https://pypi.org/manage/account/publishing/ で以下を登録。

  • PyPI Project Name:PyPIのプロジェクト名
  • オーナー:自分のGitHubアカウント
  • Repository name:GitHubのレポジトリ名
  • Workflow name:後で定義するので、適当な名前(release.yml等)

Environment nameは、空欄のままでよい。

3-2)GitHub側でワークフロー定義

.github/workflows/ ディレクトリに、上で定義したWorkflow nameのファイル名でワークフロー定義を作成する。

「配布パッケージに署名する」以下は無視で、基本そのままコピーして、以下のパッケージ名を変えれば良い。

 url: https://pypi.org/p/<package-name>  # Replace <package-name> with your PyPI project name

あとは、起動条件が単にpush時のままでは、ビルドは毎回動くので、以下を参考に条件を付けるとよい。

https://docs.github.com/ja/actions/using-workflows/triggering-a-workflow#using-filters-to-target-specific-branches-or-tags-for-push-events

今回のパッケージの例は以下。

https://github.com/Arisophy/django-compositepk-model/blob/main/.github/workflows/release.yml

Actionのバージョンは、古いとワーニングが出るので、現時点での最新に書き換えている。

4)実行

パッケージを更新する場合には、

  • pyproject.tomlファイルのバージョンを書き換え
  • タグ付けしてpush

すれば、自動でパッケージのバージョンアップ登録されるようになる。

[Django]サーバーの構築

VisualSdudioで開発していたDjango3.2のプロジェクトを、CentOSサーバーに公開する手順についてのメモ書き。

デプロイに関する3.2のドキュメントは以下。

https://docs.djangoproject.com/ja/3.2/howto/deployment/

今回は最終的にuWSGIでサーバーを構築する予定で、設定の流れは以下が参考になる。

https://uwsgi.readthedocs.io/en/latest/tutorials/Django_and_nginx.html

流れとしては、以下の順に順次確認して最終系にする。

  1. Django
  2. uWSGI –> Django
  3. nginx –> uWSGI –> Django
  4. WAF –> nginx –> uWSGI –> Django

まずはDjangoプロジェクトを設定して単体で確認する。

0.今回の条件

  • クライアント開発環境:Visual Studio Community 2022
  • Djangoバージョン:3.2
  • サーバー:CentOS Stream9
  • WAF:nginx+Naxsiサーバー
  • ソース管理:自前Gitサーバー

1.Djangoプロジェクト構築

1-1. Python

サーバーにインストールされているpythonのバージョンを確認。

python --version

今回のサーバーは3.9で、クライアント開発時と同じのためそのまま使うことにし、pyenvは使わないこととした

1-2. pip,git

dnf install pip
dnf install git

1-3. アプリ実行ユーザー作成

Django実行用のアプリユーザー(仮にappユーザー)を作成し、ユーザー変更する。

adduser app
su - app

1-4. Gitクライアント設定

gitサーバーは同じセグメント内の別サーバーにあり、鍵認証としている。

1)appユーザーで、鍵を作成する。

ssh-keygen -t rsa -b 4096

2)作成した公開鍵を、Gitサーバーに登録

作成された公開鍵(~/.ssh/id_rsa.pub)を、Gitサーバーのgitユーザーのauthorized_keysに登録する。

1-5. Djangoソース取得

Djangoプロジェクト用のディレクトリを作成(仮に~/django)して移動して、gitからソースを取得(クローン)する。

git clone ssh://git@10.10.10.1:9999/~/myapp.git

1-6. python実行環境

プロジェクトフォルダ配下に環境を構築し、必要なパッケージをインストール。

cd myapp
virtualenv env
cd env
source bin/activate
pip install -r ../requirements.txt

appユーザーの.bash_profileにvirtualenvのactivateを仕込んでおく。

1-7. Djangoの設定

設定をサーバー用に修正して、デプロイチェック。

https://docs.djangoproject.com/ja/3.2/howto/deployment/checklist/

まずはDEBUG=Flaseとする。SSL周りの設定については、WAFからの呼び出しのため後に設定。

ログファイルのディレクトリとしては以下を作成する。

mkdir /var/log/django
chown app:app /var/log/django

データベースについて、新規に作成した場合は、マイグレーションとスーパーユーザーを作成する。

https://docs.djangoproject.com/en/3.2/topics/migrations/

1-8. Django単体の確認

Django開発サーバーを立ち上げて、起動できることを確認する。

python manage.py runserver 0.0.0.0:8000

ブラウザで確認すると、DEBUG=Flaseとしているためstatic以下が取得できないが、この時点では無視して、後でnginx設定時に対応する。

2.uWSGI構築

2-1. uWSGIインストール

https://uwsgi.readthedocs.io/en/latest/Install.html

dnf groupinstall "Development Tools"
dnf install python-devel
pip install uwsgi

2-2. uWSGI設定

https://docs.djangoproject.com/ja/3.2/howto/deployment/wsgi/uwsgi/

起動用のuwsgi.iniファイルを/home/app/django/myapp/に作成する。設定例は以下。

[uwsgi]
chdir=/home/app/django/myapp/
module=myapp.wsgi:application
master=True
pidfile=/tmp/myapp-master.pid
vacuum=True
max-requests=5000
daemonize=/var/log/uwsgi/daemon-@(exec://date +%%Y-%%m-%%d).log 
http-socket = :8000
#socket=/usr/share/nginx/tmp/uwsgi.sock ※nginxとunixソケットでつなぐ場合

/var/log/uwsgiは以下で作成

mkdir /var/log/uwsgi
chown app:app /var/log/uwsgi

パラメータに関するドキュメントは以下。

https://uwsgi.readthedocs.io/en/latest/Options.html

2-3. 起動コマンド

uWSGIの起動コマンドstart_uwsgi.shを、/home/app/django/myapp/binに作成する。

#!/bin/sh
cd /home/app/django/myapp/
source env/bin/activate
env/bin/uwsgi --ini uwsgi.ini

起動コマンドに実行権を付与して実行し、正常に動作することを確認する。

2-4. uwsgiサービス登録

/usr/lib/systemd/systemに、サービス定義ファイルuwsgi.serviceを作成する。

[Unit]
Description=UWSGI
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
ExecStart=/home/app/django/myapp/bin/start_uwsgi.sh
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
User=app
Group=app

[Install]
WantedBy=multi-user.target

定義ファイルを作成したら、サービスの起動設定を行う。

systemctl enable uwsgi.service
systemctl start uwsgi.service

3.nginx構築

3.1 nginxインストール

dnf install nginx

3.2 static設定

https://docs.djangoproject.com/en/3.2/howto/static-files/#deployment

1)ディレクトリ作成

staticファイル配置用のディレクトリを作成する。nginxから参照できる場所として以下とし、appユーザーからもアクセスできるようにする。

mkdir /usr/share/nginx/static
chmod 777 /usr/share/nginx/static

2)Django設定

Djangoプロジェクトのsetting.pyのSTATIC_ROOTに、上記ディレクトリを設定

STATIC_ROOT = '/usr/share/nginx/static/'

3)ファイル配置

appユーザーにて以下を実行する。

python manage.py collectstatic

3.3 設定

https://uwsgi.readthedocs.io/en/latest/Nginx.html

nginxの設定ファイルに以下を追加。

location / {
    include    uwsgi_params;
    uwsgi_pass 127.0.0.1:8000;
    #uwsgi_pass unix:/usr/share/nginx/tmp/uwsgi.sock; ※unixソケットの場合
}

location /static {
    alias /usr/share/nginx/static;
}

※unixソケットを使う場合はこちらを有効にして、uWSGI側の設定もこれに合わせる。

3.4 起動設定

設定ファイルが問題ないことを確認して、nginxサービスを起動する。

nginx -t
systemctl start nginx

httpでアクセスして正常に表示できることを確認する。

4.WAF設定

4.1 WAFサーバー設定

WAFサーバー経由での公開設定を行い、正式なSSL証明書を取得する。

4.2 DjangoのSSL設定

Djangoのデプロイチェックで保留していたSSL周りの設定を変更する。SECURE_PROXY_SSL_HEADERを設定する場合は、nginx側で、X-Forwarded-Proto ヘッダーを付与するように設定する。

https://docs.djangoproject.com/ja/3.2/ref/settings/#std:setting-SECURE_PROXY_SSL_HEADER

具体的には以下を追加。

proxy_set_header    X-Forwarded-Proto       $scheme;

4.3 Naxsi設定

最初はNAXSIを学習モードにして、ルール調整して、最終的にfail2banを有効にする。

[Django]複合主キー

0.Djangoは複合主キーをサポートしている?

https://docs.djangoproject.com/ja/3.1/faq/models/#do-django-models-support-multiple-column-primary-keys

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys

Djangoでは複合主キーのサポートは公式にはされていない。※対応するパッケージとして以下を提供済み

「primary_key=True」が指定されているフィールドが無い場合は、自動で「id」が主キーとして追加されて動作するようなので、従来システムのDBを使っている場合、モデルを使った更新、削除が、このままでは動作しない。テーブルにidフィールドを追加するか、独自のクエリで処理する必要がある。

1.解決方法

案1:サロゲートキーを追加する

サロゲートキー(“id”)をDBに追加して、複合キーには一意制約(unique_together)を設定すれば、Djangoのモデルがそのまま使える。

(問題)DBの再構築に時間がかかり、計画停止が必要。

案2:独自クエリ―にて実装する

ORMに頼らずに、自分でクエリ―を書く実装にする。クエリの性能面等を自分でコントロールできる。

(問題)コードとテストが増える。

案3:Modelを拡張する

Djangoプロジェクトでも以前に検討されたようだが、最近はどうなっているかは、よく分からない。

https://code.djangoproject.com/ticket/373

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys#CurrentStatus

色々、挑戦した人もいるみたいだが、結局、どれも未完か?

とりあえず、自分でモデルを拡張することで、自分の仕事で使えるように、Modelのサブクラスを作成する。とりあえずは、Djangoのすべての機能をカバーする必要はないので、あくまでも自分が必要とする単体モデルのINSERT,UPDATE,DELETEがカバーできるものならできそう。

(問題)現状のModelのコードを理解する必要がある。

案4:別のORMを作る

モデルまわりのコードを見たが、少しスパゲッティな感じもした。ざっと実装を調べていて感じたのは、Model、Manager、QuerySetでの役割がきれいに分かれてないように見受けられた。複合キー対応も含めて、簡素な自分のORM作るほうがいいような気もする。

(問題)そんな時間は無い!

案5:既にある別のORMを使う

以下は、主キーではなくて、あくまで複合外部キーのためか?

https://github.com/Arisophy/django-composite-foreignkey

(問題)適切なものが見つからない

2.結論

今のプロジェクトは、Ruby On Railsで作られたレガシーなシステムを、徐々にDjangoに移行しているところで、DBに手を加えたくない。

結論として、案2と案3で進めることにした。

INSERT,UPDATE,DELETEについては、案3でModelの拡張クラスで対応する。実際に作ってみたら、少ないコードで、いい感じで動いている。このままその拡張モデルを使いながら、プロジェクトを進めていく。

SELECTについては、そもそも、すべてORMに頼るわけにはいかないので、独自クエリ―も利用する。

拡張クラスをどう作ったかは、後日、公開します。

(2021/01/29)

以下で、パッケージを公開しました。

https://github.com/Arisophy/django-compositepk-model

(2021/06/06)

パッケージの日本語説明

(2021/03/16)

Djangoのticket373についての自分の見解は以下。

[Django]DB操作

1.モデル

https://docs.djangoproject.com/ja/3.2/topics/db/models/

1.1 オブジェクトの取得

オブジェクトを取得する

1)全件取得

MyModel.objects.all()

2)1件の取得

get() を用いて1つのオブジェクトを取得する

3)複雑な条件

以下で様々な条件が作れる。

Q オブジェクトを用いた複雑な検索

Field lookups

集計して、さらにその条件で検索するような場合は、以下を参考。

アグリゲーション

Filtering on annotations

1.2 リレーション

リレーション

関係により、以下クラスでFieldを定義する。リレーションを定義された側のモデルでは、自動で小文字のモデル名でフィールドが作成されて利用できるようになる。

反対向きのリレーション

1)多対一 (many-to-one) 関係

class ForeignKey(to, on_delete, **options)

必須ではありませんが、ForeignKey フィールド名 (上記の例では manufacturer はモデル名を小文字にしたものをおすすめします。

Djangoドキュメント:モデル-リレーション

2)多対多 (many-to-many) 関係

class ManyToManyField(to, **options)

3)一対一 (one-to-one) 関係

class OneToOneField(to, on_delete, parent_link=False, **options)

オブジェクトのhasattrは、以下でやる。

hasattr(object, 'field_name')

https://docs.djangoproject.com/ja/3.1/topics/db/examples/one_to_one/#one-to-one-relationships

4)実装例

Examples of model relationship API

5)その他

・キーは、無指定の場合、モデル名+’_id’となる。違う定義の場合は、db_columnで指定する。

Database Representation

・逆側のリレーションを作りたくない場合は、「related_name=’+’」

https://docs.djangoproject.com/ja/3.1/ref/models/fields/#django.db.models.ForeignKey.related_name

2.独自のクエリ

1)カスタムSQLの操作

https://docs.djangoproject.com/en/3.1/topics/db/sql/#executing-custom-sql-directly

2)トランザクション制御

https://docs.djangoproject.com/ja/3.1/topics/db/transactions/

明示的にトランザクションをコントロールする

3)素の SQL 文の実行

https://docs.djangoproject.com/ja/3.1/topics/db/sql/#performing-raw-sql-queries

3.複合主キーの扱い

https://docs.djangoproject.com/ja/3.1/faq/models/#do-django-models-support-multiple-column-primary-keys

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys

複合主キーはサポートされていなくて、「primary_key=True」指定されているフィールドが無い場合は、「id」が主キーとして動作するので、従来システムのDBを使っている場合、モデルを使った更新、削除が、このままでは動作しない。テーブルにidフィールドを追加するか、独自のクエリで処理する必要がある。

3.Manager

3.1 カスタムマネージャーの作成

DB取得等のロジックをViewの中に色々書くのもいまいちなので、カスタムマネージャーを作成する。

(Django公式)マネージャーのカスタマイズ

上記の日本語版は、翻訳が微妙で間違った解釈になるところがあるので、英語版を読んだ方が良さそう。

(Django公式)Custom managers

拡張クラスにメソッドを定義、あるいはget_querysetを書き換える等をコーディングするなり、QuerySetを定義するなりする。以下に4パターンの方法が記載されている。

(Django公式ドキュメント)

①カスタムMangerにメソッド追加する方法

Adding extra manager methods

②カスタムMangerの初期クエリセットを変更する方法

Modifying a manager’s initial QuerySet

③QuerySetを拡張してメソッド追加する方法

Calling custom QuerySet methods from the manager

④QuerySetを複数拡張する場合に、Managerをクエリセットから定義できる方法

Creating a manager with QuerySet methods

上記から適切な方法で実装していく。

4.QuerySet

https://docs.djangoproject.com/ja/3.1/ref/models/querysets/

Pythonコーディング

0)ドキュメント

Pythonでのコーディングする時の参考ページ。

(ゼロからのパイソン講座)

https://www.python.jp/train/index.html

(Pythonドキュメント)

https://docs.python.org/ja/3/

https://docs.python.org/ja/3/contents.html

4.8. 間奏曲: コーディングスタイル

(コーディング規約:PEP 8)

https://pep8-ja.readthedocs.io/ja/latest/#

https://www.python.org/dev/peps/pep-0008/

1)Nullチェック

Pythonでは、NullではなくてNone。

None オブジェクト

チェックには、「is」演算子がある。

6.10.3. 同一性の比較

2)例外

4.3. 例外

8.4. try

例外を投げるのは、throwではなくて、raiseとのこと。

7.8. raise 文

組み込み例外

3)可変長引数

4.7. 関数定義についてもう少し

4.7.4. 任意引数リスト

argsについては記載されているが、kwargsの記載は見つけれらず。完全な文法仕様を見ると定義されている。

10. 完全な文法仕様

arguments:
    | args [','] &')' 
args:
    | ','.(starred_expression | named_expression !'=')+ [',' kwargs ] 
    | kwargs 
kwargs:
    | ','.kwarg_or_starred+ ',' ','.kwarg_or_double_starred+ 
    | ','.kwarg_or_starred+
    | ','.kwarg_or_double_starred+
starred_expression:
    | '*' expression 
kwarg_or_starred:
    | NAME '=' expression 
    | starred_expression 
kwarg_or_double_starred:
    | NAME '=' expression 
    | '**' expression 

4)型(class)チェック

class type(object)

型オブジェクト

isinstance(object, classinfo)

isinstance を使って、以下のように判断できる。

# 文字かのチェック
if isinstance(value, str):
    :

# 整数型か文字かのチェック
if isinstance(value, [int, str]):
    :

5)文字列の操作

文字列メソッド

6)辞書型

マッピング型 — dict

7)クラスについて

https://docs.python.org/ja/3/tutorial/classes.html

7-1) 静的メソッドとクラスメソッド

クラスメソッド:classmethod

静的メソッド:staticmethod

共に、「@」を付けてデコレータとして使うらしいが、普通に関数として@を付けずに使うこともできるらしい。クラスメソッドと静的メソッドの違いは、継承された場合に出てくる。

クラスメソッドが派生クラスから呼び出される場合は、その派生クラスオブジェクトが暗黙の第一引数として渡されます。

Pythonドキュメント:@classmethod
  • クラスメソッドは派生(継承)したクラスで動くので、派生クラスに影響される。
  • 静的メソッドは派生クラスに影響されない。

7-2) クラス名からクラスの取得

klass = globals()[classname]

8)多重継承

9.5.1. 多重継承

9.5.1. Multiple Inheritance ※一応、原文で確認したほうがいい

検索の基本は以下。

  1. 深さ優先
  2. 左から右

9)日付型

datetime

10)列挙型

https://docs.python.org/ja/3/library/enum.html#module-enum

11)複数の配列を同時処理する場合

zipで配列をまとめればいい。

https://docs.python.org/ja/3/library/functions.html#zip

ちなみに、ループカウンタを同時に使いたい場合は、以下を使う。

https://docs.python.org/ja/3/library/functions.html#enumerate

12)タプルについて

タプルの場合、リストの内包表記のように書くとGenaratorとなるので、以下のように「tuple」で明示的に変換する必要がある。

names= tuple((f.name for f in keys))

名前付きのタプルを使うことで、プログラムが見やすくなる。

https://docs.python.org/ja/3/library/collections.html#collections.namedtuple

名前付きタプルの一部値を変更した新しいタプルの作成は以下。

https://docs.python.org/3/library/collections.html#collections.somenamedtuple._replace

13)メタクラス

https://docs.python.org/ja/3/reference/datamodel.html#metaclasses

14)ソート

https://docs.python.org/ja/3/howto/sorting.html

15)イテレータ型

https://docs.python.org/ja/3/library/stdtypes.html?highlight=dictionary#iterator-types

・最後の要素は、インデックスが-1で取得できる。後ろから3要素なら、data[-3]で取得できる。