DjangoのModelのFieldのオプション null と blank の違いについて

DjangoにてDBのNOT NULL 制約を外したい場合、nullとblankのどちらを使えばよいのか忘れることがあるため、メモしておきます。

 
目次

 

環境

 

違い

stackoverflowにありました。
python - differentiate null=True, blank=True in django - Stack Overflow

 
公式ドキュメントの場合はこのあたり。
https://docs.djangoproject.com/en/2.0/ref/models/fields/#null

  • null オプションは、NOT NULL 制約を外す
    • 基本、何も値を与えなければNULLをセット
    • ただし、CharField型などの文字型については、NULLではなく空文字をセット
      • このため、 blank=True かつ unique=True の場合は、 null=True にしないと空文字が複数回登場することになり、エラーとなる
    • BooleanFieldでNULLを扱いたい場合は、 NullBooleanField にする
  • blank オプションは、保存時に何もデータを渡さなくても、バリデーションでOKとする (値の任意入力化)

でした。

 

確認

stackoverflowではPostgreSQLMySQLで試していたので、今回はSQLiteで試してみます。

Model

今回は

  • CharField
  • IntegerField
  • DateTimeField
  • BooleanField
  • NullBooleanField

なModelを用意します。

class NullBlank(models.Model):
    charNull = models.CharField(max_length=10, null=True)
    charBlank = models.CharField(max_length=10, blank=True)
    charNullBlank = models.CharField(max_length=10, null=True, blank=True)
    charBlankUnique = models.CharField(
        max_length=10, blank=True, unique=True,
    )
    charNullBlankUnique = models.CharField(
        max_length=10, null=True, blank=True, unique=True,
    )

    intNull = models.IntegerField(null=True)
    intBlank = models.IntegerField(blank=True)
    intNullBlank = models.IntegerField(null=True, blank=True)

    dateNull = models.DateTimeField(null=True)
    dateBlank = models.DateTimeField(blank=True)
    dateNullBlank = models.DateTimeField(null=True, blank=True)

    boolNull = models.BooleanField(null=True)
    boolNullBlank = models.BooleanField(null=True, blank=True)
    boolBlank = models.BooleanField(blank=True)

    nullboolNull = models.NullBooleanField(null=True)
    nullboolBlank = models.NullBooleanField(blank=True)
    nullboolNullBlank = models.NullBooleanField(null=True, blank=True)

 

マイグレーションファイルの作成

BooleanFieldがNULLを許可しないため、マイグレーションファイル作成時にエラーとなりました。

$ python manage.py makemigrations nullblank
nullblank.NullBlank.boolNull: (fields.E110) BooleanFields do not accept null values.
  HINT: Use a NullBooleanField instead.
nullblank.NullBlank.boolNullBlank: (fields.E110) BooleanFields do not accept null values.
  HINT: Use a NullBooleanField instead.

 
それらのFieldをコメントアウトして、再度実行すると、マイグレーションファイルができました。

$ python manage.py makemigrations nullblank
Migrations for 'nullblank':
  nullblank/migrations/0001_initial.py
    - Create model NullBlank

 

sqlmigrateで、発行されるSQLを確認

CREATE TABLEのところを整形して見やすくしてみます。

$ python manage.py sqlmigrate nullblank 0001
BEGIN;
--
-- Create model NullBlank
--
CREATE TABLE "nullblank_nullblank" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 
  "charNull" varchar(10) NULL, 
  "charBlank" varchar(10) NOT NULL, 
  "charNullBlank" varchar(10) NULL, 
  "charBlankUnique" varchar(10) NOT NULL UNIQUE, 
  "charNullBlankUnique" varchar(10) NULL UNIQUE, 
  "intNull" integer NULL, 
  "intBlank" integer NOT NULL, 
  "intNullBlank" integer NULL, 
  "dateNull" datetime NULL, 
  "dateBlank" datetime NOT NULL, 
  "dateNullBlank" datetime NULL, 
  "boolBlank" bool NOT NULL, 
  "nullboolNull" bool NULL, 
  "nullboolBlank" bool NULL, 
  "nullboolNullBlank" bool NULL
);
COMMIT;

 
いい感じです。

 

マイグレーションファイルを適用
$ python manage.py migrate nullblank
Operations to perform:
  Apply all migrations: nullblank
Running migrations:
  Applying nullblank.0001_initial... OK

 

データの投入

実際にデータを投入してみます。

以下のようなDjangoコマンドを用意します。

# management/commands/null_blank.py

from django.core.management import BaseCommand
from django.utils import timezone

from nullblank.models import NullBlank

class Command(BaseCommand):
    def handle(self, *args, **options):
        NullBlank().save()

 
投入してみます。

$ python manage.py null_blank

sqlite3.IntegrityError:
NOT NULL constraint failed: nullblank_nullblank.intBlank
NOT NULL constraint failed: nullblank_nullblank.dateBlank
NOT NULL constraint failed: nullblank_nullblank.boolBlank

blank=True なカラムがエラーとなりました。

 
そのため、

NullBlank(
    intBlank=0,
    dateBlank=timezone.now(),
    boolBlank=True,
).save()

とすると、登録できました。

 
しかし、もう一度実行すると、

$ python manage.py null_blank

sqlite3.IntegrityError:
  UNIQUE constraint failed: nullblank_nullblank.charBlankUnique

と出ました。

UNIQUE制約のある列で、空文字が重複したようです。

 
そのため、最終的には以下となります。

NullBlank(
    intBlank=0,
    dateBlank=timezone.now(),
    boolBlank=True,
    charBlankUnique=timezone.now().strftime('%Y/%m/%d %H:%M:%S')
).save()

 

SQLiteCLIでデータを確認

SQLiteCLIを使い、実際のデータベースの中身を見てみます。
https://www.sqlite.org/cli.html

手元のMacではsqlite3コマンドがインストール済なので、それを利用します。

# SQLite3ファイルがある場所へと移動
$ ls -a db.sqlite3
db.sqlite3

# データベースファイルに対し、CLIを起動
$ sqlite3 db.sqlite3 
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.

# ヘッダを付けて表示
sqlite> .header on

# カラムモードで表示
sqlite> .mode column

# NULLの場合は、"NULL" という文字を出力
# こうしないと、空文字とNULLの違いが分からないため
sqlite> .nullvalue NULL

# CharField関係
sqlite> select id, charNull, charBlank, charNullBlank, charBlankUnique, charNullBlankUnique from nullblank_nullblank;
id          charNull    charBlank   charNullBlank  charBlankUnique  charNullBlankUnique
----------  ----------  ----------  -------------  ---------------  -------------------
1           NULL                    NULL                            NULL               
2           NULL                    NULL           2018/06/20 08:4  NULL  

# IntegerField関係
sqlite> select intNull, intBlank, intNullBlank from nullblank_nullblank;
intNull     intBlank    intNullBlank
----------  ----------  ------------
NULL        0           NULL        
NULL        0           NULL  


# DatetimeField関係
sqlite> select dateNull, dateBlank, dateNullBlank from nullblank_nullblank;
dateNull    dateBlank                   dateNullBlank
----------  --------------------------  -------------
NULL        2018-06-20 08:39:24.738832  NULL         
NULL        2018-06-20 08:48:50.256008  NULL 


# BooleanField関係
sqlite> select boolBlank, nullboolNull, nullboolBlank, nullboolNullBlank from nullblank_nullblank;
boolBlank   nullboolNull  nullboolBlank  nullboolNullBlank
----------  ------------  -------------  -----------------
1           NULL          NULL           NULL             
1           NULL          NULL           NULL   

 

ソースコード

GitHubに上げました。 nullblank アプリケーション以下が、今回のファイルになります。
https://github.com/thinkAmi-sandbox/Django20-sample

Django2.0のプロジェクトのurls.pyにおける、include()での引数namespaceについて調べてみた

Django 2.0にて、プロジェクトの urls.py

from django.urls import path, include

urlpatterns = [
    path('old/', include('myapp.urls', namespace='old')),
    ...
]

アプリの urls.py

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('namespace/',
         TemplateView.as_view(template_name='myapp/url_with_include.html'),
         name='without_app'),
]

としたところ、以下のエラーが発生しました。

'Specifying a namespace in include() without providing an app_name '
django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. 

Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

 
この時に調査・対応したことをメモしておきます。

 
目次

 

環境

 

調査

エラーメッセージより、

  • includeで呼ばれるモジュールに、 app_name を設定
  • (URLパターン, 名前空間) のタプルをinclude()に渡す

のどちらかを実装すれば良さそうでした。

 
また、 django.urls.include() の公式ドキュメントを見たところ、引数 namespace を使いたい場合の記載もありました。
include() | django.urls functions for use in URLconfs | Django documentation | Django

ただ、エラーメッセージで示されたどちらかの実装をしていないと、引数 namespace が使えないようです。

 
次に、いつから app_name が導入されたのかを調べてみたところ、 1.9のようでした。
https://docs.djangoproject.com/en/1.9/topics/http/urls/#url-namespaces-and-included-urlconfs

1.8以前は、include()の引数に namespace を使う方法が書かれていました。
https://docs.djangoproject.com/en/1.8/topics/http/urls/#url-namespaces-and-included-urlconfs

 
チュートリアルも、1.9から書き直されていました。

 
最後に、いつ頃からエラーが発生する用になったのかと思い、挙動を確認したところ、

でした。

Django 2.0のリリースノートにある

Support for setting a URL instance namespace without an application namespace is removed.

https://docs.djangoproject.com/en/2.0/releases/2.0/#features-removed-in-2-0

のあたりが関係していそうでした。

 
以上より、冒頭のソースコードはDjango2.0で1.8時代の実装をしていたために発生していたようでした。

 

対応

調査方法でも書きましたが、主な対応方法として以下の3つが考えられました。

  • プロジェクトのurls.pyのinclude()では namespace 引数を使用せず、アプリのurls.pyに app_name を追加
  • プロジェクトのurls.pyのinclude()の引数として、 (アプリのurls.py, 名前空間) となるタプルを渡す
  • プロジェクトのurls.pyのinclude()に namespace 引数を使用し、アプリのurls.pyでも app_name を追加

 
どの方法が良いのかと思い、Djangoの公式チュートリアルを見たところ、アプリのurls.pyに app_name を追加する書き方をしていました。
https://docs.djangoproject.com/ja/2.0/intro/tutorial03/#namespacing-url-names

そのため、特に制限がなければ、アプリのurls.pyに app_name を書くのが良いのかなと思いました。

 

動作確認

今回は、url テンプレートタグを使った時にいずれの方法でもURLリバースできるか にて確認してみます。

 

実装

まず、プロジェクトのurls.pyは

urlpatterns = [
    # アプリのurls.pyに app_name 設定があるパターン
    path('with/', include('myapp.urls_with_app_name')),

    # アプリのurls.pyには app_name 設定がなく、includeの引数をタプルにして名前空間を渡すパターン
    # (urlpatternsのあるモジュール, 名前空間)
    path('without/', include(('myapp.urls_without_app_name', 'without'))),

    # アプリのurls.pyに app_name 設定があるが、名前空間を別に用意するパターン
    path('over/', include('myapp.urls_overwrite_app_name', namespace='replaced')),
    # アプリのurls.pyの app_name 設定をそのまま使うパターン(再掲)
    path('not_over/', include('myapp.urls_overwrite_app_name')),
]

とします。

 
次に、app_nameのあるアプリのurls.pyはそれぞれ

myapp.urls_with_app_name.py

app_name = 'with'

# urlpatternsは、他も同じなので、以下省略
urlpatterns = [
    path('namespace/',
         TemplateView.as_view(template_name='myapp/url_with_include.html'),
         name='with_app'),
]

 
myapp.urls_overwrite_app_name.py

app_name = 'overwrite'

とします。

 
あとは、テンプレートを

<ul>
    <li><a href="{% url 'with:with_app' %}">app_name がある場合</a></li>
    <li><a href="{% url 'without:without_app' %}">app_name がない場合</a></li>
    <li><a href="{% url 'replaced:over_app' %}">app_name が上書きされた場合</a></li>
    <li><a href="{% url 'overwrite:over_app' %}">app_name がそのままの場合</a></li>
</ul>

として用意します。

 

動作確認

開発サーバを起動し、curlでアクセスしてみると、いずれの方法でもURLリバースできていました。

$ curl -L http://localhost:8000/with/namespace
<!DOCTYPE html>
...
<ul>
    <li><a href="/with/namespace/">app_name がある場合</a></li>
    <li><a href="/without/namespace/">app_name がない場合</a></li>
    <li><a href="/over/namespace/">app_name が上書きされた場合</a></li>
    <li><a href="/not_over/namespace/">app_name がそのままの場合</a></li>
</ul>

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/Django20-sample

Mac + Polipoで、SSHの動的フォワード(SOCKS) に HTTP Proxy をつなげてみた

よく分からないタイトルですが...

同一LAN内で、以下の論理構成とした時に、AndroidでアクセスしたときもRaspberry Piからのアクセスに見えるかどうかを試した時のメモです。

Android
|
(HTTP Proxy、ポート 23000)
|
Mac + polipo
|
(ssh -N -D 0.0.0.0:16000)
|
Raspberry Pi
|
Windows (Web Server)

 
物理的な構成は、

Android
|
(WiFi)
|
WiFiアクセスポイント
|
(有線LAN)
|
スイッチングハブ
|
(有線LAN)
|
Raspberry Pi, Windows, Mac

です。

 
目次

 

環境

  • Android 7.0系
    • 192.168.10.100
  • Mac OS X 10.11.6
    • LUA4-U3-AGTにて有線化
    • 192.168.10.99
    • Polipo 1.1.1
  • Raspberry Pi 2 Model B
    • Rasbpian
    • Macからssh可能
    • 192.168.10.201
  • Windows10
    • Python 3.6系
    • Webサーバは、Pythonpython -m http.server 9000 で実行中
    • 192.168.10.105

 
polipoを選んだ理由ですが、SOCKSをHTTPに変換できるProxyを探してみたらヒットした、というだけです。

ただ、今はメンテナンスも終了しているため、他に同じようなのがあれば、教えていただけるとありがたいです。
https://github.com/jech/polipo

 

準備

MacからRaspberry PiSSH可能にする

以前と同じようにセットアップします。
Raspberry Pi 2 Model B + docker-compose上に、Django + PostgreSQLなアプリをデプロイしてみた - メモ的な思考的な

 
内容としては

です。

 

Polipo

Macの場合、Homebrewでインストールします。

$ brew install polipo

 

Polipoの設定ファイルを作成

今回は

  • SOCKS5をHTTPにする、HTTP Proxyを作成
    • HTTP Proxyのポートは、 23000
  • キャッシュしない
  • 設定ファイルは ~/polipo_config/config として保存

とします。

# IPv4のみ接続可能
proxyAddress = "0.0.0.0"

# HTTP Proxyのポート
proxyPort = 23000

proxyName = "localhost"

# SOCKS Proxyの設定:SOCKSのポートは16000
socksParentProxy = "127.0.0.1:16000"
socksProxyType = socks5

# キャッシュをしない
maxDiskCacheEntrySize = 0 
diskCacheUnlinkTime = 0 

# 名前解決にシステムデフォルトのDNSを使う
dnsUseGethostbyname = true

# リダイレクトキャッシュをしない
dontCacheRedirects = true

# Cookieキャッシュをしない
dontCacheCookies = true

 
設定ファイルの詳細は、以下が参考になりました。

 

各機器の起動

Windows上のWebサーバ起動
python -m http.server 9000

 
Webサーバが動作しているか、Macからcurlでアクセスしてみます。

$ curl http://192.168.10.105:9000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
...

 
Windowsコマンドプロンプト上にも、アクセスした時のMacIPアドレスが記録されました。

Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
...
192.168.10.99 - - [12/Jun/2018 21:30:53] "GET / HTTP/1.1" 200 -

 
Androidもブラウザでアクセスしてみると、Windowsコマンドプロンプト上にログが残ります。

192.168.10.100 - - [12/Jun/2018 21:32:54] "GET / HTTP/1.1" 200 -
192.168.10.100 - - [12/Jun/2018 21:32:54] code 404, message File not found
192.168.10.100 - - [12/Jun/2018 21:32:54] "GET /favicon.ico HTTP/1.1" 404 -

 

MacからRaspberry PiSSH + 動的フォワード(SOCKS)

動的フォワード(SOCKS)となるように、SSHします。念のため v オプションを付けて進行状況が分かるようにしておきます。

ssh -N -D 0.0.0.0:16000 pi@192.168.10.201 -v

 
今回はパスワード認証のため、途中でパスワードを入力します。

pi@192.168.10.201's password: 
debug1: Authentication succeeded (password).

 

Mac上のPolipoを起動

SOCKSのポートにHTTP Proxyをつなぎ、ポート 23000 でHTTPアクセスを受け付けるようにします。

$ polipo -c ./polipo_config/config 
Established listening socket on port 23000.

なお、今回はPolipoを起動したときのみ、HTTP Proxyとして動作させるようにしました。停止は Ctrl + C です。

 

AndroidのProxy設定を変更

WiFi設定にて、Proxy設定を行います。

なお、手元のAndroidではHTTP Proxyしか存在しなかったので、実験に好都合です。

 
AndroidのProxy設定は、

  • プロキシ:手動
  • プロキシのホスト名:MacIPアドレス
    • 192.168.10.101
  • プロキシポート:polipoで設定したHTTP Proxyのポート
    • 23000

とします。

 
HTTP Proxyに変更後、AndroidのブラウザでWindows上のWebサーバへアクセスします。

コマンドプロンプトのログを見てみると、Raspberry Piのものになっていました。

192.168.10.201 - - [12/Jun/2018 21:49:44] "GET / HTTP/1.1" 200 -

 
期待通りの環境ができました。

Pythonで、super()で呼ばれる親メソッドの中で呼ばれるメソッドを、子でオーバーライドしてみた

良いタイトルが思い浮かばなかったのですが...

以下のソースコードを実行した時に、何がprintされるかを試した時のメモです。

class Parent:
    def reply(self):
        self.say()

    def say(self):
        print('parent!')


class Child1(Parent):
    def reply(self):
        super().reply()

    def say(self):
        print('child1!')


if __name__ == '__main__':
    c1 = Child1()
    c1.reply()
    # => ??

 
このソースコードの挙動です。

  • 子の reply() メソッドでは、親の reply() メソッドを呼ぶ
  • 親の reply() メソッドでは、 self.say() メソッドを呼ぶ

気になる点です。

  • 親子どちらの say() メソッドが呼ばれるのか?

 
目次

 

環境

 

Python3.6の場合

前述のコードを py3.py として保存し、実行してみます。

(env36) $ python py3.py 
child1!

子の say() メソッドが呼ばれました。

 
処理順を確認するため、printを仕込んでみます。

class Parent:
    def reply(self):
        print('[parent - reply]{}'.format(type(self)))
        self.say()

    def say(self):
        print('[parent - say  ]{}'.format(type(self)))
        print('parent!')

class Child1(Parent):
    def reply(self):
        print('[child  - reply]{}'.format(type(self)))
        super().reply()

    def say(self):
        print('[child  - say  ]{}'.format(type(self)))
        print('child1!')

if __name__ == '__main__':
    print('--- parent reply --->')
    p = Parent()
    p.reply()
    print('--- child1 reply --->')
    c1 = Child1()
    c1.reply()

 
実行してみます。

$ python py3.py 
--- parent reply --->
[parent - reply]<class '__main__.Parent'>
[parent - say  ]<class '__main__.Parent'>
parent!
--- child1 reply --->
[child  - reply]<class '__main__.Child1'>
[parent - reply]<class '__main__.Child1'>
[child  - say  ]<class '__main__.Child1'>
child1!

super()で呼んだ親クラスの reply() メソッドの引数selfに Child1 クラスのインスタンスが渡されています。

これは、Python3の super().reply()super(Child1, self).reply() と同じであり、後者が引数に self が渡されていることからも分かります。

その結果、Parentクラスで self.say() した時に、Child1クラスの say() メソッドが呼ばれます。

 
もし、super()を使わない場合は、親クラス.メソッド(引数としてselfを渡す) という形式、ここでは Parent.reply(self) とします。

class Child3(Parent):
    def reply(self):
        print('[child  - reply]{}'.format(type(self)))
        Parent.reply(self)

    def say(self):
        print('[child  - say  ]{}'.format(type(self)))
        print('child3!')

 
実行してみると、super()と同じ結果となりました。

[child  - reply]<class '__main__.Child3'>
[parent - reply]<class '__main__.Child3'>
[child  - say  ]<class '__main__.Child3'>
child3!

 

Python2.7の場合

Python3のコードのうち、以下のように差し替えます。

  • print文へと変更
  • 引数なしの super() がないため、 super(Child1, self) を使う

 

class Parent:
    def reply(self):
        print '[parent - reply]{}'.format(type(self))
        self.say()

    def say(self):
        print '[parent - say  ]{}'.format(type(self))
        print 'parent!'

class Child1(Parent):
    def reply(self):
        print '[child  - reply]{}'.format(type(self))
        super(Child1, self).reply()

    def say(self):
        print '[child  - say  ]{}'.format(type(self))
        print 'child1!'

if __name__ == '__main__':
    print('--- parent reply --->')
    p = Parent()
    p.reply()
    print('--- child1 reply --->')
    c1 = Child1()
    c1.reply()

 
実行してみます。

--- parent reply --->
[parent - reply]<type 'instance'>
[parent - say  ]<type 'instance'>
parent!
--- child1 reply --->
[child  - reply]<type 'instance'>
...
TypeError: super() argument 1 must be type, not classobj

エラーとなりました。super()のところで例外が起きてるようです。

原因は、Parentクラスの定義 class Parent: が、Python2の場合 old-style classes になるためです。

 
そのため、Python2でも動作させるためには、new-style classes として、Parentクラスで object を継承します。

class Parent(object):
    def reply(self):
        # あとは同じ

 
実行結果です。  

[child  - reply]<class '__main__.Child1'>
[parent - reply]<class '__main__.Child1'>
[child  - say  ]<class '__main__.Child1'>
child1!

new-style classesのため、 <class '__main__.Child1'> へと出力が変わりました。

 
もしくは、 old-sytle classesでも使える、 Parent.reply(self) にします。

class Child1(Parent):
    def reply(self):
        print '[child  - reply]{}'.format(type(self))
        Parent.reply(self)

 
実行結果です。

child  - reply]<type 'instance'>
[parent - reply]<type 'instance'>
[child  - say  ]<type 'instance'>
child1!

old-style classesのため、 <type 'instance'> と出力されています。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/python_misc_samples/tree/master/e.g._call_overrided_method_using_super

 

参考

Djangoで、アプリ用ディレクトリを作成し、INSTALLED_APPSにAppConfigのサブクラスを設定してみた

Djangoでアプリの数が増えると見づらいかなと思い、アプリ用ディレクトリを作成してみました。

その時に INSTALLED_APPSAppConfig を指定する場合で少し悩んだため、メモを残します。

 
なお、こんな感じでプロジェクトとアプリを作成しました。

# Djangoプロジェクトを作成
$ django-admin startproject myproject .

# アプリ用ディレクトリを作成
$ mkdir apps

# その中に各アプリを作成
$ cd apps/
$ python ../manage.py startapp old_style
$ python ../manage.py startapp new_style

# 比較対象として、アプリ用ディレクトリを使用しないアプリも用意
$ cd ..
$ python manage.py startapp flat_old_style_app
$ python manage.py startapp flat_new_style_app

 
この場合、Djangoプロジェクトのディレクトリ構成は以下となります。

.
├── flat_new_style_app
│   ├── __init__.py
│   ├── apps.py
│   ├── models.py
│   └── views.py
├── flat_old_style_app
│   ├── __init__.py
│   ├── apps.py
│   ├── models.py
│   └── views.py
├── manage.py
├── myproject
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── nest_apps
    ├── new_style
    │   ├── __init__.py
    │   ├── apps.py
    │   ├── models.py
    │   └── views.py
    └── old_style
        ├── __init__.py
        ├── apps.py
        ├── models.py
        └── views.py

 
目次

 

環境

 

設定方法

Django

  • flat_old_style_app
  • flat_new_style_app
  • nest_apps/new_style_app
  • nest_apps/old_style_app

の各アプリを使うためには、それぞれを INSTALLED_APPS に設定する必要があります。

 
設定方法は

  • an application configuration class (preferred), or
  • a package containing an application.

https://docs.djangoproject.com/ja/2.0/ref/settings/#installed-apps

とのことです。

 
AppConfigを使う方法が推奨されているようですが、今回は

  • AppConfigを指定
  • パッケージ名を指定

の両パターンを試してみます。

 
設定はsettingsに行います。

アプリ用ディレクトリがない場合は、

INSTALLED_APPS = [
    ...
    'flat_old_style_app',
    'flat_new_style_app.apps.FlatNewStyleAppConfig',
]

とします。

 
一方、アプリ用ディレクトリがある場合は、

INSTALLED_APPS = [
    ...
    'nest_apps.old_style',
    'nest_apps.new_style.apps.NewStyleConfig',
]

と、アプリ用ディレクトnest_apps も設定に含めます。

 
ただ、 AppConfigを使った設定の場合は、これだけだとmakemigrationsなどで

django.core.exceptions.ImproperlyConfigured: Cannot import 'new_style'. Check that 'nest_apps.new_style.apps.NewStyleConfig.name' is correct.

とエラーになります。

 
そこで、エラーメッセージに従いAppConfigのあるファイルを修正します (今回の場合は nest_apps/new_style/apps.py )。

変数 name に、ディレクトリ名も含めるよう変更します。

class NewStyleConfig(AppConfig):
    # name = 'new_style'
    name = 'nest_apps.new_style'

 
これで makemigrations などが動作します。

 

その他

INSTALLED_APPSにAppConfig指定が推奨されるようになったタイミングについて

AppConfigはDjango1.7から使えるようになったようです。

 
INSTALLED_APPSの説明でも、Django1.7から変更されています。

なお、文中の説明に preferred が入ったのは、Django1.9からのようです。
https://docs.djangoproject.com/en/1.9/ref/settings/#std:setting-INSTALLED_APPS

 
preferred が入ったのに合わせ、チュートリアルもDjango1.9から変更されていました。

 
細かい修正でもチュートリアルに反映されているのが良いです。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/Django_AppConfig-sample

*1:公式ドキュメントでは1.6表記がなかったので、こちらを利用しました

Raspberry Pi 2 Model B + docker-compose上に、Django + PostgreSQLなアプリをデプロイしてみた

Raspberry Pi 2 Model Bを使ってDjangoアプリを作ろうかなと思いました。

環境構築をどうするかと考えたところ、RaspbianでDockerを動かせると知りました。
Docker comes to Raspberry Pi - Raspberry Pi

Dockerが動くならdocker-composeもいけるのではと思い、調べてみたところ動かしている方々がいました。

そこで、Raspberry Pi 2 Model B + docker-compose上に、Django + PostgreSQLなアプリをデプロイしてみました。

 
目次

 

環境

 
なお、DjangoアプリやDockerまわりの設定は、公式チュートリアルを改変したものです。
Quickstart: Compose and Django | Docker Documentation

 

Raspbian 2018-04-18のセットアップ

以前の記事を参考にセットアップしました。
Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku - メモ的な思考的な

内容としては、

です。

 

デプロイするものを作成

以下の

  • Djangoアプリ
  • Dockerfile
  • docker-compose.yml

GitHubにpushしておきます。

 

Djangoアプリ

今回作成するDjangoアプリは

  • localhost:8000/myapp/<pk> にアクセスするとTemplateViewを返す
  • Django adminを利用可能
  • MacではSQLite、Raspbian上ではPostgreSQLを使用
  • makemigrationsも実施済
  • 環境変数により、DBバックエンドをSQLitePostgreSQLのどちらかを利用する

という簡単なものです。

リポジトリにpushしてあるので、今回は省略します。
https://github.com/thinkAmi-sandbox/rpi_docker_django_postgres-sample

 
なお、requirements.txtも用意しましたが、PostgreSQL用のライブラリとして psycopg2-binary を指定しました。

リリースノートによると、2.7.4からパッケージが分かれ、 psycopg2-binary を使うように書かれているためです。
http://initd.org/psycopg/articles/2018/02/08/psycopg-274-released/

そのため、psycopg2を指定するとワーニングが出たため、 psycopg2-binary を指定しています。

 

Dockerfile

Docker公式チュートリアルのものです。

FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/

 

docker-compose.ymlの作成

こちらは、公式チュートリアルのものを一部修正しています。

  • 外部からPostgreSQLに接続できるよう、dbにポートを追加
  • Docker上かどうかを判断するため、 IS_DOCKER という環境変数を設定
    • 良い方法でないかもしれませんが、今回はお手軽に...
version: '3'

services:
  db:
    image: postgres
  web:
    build: .
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

 

RaspbianにDockerまわりをインストール

  • Docker
  • docker-compose

をインストールします。

 

Docker

公式ブログの通り、Dockerをインストールします。

pi@raspberrypi:~ $ curl -sSL https://get.docker.com | sh

# Executing docker install script, commit: 36b78b2
+ sudo -E sh -c apt-get update -qq >/dev/null
+ sudo -E sh -c apt-get install -y -qq apt-transport-https ca-certificates curl >/dev/null
+ sudo -E sh -c curl -fsSL "https://download.docker.com/linux/raspbian/gpg" | apt-key add -qq - >/dev/null
Warning: apt-key output should not be parsed (stdout is not a terminal)
+ sudo -E sh -c echo "deb [arch=armhf] https://download.docker.com/linux/raspbian stretch edge" > /etc/apt/sources.list.d/docker.list
+ [ raspbian = debian ]
+ sudo -E sh -c apt-get update -qq >/dev/null
+ sudo -E sh -c apt-get install -y -qq --no-install-recommends docker-ce >/dev/null
+ sudo -E sh -c docker version
Client:
 Version:      18.05.0-ce
 API version:  1.37
 Go version:   go1.9.5
 Git commit:   f150324
 Built:        Wed May  9 22:24:36 2018
 OS/Arch:      linux/arm
 Experimental: false
 Orchestrator: swarm

Server:
 Engine:
  Version:      18.05.0-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.5
  Git commit:   f150324
  Built:        Wed May  9 22:20:37 2018
  OS/Arch:      linux/arm
  Experimental: false
If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like:

  sudo usermod -aG docker pi

Remember that you will have to log out and back in for this to take effect!

WARNING: Adding a user to the "docker" group will grant the ability to run
         containers which can be used to obtain root privileges on the
         docker host.
         Refer to https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
         for more information.

 
インストール時のログに従い、 docker グループに pi ユーザを追加します。

pi@raspberrypi:~ $ sudo usermod -aG docker pi

 
念のため、再起動しておきます。

pi@raspberrypi:~ $ sudo shutdown -r now 

 

docker-compose

docker-composeはpipでインストールします。

なお、 sudo 忘れると docker-compose コマンドが使えなかったので、忘れずにつけておきます。

pi@raspberrypi:~ $ sudo pip install docker-compose
...
Successfully installed PyYAML-3.12 backports.ssl-match-hostname-3.5.0.1 cached-property-1.4.2 docker-3.3.0 docker-compose-1.21.2 docker-pycreds-0.2.3 dockerpty-0.4.1 docopt-0.6.2 functools32-3.2.3.post2 jsonschema-2.6.0 texttable-0.9.1 websocket-client-0.47.0

 

ソースコードをclone

Raspbian上の適当なディレクトリに、GitHubリポジトリをcloneしておきます。

pi@raspberrypi $ git clone https://github.com/thinkAmi-sandbox/rpi_docker_django_postgres-sample.git .

 

manage.pyの実行

1回限りの作業

  • migrate
  • loaddata
  • createsuperuser

docker-compose run で実行していきます。

 

migrate

初めて docker-compose コマンドを使ったため、migrate前に色々な設定が走ります *1

pi@raspberrypi $ docker-compose run web python manage.py migrate

Creating network "docker_django_default" with the default driver
Creating docker_django_db_1 ... done
Building web
Step 1/8 : FROM python:3
 ---> 98281429ea18
Step 2/8 : ENV PYTHONUNBUFFERED 1
 ---> Running in 913b4e88ef19
Removing intermediate container 913b4e88ef19
---> 52f2af605dd4
Step 3/8 : RUN mkdir /code
---> Running in 272a8670ae9e
Removing intermediate container 272a8670ae9e
---> ab158a0f4822
Step 4/8 : WORKDIR /code
Removing intermediate container d991836249af
---> 35eec4df510b
Step 5/8 : ADD requirements.txt /code/
---> 0f31d94edaf2
Step 6/8 : RUN pip install -r requirements.txt
---> Running in ecf1decd7b32
Collecting django (from -r requirements.txt (line 1))
 Downloading https://files.pythonhosted.org/packages/23/91/2245462e57798e9251de87c88b2b8f996d10ddcb68206a8a020561ef7bd3/Django-2.0.5-py3-none-any.whl (7.1MB)
Collecting psycopg2-binary (from -r requirements.txt (line 2))
 Downloading https://files.pythonhosted.org/packages/77/09/4991fcd9a8f4bea1ee3948e1729fa17c184d25bd10809bacc143626361b9/psycopg2-binary-2.7.4.tar.gz (426kB)
Collecting pytz (from django->-r requirements.txt (line 1))
 Downloading https://files.pythonhosted.org/packages/dc/83/15f7833b70d3e067ca91467ca245bae0f6fe56ddc7451aa0dc5606b120f2/pytz-2018.4-py2.py3-none-any.whl (510kB)
Building wheels for collected packages: psycopg2-binary
 Running setup.py bdist_wheel for psycopg2-binary: started
 Running setup.py bdist_wheel for psycopg2-binary: still running...
 Running setup.py bdist_wheel for psycopg2-binary: still running...
 Running setup.py bdist_wheel for psycopg2-binary: finished with status 'done'
 Stored in directory: /root/.cache/pip/wheels/60/a9/b1/390d13a4c6ae769c74c56efdd25573d76b9fba441430189658
Successfully built psycopg2-binary
Installing collected packages: pytz, django, psycopg2-binary
Successfully installed django-2.0.5 psycopg2-binary-2.7.4 pytz-2018.4
Removing intermediate container ecf1decd7b32
---> 4cb1031555c5
Step 7/8 : ADD . /code/
 ---> cfa31851aba0
Step 8/8 : ENV IS_DOCKER true
 ---> Running in 9d76ae22228f
Removing intermediate container 9d76ae22228f
 ---> dd8368ef0606
Successfully built dd8368ef0606
Successfully tagged docker_django_web:latest
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying myapp.0001_initial... OK
  Applying sessions.0001_initial... OK

 

fixture
pi@raspberrypi $ docker-compose run web python manage.py loaddata initial_data

Starting docker_django_db_1 ... done
Installed 1 object(s) from 1 fixture(s)

 

createsuperuser
pi@raspberrypi $ docker-compose run web python manage.py createsuperuser

Starting rpi_docker_django_postgres_sample_db_1 ... done
Username (leave blank to use 'root'): admin
Email address: admin@example.com
Password: 
Password (again): 
Superuser created successfully.

 

docker-composeによる起動

docker-compose up で起動します。

pi@raspberrypi $ docker-compose up

docker_django_db_1 is up-to-date
Creating docker_django_web_1 ... done
Attaching to docker_django_db_1, docker_django_web_1
db_1   | The files belonging to this database system will be owned by user "postgres".
db_1   | This user must also own the server process.
...
db_1   | 2018-05-24 12:39:39.750 UTC [1] LOG:  database system is ready to accept connections
web_1  | Performing system checks...
web_1  | 
web_1  | System check identified no issues (0 silenced).
web_1  | May 24, 2018 - 12:42:26
web_1  | Django version 2.0.5, using settings 'myproject.settings'
web_1  | Starting development server at http://0.0.0.0:8000/
web_1  | Quit the server with CONTROL-C.

 

接続確認

curl

問題なく動作していました。

$ curl http://192.168.10.201:8000/myapp/1/

<div class="container">
    ふー
</div>

 

Django admin

Django adminのログイン〜データ作成まで問題なくできました。

f:id:thinkAmi:20180524215923p:plain:w300

 
追加したデータへのcurlも成功しました。

$ curl http://192.168.10.201:8000/myapp/2/
<div class="container">
    ばー
</div>

 

PostgreSQL

psql を使って、登録したデータを確認します。

 

接続

まず、Raspbian上で docker-compose psPostgreSQLのポートがRaspbianに転送されているかを確認します。

# docker-compose.ymlファイルが存在するディレクトリにいることを確認
pi@raspberrypi $ ls -al
...
-rw-r--r-- 1 pi pi  282 May 26 12:30 docker-compose.yml

# PostgreSQLの5432ポートが、Raspbianの9876ポートに転送されていることを確認
pi@raspberrypi $ docker-compose ps
       Name                      Command               State           Ports         
-------------------------------------------------------------------------------------
docker_django_db_1    docker-entrypoint.sh postgres    Up      0.0.0.0:9876->5432/tcp
docker_django_web_1   python3 manage.py runserve ...   Up      0.0.0.0:8000->8000/tcp

 
次に、RaspbianもしくはMac上で psql を使って接続します。

# Macの場合
$ psql -h 192.168.10.201 -U postgres -p 9876
psql (10.3, server 10.4 (Debian 10.4-1.pgdg90+1))
Type "help" for help.

postgres=# 

 

テーブル一覧とデータの確認

\dt で、テーブルの一覧を確認します。Djangoのテーブルがあるようです。

postgres=# \dt
                   List of relations
 Schema |            Name            | Type  |  Owner   
--------+----------------------------+-------+----------
 public | auth_group                 | table | postgres
 public | auth_group_permissions     | table | postgres
 public | auth_permission            | table | postgres
 public | auth_user                  | table | postgres
 public | auth_user_groups           | table | postgres
 public | auth_user_user_permissions | table | postgres
 public | django_admin_log           | table | postgres
 public | django_content_type        | table | postgres
 public | django_migrations          | table | postgres
 public | django_session             | table | postgres
 public | myapp_foo                  | table | postgres
(11 rows)

 
続いて、データを確認します。

postgres=# select * from myapp_foo;
 id | name 
----+------
  1 | ふー
  2 | ばー
(2 rows)

 

psqlの終了

データが確認できたので、 \qpsqlを終了します。

postgres=# \q

 

停止

Ctrl + C にて停止します。

^CGracefully stopping... (press Ctrl+C again to force)
Stopping docker_django_web_1 ... done
Stopping docker_django_db_1  ... done

 

いらないものを削除

docker-compose down にて削除します。

# 現在の環境を確認
pi@raspberrypi $ docker-compose ps

       Name                      Command                State     Ports
-----------------------------------------------------------------------
docker_django_db_1    docker-entrypoint.sh postgres    Exit 0          
docker_django_web_1   python3 manage.py runserve ...   Exit 137        
pi@raspberrypi:~/work/docker_django $ docker-compose images
     Container           Repository        Tag       Image Id      Size 
------------------------------------------------------------------------
docker_django_db_1    postgres            latest   af4e361a5ae8   218 MB
docker_django_web_1   docker_django_web   latest   d0958cc8b3f7   619 MB

# いらないものを削除
pi@raspberrypi $ docker-compose down
Removing docker_django_web_1     ... done
Removing docker_django_web_run_4 ... done
Removing docker_django_web_run_3 ... done
Removing docker_django_web_run_2 ... done
Removing docker_django_web_run_1 ... done
Removing docker_django_db_1      ... done
Removing network docker_django_default


# 削除されていることを確認
pi@raspberrypi $ docker-compose ps

Name   Command   State   Ports
------------------------------

pi@raspberrypi $ docker-compose images
Container   Repository   Tag   Image Id   Size
----------------------------------------------

## Dockerイメージは残っていることに注意
pi@raspberrypi $ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker_django_web   latest              d0958cc8b3f7        About an hour ago   649MB
postgres            latest              af4e361a5ae8        9 days ago          228MB
python              3                   98281429ea18        2 weeks ago         608MB

 

Raspbianシャットダウン

すべて終わったので、Raspbianをシャットダウンしておきます。

pi@raspberrypi $ sudo shutdown -h now 
Connection to 192.168.10.201 closed by remote host.
Connection to 192.168.10.201 closed.

 

ソースコード

GitHubに上げてあります。
https://github.com/thinkAmi-sandbox/rpi_docker_django_postgres-sample

 

参考

*1:時々、 「django.db.utils.OperationalError: could not connect to server: Connection refused」が発生します。ただ、再度実行すると成功します。タイミングなのでしょうか...

Python 3 エンジニア認定基礎試験に合格しました

5/18にPython 3 エンジニア認定基礎試験を受けてきました。
基礎試験 | 一般社団法人Pythonエンジニア育成推進協会

無事に合格したため、メモを残します。

 

受験した経緯

社内Slackにて試験話が盛り上がっていました。

自分もビッグウェーブに乗ろうとPython 3 エンジニア認定基礎試験の受験要項を調べたところ、当日申込でも午後に受験できると知りました。東京すごい。
対象試験(コン検、Rails、ビジネス統計、リユース検定、Python)申込みから受験までの流れ|オデッセイ テスティング センター

 

試験まで

毎日Pythonにふれているとはいえノー勉強ではマズイだろうと思い、先に合格している同僚の受験記を参考に、何をすれば良いか確認しました。
Python 3 エンジニア認定基礎試験に合格しました - プログレッシブ・プロレタリアート

模試があるとのことだったので、DjangoCongress JP参加のために上京する車中にて

をしました。

模試は試験の形式が分かったので、目を通しておいて良かったです。

また、Pythonチュートリアル本には、知らなかった内容も書かれていたため、参考になりました。

 

結果

無事に終わって良かったです。