初期のモデル実装の調整方法

背景

実装初期、モデルの実装の調整は多く発生します。今行っているプロジェクトも業務内容が複雑で、なかなか細部まで理解するのが難しい状態です。

そう言った場合、テーブルの追加削除、カラムの追加削除、リレーションの設定等、たくさんの変更が入ります。

そういった作業をスムーズに行うために、モデルの修正->ロールバック->マイグレーションファイルへの適用->マイグレーション実行->データベース確認、の流れをまとめて記述しておきたいと思います。

ロールバック

すでにテーブルなど作成済の場合は、モデルの変更をマイグレーションファイルに適用する前にロールバックを行い、テーブルを削除しておきます。

1
2
3
4
5
6
7
$ docker-compose rum --rm web python manage.py migrate example_app zero
Creating example-app_web_run ... done
Operations to perform:
Unapply all migrations: example_app
Running migrations:
Rendering model states... DONE
Unapplying example_app.0001_initial... OK

これでテーブルは全て削除されました。

マイグレーションファイルへの適用

モデルを修正した後は、マイグレーションファイルへその変更を反映する必要があります。

しかし、マイグレーションファイルがある状態でマイグレーションファイルを作成しようとすると、新しいマイグレーションファイルができてしまいます。

運用フェーズに入っている場合はそれが正しいと思うのですが、初期のモデル調整で複数のマイグレーションファイルを作成したくないので、一旦マイグレーションファイルを削除します。

1
$ rm example_app/migrations/0001_initial.py

次にマイグレーション作成コマンドを実行します。

1
2
3
4
5
6
$ docker-compose run --rm web python manage.py makemigrations example_app
Creating example-app_web_run ... done
Migrations for 'example_app':
example_app/migrations/0001_initial.py
- Create model ...
(省略)

これで再度example_app/migrations/0001_initial.pyが作成されました。

マイグレーションの実行

マイグレーションファイルを作成したので、マイグレーションを実行します。

1
2
3
4
5
6
$ docker-compose run --rm web python manage.py migrate example_app
Creating example-app_web_run ... done
Operations to perform:
Apply all migrations: example_app
Running migrations:
Applying example_app.0001_initial... OK

マイグレーションが完了しました。

テーブルの確認

マイグレーションによりテーブルを作成したら、dhshellコマンドでモデルを変更した箇所が正しくテーブルに反映されているか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker-compose run --rm web python manage.py dbshell
Creating example-app_web_run ... done
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 30
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [example_app]> show create table xxxx;

show create tableコマンドなどでテーブルを確認します。

修正したファイルのコミット(Git)

テーブルを確認し問題なければ修正したファイルをコミットします。モデルの修正点がわかりやすいようコミットコメントを記述します。

以上でモデルの修正が完了となります。

まとめ

アプリケーション開発初期はモデルの修正が頻繁に行われます。最初から完全なモデルを実装するのは難しいですからね。

修正・変更のログを取得しなくてもよい初期のモデル、テーブル構成はまっさらにしてから再作成するのがよいと思います。そうしないとマイグレーションファイルがどんどん増えて、煩雑になってしまいます。

参考図書

メールアドレスで認証するように変更する

背景

前回、Djangoが提供してくれている機能を用いてユーザー認証ページを作成しました。

ユーザー名はメールアドレスの場合が多いと思いますので、ユーザー名ではなくメールアドレスで認証できるように変更したいと思います。

認証バックエンドの追加

認証方法を変更するにはどのようにするかというと、ドキュメントに記載がありました。

このページの中の認証バックエンドを指定するをみると、認証バックエンドとして利用するクラスのリストはAUTHENTICATION_BACKENDSに定義されており、デフォルトでは

1
['django.contrib.auth.backends.ModelBackend']

となっているようです。確認してみます。

1
2
3
4
5
6
7
$ docker-compose run --rm web python manage.py shell
Creating example_web_run ... done
>>>
>>> from django.conf import settings
>>>
>>> settings.AUTHENTICATION_BACKENDS
['django.contrib.auth.backends.ModelBackend']

確かに設定されていました。

settings.AUTHENTICATION_BACKENDSはリストで、最初のクラスから順番にauthenticate()メソッドを呼び出し、最初に有効な認証と判断した段階で処理を終えるようです。

認証バックエンドの実装

では認証バックエンドを実装していきます。実装に必要な内容はドキュメントを参照します。

親クラスの指定

親クラスにはdjango.contrib.auth.backends.BaseBackendクラスを指定します。

デフォルトで指定されているクラスの親クラスも調べてみます。親クラスを参照するには__bases__メソッドを利用します。

1
2
3
4
5
6
7
$ docker-compose run --rm web python manage.py shell
Creating example_web_run ... done
>>>
>>> from django.contrib.auth.backends import ModelBackend
>>> ModelBackend.__bases__
(<class 'django.contrib.auth.backends.BaseBackend'>,)
>>>

デフォルトのModelBackendの親クラスもBaseBackendでした。

こちらのクラスを継承して実装してみようと思ったのですが、BaseBackendクラスの実装はほぼないので、自分で全て実装しないといけません。
https://github.com/django/django/blob/main/django/contrib/auth/backends.py

今回は認証部分(authenticateメソッド)のみを変更したいので、ModelBackendを継承して実装したいと思います。

空の実装で追加してみる

一旦認証バックエンドを追加しますが、試しに空の実装で追加してみます。accounts/backends.pyを作成し、以下のように実装します。

1
2
3
4
5
from django.contrib.auth.backends import ModelBackend


class EmailAuthenticationBackend(ModelBackend):
pass

そして、settings.pyにAUTHENTICATION_BACKENDSを定義します。

1
2
3
4
AUTHENTICATION_BACKENDS = [
'accounts.backends.EmailAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
]

順番が重要なので、デフォルトの認証バックエンドよりも前にします。

動作確認

では動作確認を行います。ドキュメントによると、

ユーザーが一度認証されると、Djangoはどの認証バックエンドが認証に使用されたかを、ユーザーセッションに保持します

と記載されているので、sessionのデータを確認することでどの認証バックエンドが利用されたかがわかりそうです。

セッションはrequest.sessionでアクセスできます。ログイン後、適当なViewを作成し、request.sessionの中身を確認します。

1
2
3
4
5
from django.http import HttpResponse


def top(request):
return HttpResponse((',').join(request.session.keys()))

レスポンスを確認すると、

1
_auth_user_id,_auth_user_backend,_auth_user_hash

となりました。_auth_user_backendがいかにもそれっぽいですね。次はそのキーを指定します。

1
2
def top(request):
return HttpResponse(request.session['_auth_user_backedn'])

レスポンスは

1
accounts.backends.EmailAuthenticationBackend

となりました。新しく作成した認証バックエンドが利用されていることがわかりました。

メールアドレスで認証する

メールアドレスで認証するようauthenticate()メソッドを変更します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

UserModel = get_user_model()

class EmailAuthenticationBackend(ModelBackend):
def authenticate(self, request, email=None, password=None, **credentials):
try:
user = UserModel.objects.get(email=email)
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user

こちらで実装が完了しました。

ただこれだけではメールアドレスで認証ができません。ログインフォームの改修が必要です。

ログインフォームの実装

ログインフォームについては実践Djangoのコードそのまま拝借します。以下のコードをaccounts/forms.pyとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from django import forms
from django.contrib.auth import authenticate, get_user_model
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _

UserModel = get_user_model()

class EmailAuthenticationForm(forms.Form):
email = forms.EmailField(max_length=254, widget=forms.TextInput(attrs={'autofocus': True}))
password = forms.CharField(label=_("Password"), strip=False, widget=forms.PasswordInput)

error_messages = {
'invalid_login': "メールアドレスまたはパスワードに誤りがあります",
'inactive': _("This account is inactive"),
}

def __init__(self, request=None, *args, **kwargs):
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)

self.email_field = UserModel._meta.get_field("email")
if self.fields["email"].label is None:
self.fields["email"].label = capfirst(self.email_field.verbose_name)

def clean(self):
email = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")

if email is not None and password:
self.user_cache = authenticate(self.request, email=email, password=password)
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={"email": self.email_field.verbose_name}
)
else:
self.confirm_login_allowed(self.user_cache)

return self.cleaned_data

def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(self.error_messages["inactive"], code='inactive')

def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None

def get_user(self):
return self.user_cache

URLの設定

フォームクラスを実装したら、そのフォームクラスを利用するようurls.pyを修正します。

accounts/urls.pyloginを以下のように修正します。

1
2
3
4
5
path('login/', LoginView.as_view(
form_class=EmailAuthenticationForm,
redirect_authenticated_user=True,
template_name='accounts/login.html'
), name='login'),

form_classの指定を追加しました。これで、テンプレートは変更することなく、フォームがメールアドレスをPOSTするフォームに変わります

from accounts.forms import EmailAuthenticationFormの1行も忘れずに

動作確認

/accounts/logout/にアクセスしてログアウト後、ログインフォームを表示し、メールアドレス、パスワードを入力してsubmitすると、TOPページに遷移することが確認できます。

メールアドレスで認証するよう実装することができました

まとめ

Djangoが提供してくれているユーザー認証の仕組みはそのまま使える感じだと思いますが、今回のようにメールアドレスで認証したい場合などは少し手を入れる必要があります。

ですが、それも決まったメソッドをオーバーライドするだけで非常に簡単に実装できました。

次回はユーザーモデルのカスタマイズを試してみたいと思います。

参考図書

Djangoでユーザー認証を実装する

背景

今実装しているアプリケーションでは、ユーザー認証機能が必須です。
実践Djangoのサンプルアプリケーションで、とても簡潔に実装されていたので、こちらにまとめておきます。

認証用アプリケーションの追加

まず認証用アプリケーションを追加します。

1
$ docker-compose run --rm web python manage.py startapp accounts

作成されたファイルを確認します。

1
2
3
4
5
6
7
8
$ find accounts -type f
accounts/migrations/__init__.py
accounts/models.py
accounts/__init__.py
accounts/apps.py
accounts/admin.py
accounts/tests.py
accounts/views.py

アプリケーションを追加するためにINSTALLED_APPSに追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ vi example/settings.py
...
...(省略)...

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'accounts.apps.AccountsConfig',
]
...(省略)...
...

アプリケーションの追加が完了しました。

URLの設定

Djangoが提供しているビューを利用して、認証用アプリケーションのURLを設定します。

Djangoはログイン用のビューとしてLoginView、ログアウト用のビューとしてLogoutViewというクラスを提供してくれています。

accounts/urls.pyを以下のように書き換えます。

1
2
3
4
5
6
7
8
9
10
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path

urlpatterns = [
path('login/', LoginView.as_view(
redirect_authenticated_user=True,
template_name='accounts/login.html'
), name='login'),
path('logout/', LogoutView.as_view(), name='logout')
]

pathの第3引数のname=はURLを逆引きするときの名前を指定します。

では作成したaccounts/urls.pyをプロジェクト配下のurls.pyで読み込むようにします。

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
]

テンプレートの作成

ログイン画面のテンプレートaccounts/login.htmlをまだ実装してないので、こちらを実装します。

bootstrapを利用して見た目を調整する

見た目を整えるためにbootstrap5を利用しようと思います。Djangoで利用するにはdjango-bootstrap5というパッケージをインストールします。

パッケージのインストール

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker-compose run --rm web poetry add django-bootstrap5
Creating example_web_run ... done
Skipping virtualenv creation, as specified in config file.
Using version ^21.3 for django-bootstrap5

Updating dependencies
Resolving dependencies... (22.2s)

Writing lock file

Package operations: 3 installs, 0 updates, 0 removals

• Installing soupsieve (2.3.1)
• Installing beautifulsoup4 (4.10.0)
• Installing django-bootstrap5 (21.3)

インストール完了しました。

INSTALLED_APPSに追加する

利用するためにはINSTALLED_APPSへの追加が必要です。

1
2
3
4
5
INSTALLED_APPS = [
...(省略)...
'accounts.apps.AccountsConfig',
'django_bootstrap5'
]

テンプレートの追加

accounts/templates/accounts/login.htmlを追加します。

レイアウトファイルの追加

ページの統一感を持たせるためレイアウトファイルを追加します。templates/base.htmlというファイルを追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% load static %}
{% load django_bootstrap5 %}
<html>
<head>
...(省略)...
</head>
<body>
...(省略)...
<main>
<div class="container">
{% block main %}{% endblock %}
</div>
</main>
</body>
</html>

accounts/login.htmlの追加

accounts/templates/accounts/login.htmlは先程作成したレイアウトファイルのmainブロックを上書きします。

1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base.html" %}
{% load django_bootstrap5 %}

{% block main %}
<h2>ログイン</h2>
<form method="post">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
{% bootstrap_form form %}
{% bootstrap_button button_type="submit" content="ログイン" %}
</form>
{% endblock %}

作成したので、ログインページにアクセスしてみます。

テンプレート検索ディレクトリの追加

アクセスしてみるとTemplateDoesNotExist at /accounts/login/というエラーが発生してしまいます。理由はレイアウトファイルを置いたディレクトリはテンプレート検索パスに含まれていないため、テンプレートが見つからないからです。

templatesディレクトリをテンプレート検索パスに追加します。example/settings.pyTEMPLATESリストのDIRSを以下のように変更します。

1
'DIRS': [BASE_DIR / 'templates'],

再度アクセスすると、無事ログイン画面が表示されました!

動作確認

以前にpython manage.py createsuperuseradminユーザーを作成していたので、adminでログインできることを確認します。

ログインフォームにユーザー名とパスワードを入力してsubmitすると、認証は通ったようですが、/accounts/profile/というページにリダイレクトされ、Page not foundエラーが表示されてしまいました。

ログイン後のリダイレクトURL設定

ログイン後のリダイレクトURLはsettings.LOGIN_REDIRECT_URLで指定します。デフォルトの値を確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker-compose run --rm web python manage.py shell_plus
Creating example_web_run ... done
# Shell Plus Model Imports
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
# Shell Plus Django Imports
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, When
from django.utils import timezone
from django.urls import reverse
from django.db.models import Exists, OuterRef, Subquery
>>> settings.LOGIN_REDIRECT_URL
'/accounts/profile/'

デフォルトでは/accounts/profile/になっていることがわかりました。こちらの設定を上書きします。

1
2
3
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

LOGIN_URLはデフォルトと同じですが明示的に記載しておきます。LOGOUT_REDIRECT_URLはデフォルトでは指定されていませんでしたので/に指定しました。

再度/accounts/login/にアクセスします。先程すでに認証されているので、TOPページにリダイレクトされました。

まとめ

Djangoが提供しているビューを利用してユーザー認証を実装しました。テンプレートを作成したくらいで、ほとんど実装することなく認証ページを作成することができました。とても便利だと思います。

次回はメールアドレスで認証するように変更してみたいと思います。

参考図書

MySQLコンテナの文字コードをutf8mb4に設定する

背景

実践Djangoのサンプルアプリケーションを実装していて、フォームからマルチバイト文字を保存しようとしたところ、エラーが発生しました。

MySQLのオフィシャルコンテナは、そのままだとデフォルトの文字コードがutf8になっていないようなので、その設定方法をまとめておきます。

クライアント側の設定

クライアント側はDjangoなので、Djangoの設定になります。
プロジェクト/settings.pyのデータベース設定に、OPTIONSに文字コードを指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangosnippets',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
'OPTIONS': {
'charset': 'utf8mb4'
}
}
}

以上でクライアント側の設定は完了です。

サーバー側の設定

MySQLのオフィシャルコンテナのページを見ると、以下のように記載があります。

1
2
3
4
5
Configuration without a cnf file

Many configuration options can be passed as flags to mysqld. This will give you the flexibility to customize the container without needing a cnf file. For example, if you want to change the default encoding and collation for all tables to use UTF-8 (utf8mb4) just run the following:

$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

設定ファイルをカスタマイズして読み込ませてもいいと思いますが、そうしなくていい方法を選ぼうと思います。コマンドのオプションで指定できるようなので、docker-compose.ymlにそのように記載します。

1
2
3
4
5
6
7
8
9
10
11
12
db:
image: mysql:5.7
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: 'django'
MYSQL_PASSWORD: 'django'
MYSQL_DATABASE: 'djangosnippets'
ports:
- 33060:3306
volumes:
- mysql:/var/lib/mysql

commandを記述して、起動コマンドを上書きします。

これでサーバー側の設定は完了です。

動作確認

では動作確認を行います。Djangoのコマンドでデータベースに接続します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker-compose run --rm web python manage.py dbshell
Creating djangosnippets_web_run ... done
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 24
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [djangosnippets]>

文字コードを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MySQL [djangosnippets]> show variables like '%char%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database | utf8mb4 |
| character_set_filesystem | binary |
| character_set_results | utf8mb4 |
| character_set_server | utf8mb4 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.011 sec)

ちゃんとutf8mb4になっていました。

まとめ

MySQLコンテナの文字コードの設定方法を記述しました。

utf8mb4にしておくことで、絵文字も保存できるようになります。

開発環境を作る際に忘れてしまったらこのページを参照しようと思います。

参考図書

poetryを使ってDjango4.0にアップデートする

背景

2021年12月7日にDjango4.0がリリースされました。

リリースノート
https://docs.djangoproject.com/ja/4.0/releases/4.0/

今は3.2.9を利用していて、LTSなので使い続けることは可能なのですが、バージョンアップで後で苦労するのが大変そうだったので、今のうちにあげてみることにしました。

バージョン管理にPoertyを利用しているので、それを前提に記載していきます。

pyproject.tomlの変更

現在のバージョンの確認

現在のバージョンの確認をします

1
2
3
4
5
6
$ docker-compose run --rm web pip list
...
distlib 0.3.4
Django 3.2.9
django-extensions 3.1.5
...

現在は3.2.9です。

pyproject.tomlの確認

pyproject.tomlでの指定を確認します。

1
2
3
4
5
6
7
[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.20.3"
Django = "^3.2.9"
mysqlclient = "^2.1.0"
requests = "^2.26.0"
platformdirs = "^2.4.0"

Djangoのバージョン指定を4.0.0にします。

poetryのバージョン指定

4.0系のバグフィックスバージョンは含めたいので、^4.0.0と指定します。
~4.0と書いてもいいのかもしれませんが、^4.0.0の方がわかりやすいかなと思います。

diffを取ると以下のようになりました

1
2
-Django = "^3.2.9"
+Django = "^4.0.0"

パッケージの更新

パッケージの更新にはpoetry updateコマンドを利用します。Djangoのみ更新したいので、パッケージも指定します。

1
2
3
4
5
6
7
8
9
10
11
$ docker-compose run --rm web poetry update Django
...
Updating dependencies
Resolving dependencies... (37.8s)

Writing lock file

Package operations: 0 installs, 1 update, 1 removal

• Removing pytz (2021.3)
• Updating django (3.2.9 -> 4.0)

Djangoが4.0にupdateされたと同時にpytzが削除されたようです。

poetry.lockの確認

poetry.lockのdiffも確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git diff poetry.lock 
...
[[package]]
name = "django"
-version = "3.2.9"
-description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+version = "4.0"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"

[package.dependencies]
-asgiref = ">=3.3.2,<4"
-pytz = "*"
+asgiref = ">=3.4.1,<4"
sqlparse = ">=0.2.2"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
...

Djangoのバージョン指定が変わっていることがわかります。また、pytzが削除されています。

pytzが削除されて、tzdataが追加されている件については、
https://docs.djangoproject.com/ja/4.0/releases/4.0/#zoneinfo-default-timezone-implementation
が関係していると思います。

この変更の理由については、以下のスライドがわかりやすいと思います。
https://www.slideshare.net/ryu22e/python-39zoneinfo

まとめ

まだアプリケーションを作り込んでいないので、バージョンをあげたことによる変化は感じていませんが、逆に早めにバージョンを上げることで、これ以降のバージョンアップもスムーズにできるというメリットがあると思います。

新しいバージョンが発表されたら早めに対応するのが良いと思います。

記事ページにJSON-LDを追加する

背景

サイトの成長を感じるために毎日Google Search Consoleを見ています。

どれくらい検索結果に表示されて、どれくらいクリックされているのかを確認するのが毎日とても楽しみにしているのですが、最近クリック率が徐々に下がってきてしまっています(6.0%->5.7%)

表示回数が増えるとクリック率も下がるかとは思うのですが、このままでは良くないと思い、json-ldを導入することにしました。

JSON-LDの仕様の確認

Googleの構造化データを含む非AMPページの箇所を確認します

https://developers.google.com/search/docs/advanced/structured-data/article

JSON-LDの箇所だけ抜き出すと

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": "Article headline",
"image": [
"https://example.com/photos/1x1/photo.jpg",
"https://example.com/photos/4x3/photo.jpg",
"https://example.com/photos/16x9/photo.jpg"
],
"datePublished": "2015-02-05T08:00:00+08:00",
"dateModified": "2015-02-05T09:20:00+08:00",
"author": [{
"@type": "Person",
"name": "Jane Doe",
"url": "http://example.com/profile/janedoe123"
},{
"@type": "Person",
"name": "John Doe",
"url": "http://example.com/profile/johndoe123"
}]
}
</script>

となっています。

これを各記事ページで動的に対応していきます。

項目の精査

それぞれ項目をどう設定するかを見ていきます。

@context

こちらは固定で https://schema.orgで良さそうです。

@type

こちらはArticleNewsArticleBlogPostingが選択できるようですが、一旦Articleにしておきます

headline

Headlineはtitleと同じでよいかなと思います。変数で指定します

image

記事内の画像の一覧を変数で指定します

datePublished

記事内の変数を参照します

datePublished

こちらも同じように記事内の変数を参照します

author

こちらは固定で@typeはPerson、nameは自分の名前、urlはaboutmeを指定します

ではパーシャルを作成していきます。

パーシャルの作成

themes/landscape/layout/_partial/ディレクトリの下にjson-ld.ejsというファイルを作成します。

中身は以下のようにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "<%- post.title %>",
<% if (post.twitter_image){ %>
"image": [
"<%- post.twitter_image %>"
],
<% } %>
"datePublished": "<%- date(post.date, "YYYY-MM-DDTHH:mm:ss") %>",
"dateModified": "<%- date(post.updated, "YYYY-MM-DDTHH:mm:ss") %>",
"author": [{
"@type": "Person",
"name": "Motoaki Shibagaki",
"url": "https://book-reviews.blog/aboutme/"
}]
}
</script>

最初、"image"は複数設定できた方が良いかなと思い、記事内のymlで画像URLの配列を作成していたのですが、うまく文字列の配列を作成できず断念しました。こちらについてはまた時間を作ってチャレンジしたいと思います。

記事ページの判定

作成したパーシャルをheadタグ周りのパーシャルであるthemes/landscape/layout/_partial/head.ejsで読み込みます。

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<%- partial('google-analytics') %>
<%- partial('google-adsense') %>
<%- partial('json-ld') %>

しかし、このままでは、記事ページ以外でもjson-ldのスクリプトが表示されてしまいます。

ですので、記事ページの判定を行います。記事ページの判定にはis_post()という関数を利用します。

最終的にjson-ld.ejsは以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<% if (is_post()){ %>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "<%- post.title %>",
<% if (post.twitter_image){ %>
"image": [
"<%- post.twitter_image %>"
],
<% } %>
"datePublished": "<%- date(post.date, "YYYY-MM-DDTHH:mm:ss") %>",
"dateModified": "<%- date(post.updated, "YYYY-MM-DDTHH:mm:ss") %>",
"author": [{
"@type": "Person",
"name": "Motoaki Shibagaki",
"url": "https://book-reviews.blog/aboutme/"
}]
}
</script>
<% } %>

動作確認

では動作確認をします。

記事ページを表示させて、json-ldが表示されているかを確認します。記事ページはhttps://book-reviews.blog/error-creating-the-test-database-on-Django/で試しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Djangoでテスト実行時にテスト用データベース作成エラーが発生した時の対応",

"image": [
"https://book-reviews.blog/images/django-logo.png"
],

"datePublished": "2021-12-07T23:30:00",
"dateModified": "2021-12-07T23:30:00",
"author": [{
"@type": "Person",
"name": "Motoaki Shibagaki",
"url": "https://book-reviews.blog/aboutme/"
}]
}

大丈夫そうですね!

エラー発生

リリースしてから何日か後にサーチコンソールでエラーが発生しました。

解析不能な構造化データがあるというエラーでした。

原因を調べてみると、タイトルにダブルクォートが入っていて、タイトルの区切り文字として認識されてしまい、syntax errorになっていました。

一旦ダブルクォートを利用しないようにして対応しましたが、本当はもっといい方法があると思います。こちらも時間があるときに調査したいと思います。

まとめ

CTRを上げるために、JSON-LDを追加しました。結果はというと、かなり良い感じでCTRが上がっています(5.7% -> 6.5%)。

短期的に見ているので、実際はもう少し下がるのかなと思うのですが、効果が出ていて嬉しいです。

良い構造化データを作成するために、画像の複数指定や、タイトルの作成についてもう少し掘っていきたいと思います。

Djangoでテスト実行時にテスト用データベース作成エラーが発生した時の対応

背景

以前の記事でdocker-composeを利用してDjango+MySQLの環境を作成しました。

プロジェクトとアプリケーションを作成し、テストを実行するところで、以下のエラーが発生しました。

1
2
3
4
$ docker-compose run --rm web python manage.py test
Creating test database for alias 'default'...
Got an error creating the test database: (1044, "Access denied for user 'django'@'%' to database 'test_example'")
ERROR: 2

初めてのDjangoでのテスト実行ということもあってわからないことが多かったので調べた結果を記載していきます。

テスト用データベースについて

エラーメッセージを見て思ったのは、どのようにしてテスト用データベースの名前が決まっているのか?ということでした。公式ドキュメントを探していると見つかりました。

テストを書いて実行する | Django ドキュメント | Django

こちらによると、

テストデータベースのデフォルトの名前は、 DATABASES 設定内の各 NAME の値の前に test_ を付けたものになります。設定の DATABASES 内の TEST ディクショナリには、テストデータベースに対するいろいろな設定を書くことができます。例えば、別のデータベース名を指定したければ、 TEST ディクショナリの NAME に、 DATABASES の中から好きなデータベースを選んで指定することができます。

ということでした。TESTディクショナリを指定していないので、結果としては、test_exampleで良さそうです。

settings.pyは以下のようになっています。

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'example',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
}
}

TESTディクショナリを設定する場合は

1
2
3
4
5
6
7
8
9
10
11
12
13
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'example',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
'TEST': {
'NAME': 'custom_test_database'
}
}
}

となります。

現状の確認

データベースの確認

データベースが存在していないのか、存在しているがアクセスできないのかを確認したいと思います。まずはdbshellでアクセスしてみます。

1
2
3
4
5
6
7
8
9
$ docker-compose run --rm web python manage.py dbshell
MySQL[example]> show databases;
+---------------------+
| Database |
+---------------------+
| information_schema |
| example |
+---------------------+
2 rows in set (0.001 sec)

データベースがないように見えます。権限の問題かもしれないので権限を確認します。

権限の確認

次に権限を確認します。権限の確認にはshow grants forというSQL文があるので実行します。

1
2
3
4
5
6
7
8
MySQL[example]> show grants for 'django'@'%';
+-----------------------------------------------------+
| Grants for django@% |
+-----------------------------------------------------+
| GRANT USAGE ON *.* TO 'django'@'%' |
| GRANT ALL PRIVILEGES ON `example`.* TO 'django'@'%' |
+-----------------------------------------------------+
2 rows in set (0.001 sec)

GRANT USAGEは見慣れない権限なのですが、権限を与えないということのようです。全ての権限をなくしてからexampleデータベースにのみ全権限を与えているということのようです。

これでは現状がわからないのでrootで接続してみます。

webコンテナにログインしてからmysqlクライアントで接続します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker-compose run --rm web /bin/bash 
Creating example_web_run ... done
root@6e114e4f73b3:/code#
root@6e114e4f73b3:/code# mysql -u root -p -h db
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.7.36 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]>

rootのパスワードはdocker-compose.ymlでMYSQL_ROOT_PASSWORD環境変数で指定したパスワードを入力しました。

ではデータベースを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
MySQL [(none)]> show databases;
+---------------------+
| Database |
+---------------------+
| information_schema |
| example |
| mysql |
| performance_schema |
| sys |
+---------------------+
5 rows in set (0.010 sec)

MySQL [(none)]>

データベースはありませんでした。一旦作成してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MySQL [(none)]> create database test_example;
Query OK, 1 row affected (0.003 sec)

MySQL [(none)]> show databases;
+--------------------------+
| Database |
+--------------------------+
| example |
| information_schema |
| mysql |
| performance_schema |
| sys |
| test_example |
+--------------------------+
6 rows in set (0.001 sec)

権限も付与します。

1
2
3
4
5
6
7
8
9
10
11
12
13
MySQL [(none)]> grant all privileges on `test_example`.* to 'django'@'%';
Query OK, 0 rows affected (0.003 sec)

MySQL [(none)]>
MySQL [(none)]> show grants for 'django'@'%';
+----------------------------------------------------------------------+
| Grants for django@% |
+----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'django'@'%' |
| GRANT ALL PRIVILEGES ON `example`.* TO 'django'@'%' |
| GRANT ALL PRIVILEGES ON `test_example`.* TO 'django'@'%' |
+----------------------------------------------------------------------+
3 rows in set (0.001 sec)

権限の付与ができました。

テスト再実行

ではテストを再実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker-compose run --rm web python manage.py test
Creating example_web_run ... done
Creating test database for alias 'default'...
Got an error creating the test database: (1007, "Can't create database 'test_example'; database exists")
Type 'yes' if you would like to try deleting the test database 'test_example', or 'no' to cancel: yes
Destroying old test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.006s

OK
Destroying test database for alias 'default'...

すでにデータベースがあるので削除していいかと聞かれてしまいました…
yesとタイプして先に進むと、無事テストが通りました。

もう一度実行してみます。

1
2
3
4
5
6
7
8
9
10
$ docker-compose run --rm web python manage.py test
Creating example_web_run ... done
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.010s

OK
Destroying test database for alias 'default'...

今度は問題なくできました。

ということは、権限のみ与えれば良いということになります。

まとめ

Djangoのテストを実行するために、接続ユーザーに対してテスト用データベースへの権限を付与する必要があるということがわかりました。

新しい環境を作成したあとの初回は都度権限を付与するのはちょっと面倒な印象があります。MySQLコンテナでの/docker-entrypoint-initdb.dを利用した初期化がうまく使えないか検証してみようと思います。

docker-composeでMySQL認証エラーが出た時の対応

背景

以前の記事でdocker-composeを利用してDjango+MySQLの環境を作成しました。

今回、別のプロジェクトでDjango+MySQLのアプリケーションを作成することにしたので、久々環境を構築したところ、以下のエラーにハマったので備忘録として記載します。

エラー内容

エラーはこんな感じでした。

1
django-web-1  | django.db.utils.OperationalError: (1044, "Access denied for user 'django'@'%' to database 'django'")

認証に失敗しているようです…

docker-compose.ymlの確認

まずMySQLコンテナの認証を設定しているdocker-compose.ymlを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '3'

services:
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: 'django'
MYSQL_PASSWORD: 'django'
MYSQL_DATABASE: 'django'
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
web:
build: .
command: python3 manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
- site-packages:/usr/local/lib/python3.10/site-packages
- usr-local-bin:/usr/local/bin
ports:
- "8000:8000"
links:
- db

volumes:
site-packages:
driver: local
usr-local-bin:
driver: local
mysql:
driver: local

問題なさそうです。

Djangoのデータベース接続設定の確認

次に接続に行っているDjango側の設定を確認します。プロジェクト配下のsettings.pyを確認します。

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
}
}

こちらも問題なさそうです。

原因

だいぶ長い間ハマって気づいたのは、mysqlのvolumeが意図しない動作を引き起こしているのではないかということでした。

一度volumeを削除してみます。

1
$ docker volume rm 該当のvolume

(volumeの確認はdocker volume lsで行いました)

削除後にdocker-compose upを行うと、認証のエラーはなくなり、無事アプリケーションが起動しました。

公式ドキュメントを確認

MySQLコンテナのドキュメントEnvironment Variablesを確認するとちゃんと書いてありました。

Do note that none of the variables below will have any effect if you start the container with a data directory that already contains a database: any pre-existing database will always be left untouched on container startup.

Google翻訳にすると

すでにデータベースが含まれているデータディレクトリでコンテナを起動した場合、以下の変数はいずれも効果がないことに注意してください。既存のデータベースは、コンテナの起動時に常に変更されません。

ということなので、認証情報を変更する場合は毎回volumeを削除する必要があるということでした。

まとめ

MySQLコンテナはvolumeを使っている場合、認証情報は環境変数で変更することはできないことがわかりました。(最初の1回だけ環境変数で指定)

認証情報だけではなく、データベース名なども変更する場合は、既存のvolumeを削除してから起動すると、環境変数で指定した値が反映されます。

Amazon EventBridgeを使ってAWS Batchを起動する

背景

少し前からAWS Batchを使ってDjangoのカスタムコマンドを実行する仕組みを実装していました。今回はAWS BatchをAmazon EventBridgeを使って実行してみようと思います。

一つ気になるところがありまして、AWS Batchで利用するコンテナイメージやジョブ定義ファイルはGitHub Actionsを用いてデプロイしています。(以前の記事参照

ジョブ定義ファイルを更新した場合、Amazon EventBridgeも更新する必要があるのかどうかを検証してみたいと思います。

Amazon EventBridgeの設定

まず、EventBridgeの設定を行います。

マネジメントコンソールからAmazon EventBridgeを選択し、左メニューにあるルールをクリックします。

イベントバスはdefaultを選択したままにしておきます。

ルールの一覧の右上にルールを作成というボタンがあるのでそれを押下します。

ルールの作成

ルールの作成画面に遷移します。

名前と説明は適当に入力します。名前は必須なので、必ず入力します。

パターンの定義では大きく2種類あって、イベントパターンとスケジュールがあります。

今回はCronのように特定の時間でAWS Batchを起動したいので、スケジュールを選択します。

スケジュールの設定

スケジュールをクリックすると、さらに下に設定項目が表示されます。
固定速度ごとCron式が選択できます。Cronのように設定したいので、Cron式を選択します。

試しに設定してみます。5分ごとに実行するようにしてみます。

1
0/5 * * * * *

一見あっていそうですが、この記述方法では設定できません…

なにがダメなのかというと、曜日の設定は、日が*の場合は、*ではなく、?となっています。曜日を指定する場合は、日を?にしないといけなかったりします。この辺りの設定についてはドキュメントを読んでみてください。

また、Cronの設定が正しいと、実行する日時が下に表示されます。
Cron式の設定

設定が正しいかどうかは実行日時が表示されるかどうかで判断することができます。時刻の表記はGMTではなくローカルタイムゾーンにしておいたほうがわかりやすいと思います。基本的には9時間を引いてCron式を設定することになります。

イベントバスの設定

イベントバスはAWSのデフォルトのイベントバスのままにします。

ターゲットの設定

次にターゲットの設定を行います。今回のターゲットはAWS Batchなので、ターゲットのところでバッチジョブのキューを選択します。

すると、ジョブキュージョブ定義,ジョブ名が必須の入力項目として現れます。

ジョブキュージョブ名はジョブキューのARNとジョブ名を入力すれば良いのですが、注意が必要なのはジョブ定義です。

ジョブ定義はジョブ定義の名前またはARNと書かれています。ジョブ定義のARNを参照すると、

1
arn:aws:batch:ap-northeast-1:xxxxxxxxxxxx:job-definition/jobname:1

のように最後にrevisionがついています。revisionまで指定してしまうと、GitHub Actionsでのデプロイでジョブ定義ファイルが更新されて新しいrevisionが作成された場合、EventBridgeから起動できなくなってしまいます。

では、どうすればいいのかというと、方法は2つあります。

  • ジョブ定義の名前で指定する
  • ARNで指定する場合は:revisionを書かない(jobnameまでにする)

この2つの方法で設定すると、GitHub Actionsでジョブ定義ファイルを更新しても、EventBridgeが最新のジョブ定義ファイルで実行してくれます。

ロールの設定

ロールは新しいロールを追加します。

再試行ポリシーとデッドレターキューの設定

この辺り、細かく設定できるので、うまく実行できなかった場合は通知を受け取ることもできると思いますが、一旦スキップします。

最後に作成ボタンを押下してルールを作成します。

バッチ実行確認

指定した時間にジョブが起動し、正しく実行されることを確認しました。

また、GitHub Actionsでデプロイし、ジョブ定義ファイルを更新した後も正しく実行されることを確認しました。

まとめ

Amazon EventBridgeを使ってAWS Batchを起動する方法を確認しました。

最初はGitHub Actionsでジョブ定義ファイルをデプロイした後に、Amazon EventBridgeの設定を修正する処理を追加しないといけないかなと思っていたのですが、最新のrevisionを指定できるようになっていてよかったです。

Poetryを使ってみる

背景

以前の記事では、Dockerコンテナ上でPipenvを用いてパッケージ管理をしようとしていました。

利用していて、難点だったのが、仮想環境を用いずPipfile.lockを利用してパッケージをインストールできないという点でした。

--systemというオプションを指定しても、Pipfile.lockを利用する場合は以下のようになってしまっていました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web python -m pipenv sync --system      
Creating a virtualenv for this project...
Pipfile: /code/Pipfile
Using /usr/local/bin/python3 (3.10.0) to create virtualenv...
⠦ Creating virtual environment...created virtual environment CPython3.10.0.final.0-64 in 391ms
creator CPython3Posix(dest=/root/.local/share/virtualenvs/code-_Py8Si6I, clear=False, no_vcs_ignore=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/root/.local/share/virtualenv)
added seed packages: pip==21.3.1, setuptools==58.3.0, wheel==0.37.0
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

✔ Successfully created virtual environment!
Virtualenv location: /root/.local/share/virtualenvs/code-_Py8Si6I
Installing dependencies from Pipfile.lock (d9bd7e)...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 15/15 — 00:00:02
All dependencies are now up-to-date!

しかし、やはりlockファイルを利用してバージョンを固定したいと思いました。後輩に相談してみると、poetryというパッケージ管理ツールがあるので試してみてはどうか?と教えてもらったので試してみます。

既存のパッケージの削除

パッケージはDocker volume内に残っていますので、一旦既存パッケージを削除します。

1
2
$ docker-compose run --rm web python -m pip freeze > pip_list
$ docker-compose run --rm web python -m pip uninstall -r pip_list -y

-yをつけることで、uninstallしていいかどうかの確認を省略できます。

アンインストールできたかを確認します

1
2
3
4
5
6
$ docker-compose run --rm web python -m pip list
Package Version
---------- -------
pip 21.3.1
setuptools 57.5.0
wheel 0.37.0

アンインストールできていることが確認できました。

poetryのインストール

ドキュメントを確認すると、いろんな仮想環境でpoetryを用いる場合は、インストーラーを使った方がよいと書いてあります。

しかし、わたしはDockerを利用するため、特定のPythonのバージョンに依存してよいかと思いますので、非推奨になっているpipを用いてインストールさせてもらいます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
$ docker-compose run --rm web python -m pip install poetry
Collecting poetry
Downloading poetry-1.1.11-py2.py3-none-any.whl (175 kB)
|████████████████████████████████| 175 kB 3.3 MB/s
Collecting cachy<0.4.0,>=0.3.0
Downloading cachy-0.3.0-py2.py3-none-any.whl (20 kB)
Collecting clikit<0.7.0,>=0.6.2
Downloading clikit-0.6.2-py2.py3-none-any.whl (91 kB)
|████████████████████████████████| 91 kB 4.6 MB/s
Collecting pkginfo<2.0,>=1.4
Downloading pkginfo-1.7.1-py2.py3-none-any.whl (25 kB)
Collecting crashtest<0.4.0,>=0.3.0
Downloading crashtest-0.3.1-py3-none-any.whl (7.0 kB)
Collecting html5lib<2.0,>=1.0
Downloading html5lib-1.1-py2.py3-none-any.whl (112 kB)
|████████████████████████████████| 112 kB 6.9 MB/s
Collecting requests-toolbelt<0.10.0,>=0.9.1
Downloading requests_toolbelt-0.9.1-py2.py3-none-any.whl (54 kB)
|████████████████████████████████| 54 kB 3.4 MB/s
Collecting poetry-core<1.1.0,>=1.0.7
Downloading poetry_core-1.0.7-py2.py3-none-any.whl (424 kB)
|████████████████████████████████| 424 kB 6.8 MB/s
Collecting shellingham<2.0,>=1.1
Downloading shellingham-1.4.0-py2.py3-none-any.whl (9.4 kB)
Collecting virtualenv<21.0.0,>=20.0.26
Downloading virtualenv-20.10.0-py2.py3-none-any.whl (5.6 MB)
|████████████████████████████████| 5.6 MB 7.8 MB/s
Collecting keyring<22.0.0,>=21.2.0
Downloading keyring-21.8.0-py3-none-any.whl (32 kB)
Collecting cachecontrol[filecache]<0.13.0,>=0.12.4
Downloading CacheControl-0.12.10-py2.py3-none-any.whl (20 kB)
Collecting pexpect<5.0.0,>=4.7.0
Downloading pexpect-4.8.0-py2.py3-none-any.whl (59 kB)
|████████████████████████████████| 59 kB 3.6 MB/s
Collecting tomlkit<1.0.0,>=0.7.0
Downloading tomlkit-0.7.2-py2.py3-none-any.whl (32 kB)
Collecting cleo<0.9.0,>=0.8.1
Downloading cleo-0.8.1-py2.py3-none-any.whl (21 kB)
Collecting requests<3.0,>=2.18
Downloading requests-2.26.0-py2.py3-none-any.whl (62 kB)
|████████████████████████████████| 62 kB 941 kB/s
Collecting packaging<21.0,>=20.4
Downloading packaging-20.9-py2.py3-none-any.whl (40 kB)
|████████████████████████████████| 40 kB 3.8 MB/s
Collecting msgpack>=0.5.2
Downloading msgpack-1.0.2.tar.gz (123 kB)
|████████████████████████████████| 123 kB 7.5 MB/s
Preparing metadata (setup.py) ... done
Collecting lockfile>=0.9
Downloading lockfile-0.12.2-py2.py3-none-any.whl (13 kB)
Collecting pylev<2.0,>=1.3
Downloading pylev-1.4.0-py2.py3-none-any.whl (6.1 kB)
Collecting pastel<0.3.0,>=0.2.0
Downloading pastel-0.2.1-py2.py3-none-any.whl (6.0 kB)
Collecting six>=1.9
Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Collecting webencodings
Downloading webencodings-0.5.1-py2.py3-none-any.whl (11 kB)
Collecting jeepney>=0.4.2
Downloading jeepney-0.7.1-py3-none-any.whl (54 kB)
|████████████████████████████████| 54 kB 792 kB/s
Collecting SecretStorage>=3.2
Downloading SecretStorage-3.3.1-py3-none-any.whl (15 kB)
Collecting pyparsing>=2.0.2
Downloading pyparsing-3.0.5-py3-none-any.whl (97 kB)
|████████████████████████████████| 97 kB 5.9 MB/s
Collecting ptyprocess>=0.5
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB)
Collecting certifi>=2017.4.17
Using cached certifi-2021.10.8-py2.py3-none-any.whl (149 kB)
Collecting charset-normalizer~=2.0.0
Downloading charset_normalizer-2.0.7-py3-none-any.whl (38 kB)
Collecting idna<4,>=2.5
Downloading idna-3.3-py3-none-any.whl (61 kB)
|████████████████████████████████| 61 kB 5.1 MB/s
Collecting urllib3<1.27,>=1.21.1
Downloading urllib3-1.26.7-py2.py3-none-any.whl (138 kB)
|████████████████████████████████| 138 kB 8.5 MB/s
Collecting distlib<1,>=0.3.1
Using cached distlib-0.3.3-py2.py3-none-any.whl (496 kB)
Collecting filelock<4,>=3.2
Downloading filelock-3.3.2-py3-none-any.whl (9.7 kB)
Collecting platformdirs<3,>=2
Using cached platformdirs-2.4.0-py3-none-any.whl (14 kB)
Collecting backports.entry-points-selectable>=1.0.4
Downloading backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl (6.2 kB)
Collecting cryptography>=2.0
Downloading cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl (3.5 MB)
|████████████████████████████████| 3.5 MB 8.5 MB/s
Collecting cffi>=1.12
Downloading cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (446 kB)
|████████████████████████████████| 446 kB 10.2 MB/s
Collecting pycparser
Downloading pycparser-2.21-py2.py3-none-any.whl (118 kB)
|████████████████████████████████| 118 kB 8.8 MB/s
Building wheels for collected packages: msgpack
Building wheel for msgpack (setup.py) ... done
Created wheel for msgpack: filename=msgpack-1.0.2-cp310-cp310-linux_x86_64.whl size=15833 sha256=fee9473ca391ef73f69dde97df9a6829fdd66addbd0cfa04b1cc7cd40600d620
Stored in directory: /root/.cache/pip/wheels/c6/4d/b6/98ae445826f0944cb9a6a963bceb7259c3b7467fd4cb8df818
Successfully built msgpack
Installing collected packages: pycparser, urllib3, idna, charset-normalizer, cffi, certifi, requests, pylev, pastel, msgpack, jeepney, cryptography, crashtest, webencodings, six, SecretStorage, pyparsing, ptyprocess, platformdirs, lockfile, filelock, distlib, clikit, cachecontrol, backports.entry-points-selectable, virtualenv, tomlkit, shellingham, requests-toolbelt, poetry-core, pkginfo, pexpect, packaging, keyring, html5lib, cleo, cachy, poetry
Successfully installed SecretStorage-3.3.1 backports.entry-points-selectable-1.1.1 cachecontrol-0.12.10 cachy-0.3.0 certifi-2021.10.8 cffi-1.15.0 charset-normalizer-2.0.7 cleo-0.8.1 clikit-0.6.2 crashtest-0.3.1 cryptography-35.0.0 distlib-0.3.3 filelock-3.3.2 html5lib-1.1 idna-3.3 jeepney-0.7.1 keyring-21.8.0 lockfile-0.12.2 msgpack-1.0.2 packaging-20.9 pastel-0.2.1 pexpect-4.8.0 pkginfo-1.7.1 platformdirs-2.4.0 poetry-1.1.11 poetry-core-1.0.7 ptyprocess-0.7.0 pycparser-2.21 pylev-1.4.0 pyparsing-3.0.5 requests-2.26.0 requests-toolbelt-0.9.1 shellingham-1.4.0 six-1.16.0 tomlkit-0.7.2 urllib3-1.26.7 virtualenv-20.10.0 webencodings-0.5.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

たくさんのパッケージがインストールされました。確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ docker-compose run --rm web python -m pip list
Package Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
CacheControl 0.12.10
cachy 0.3.0
certifi 2021.10.8
cffi 1.15.0
charset-normalizer 2.0.7
cleo 0.8.1
clikit 0.6.2
crashtest 0.3.1
cryptography 35.0.0
distlib 0.3.3
filelock 3.3.2
html5lib 1.1
idna 3.3
jeepney 0.7.1
keyring 21.8.0
lockfile 0.12.2
msgpack 1.0.2
packaging 20.9
pastel 0.2.1
pexpect 4.8.0
pip 21.3.1
pkginfo 1.7.1
platformdirs 2.4.0
poetry 1.1.11
poetry-core 1.0.7
ptyprocess 0.7.0
pycparser 2.21
pylev 1.4.0
pyparsing 3.0.5
requests 2.26.0
requests-toolbelt 0.9.1
SecretStorage 3.3.1
setuptools 57.5.0
shellingham 1.4.0
six 1.16.0
tomlkit 0.7.2
urllib3 1.26.7
virtualenv 20.10.0
webencodings 0.5.1
wheel 0.37.0

ショートカットも確認します

1
2
$ docker-compose run --rm web which poetry
/usr/local/bin/poetry

無事インストールできました。

プロジェクトの設定

公式のドキュメントを読むと、まずプロジェクトの設定を行なっています。

プロジェクトのディレクトリはすでに存在しているので、ドキュメントにあるこちらの方法で雛形を作成します。

現在プロジェクトのルートにいるので、そのままコマンドを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ docker-compose run --rm web poetry init

This command will guide you through creating your pyproject.toml config.

Package name [code]:
Version [0.1.0]:
Description []:
Author [None, n to skip]:
expected string or bytes-like object
Author [None, n to skip]: n
License []:
Compatible Python versions [^3.10]:

Would you like to define your main dependencies interactively? (yes/no) [yes]
You can specify a package in the following forms:
- A single name (requests)
- A name and a constraint (requests@^2.23.0)
- A git url (git+https://github.com/python-poetry/poetry.git)
- A git url with a revision (git+https://github.com/python-poetry/poetry.git#develop)
- A file path (../my-package/my-package.whl)
- A directory (../my-package/)
- A url (https://example.com/packages/my-package-0.1.0.tar.gz)

Search for package to add (or leave blank to continue):

Would you like to define your development dependencies interactively? (yes/no) [yes]
Search for package to add (or leave blank to continue):

Generated file

[tool.poetry]
name = "code"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"


Do you confirm generation? (yes/no) [yes]

特になにも入力せずデフォルト値で作成しました。あとから記載する形でよいかと思います。

パッケージの追加

パッケージの追加はpoetry addコマンドで行います。もともと入っていたboto3をインストールしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker-compose run --rm web poetry add boto3
Creating virtualenv code-MATOk_fk-py3.10 in /root/.cache/pypoetry/virtualenvs
Using version ^1.20.1 for boto3

Updating dependencies
Resolving dependencies... (2.7s)

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

• Installing six (1.16.0)
• Installing jmespath (0.10.0)
• Installing python-dateutil (2.8.2)
• Installing urllib3 (1.26.7)
• Installing botocore (1.23.1)
• Installing certifi (2021.10.8)
• Installing charset-normalizer (2.0.7)
• Installing idna (3.3)
• Installing s3transfer (0.5.0)
• Installing boto3 (1.20.1)
• Installing requests (2.26.0)

virtualenvを作成してしまっているのでインストールされません…
virtualenvを利用しないようにします。

poetryの設定

poetryの設定はpoetry configコマンドで行います。

設定項目については、こちらに記載があります。

virtualenvを利用しないようにするには以下のようにします

1
$ docker-compose run --rm web poetry config virtualenvs.create false --local

--localのオプションがないと反映されませんでした)

実行後の設定を確認します

1
2
3
4
5
6
7
$ docker-compose run --rm web poetry config --list
cache-dir = "/root/.cache/pypoetry"
experimental.new-installer = true
installer.parallel = true
virtualenvs.create = false
virtualenvs.in-project = null
virtualenvs.path = "{cache-dir}/virtualenvs" # /root/.cache/pypoetry/virtualenvs

再度パッケージを追加してみる

ではこの設定でパッケージの追加を行います。同じようにboto3を追加してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add boto3
Skipping virtualenv creation, as specified in config file.
Using version ^1.20.3 for boto3

Updating dependencies
Resolving dependencies... (0.8s)

Package operations: 5 installs, 0 updates, 0 removals

• Installing jmespath (0.10.0)
• Installing python-dateutil (2.8.2)
• Installing botocore (1.23.3)
• Installing s3transfer (0.5.0)
• Installing boto3 (1.20.3)
$

インストール作業は完了しました。念のため、pip listコマンドで確認します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ docker-compose run --rm web python -m pip list        
Package Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
boto3 1.20.3
botocore 1.23.3
CacheControl 0.12.10
cachy 0.3.0
certifi 2021.10.8
cffi 1.15.0
charset-normalizer 2.0.7
cleo 0.8.1
clikit 0.6.2
crashtest 0.3.1
cryptography 35.0.0
distlib 0.3.3
filelock 3.3.2
html5lib 1.1
idna 3.3
jeepney 0.7.1
jmespath 0.10.0
keyring 21.8.0
lockfile 0.12.2
msgpack 1.0.2
packaging 20.9
pastel 0.2.1
pexpect 4.8.0
pip 21.3.1
pkginfo 1.7.1
platformdirs 2.4.0
poetry 1.1.11
poetry-core 1.0.7
ptyprocess 0.7.0
pycparser 2.21
pylev 1.4.0
pyparsing 3.0.5
python-dateutil 2.8.2
requests 2.26.0
requests-toolbelt 0.9.1
s3transfer 0.5.0
SecretStorage 3.3.1
setuptools 57.5.0
shellingham 1.4.0
six 1.16.0
tomlkit 0.7.2
urllib3 1.26.7
virtualenv 20.10.0
webencodings 0.5.1
wheel 0.37.0

正しくインストールされていることを確認しました。

Djangoもインストールしておきます

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add django
Skipping virtualenv creation, as specified in config file.
Using version ^3.2.9 for Django

Updating dependencies
Resolving dependencies... (1.7s)

Writing lock file

Package operations: 4 installs, 0 updates, 0 removals

• Installing asgiref (3.4.1)
• Installing pytz (2021.3)
• Installing sqlparse (0.4.2)
• Installing django (3.2.9)

devパッケージの追加

linterなどdevelopment環境でしか使用しないパッケージを追加します。

development dependencyに設定するには--devを指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add flake8 --dev
Skipping virtualenv creation, as specified in config file.
Using version ^4.0.1 for flake8

Updating dependencies
Resolving dependencies... (3.2s)

Writing lock file

Package operations: 4 installs, 0 updates, 0 removals

• Installing mccabe (0.6.1)
• Installing pycodestyle (2.8.0)
• Installing pyflakes (2.4.0)
• Installing flake8 (4.0.1)

インストールできました。pyproject.tomlを確認すると、tool.poetry.dev-dependenciesに記載されいていることがわかります。

1
2
[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"

その他、isort, blackもインストールしておきます。

依存関係の設定

インストールしたパッケージはpoetry.lockに記載されています。これをバージョン管理することによって、いつでも環境を再現できるようになります。

再現するには

1
$ docker-compose run --rm web poetry install

とします。dev-dependenciesをインストールしたくない場合は--no-devをオプションに指定します。

まとめ

今回pythonのパッケージマネージャとしてpoetryを試してみました。Pipenvのときのような、lockファイルの作成でハマったりしなかったですし、とてもシンプルで使いやすいという印象でした。

今後しばらくはpoetryを使っていきたいと思います。