DjangoCongress JP 2019で発表するまでの準備を振り返ってみた #djangocongress

前回書いた通り、DjangoCongress JP 2019にて発表しました。
DjangoCongress JP 2019

 
去年も同じようなことを書いたのが、当時の様子を振り返るのに役立ったため、今年も残しておきます。

なお、CfPを出した時点の、自分のスペックは以下の通りです。あまり去年と変わってないです。

  • Blog書いているので、文章を書くのは苦にならない
  • 人前で話すのは苦手、特にアドリブがダメ
  • 勉強会でのLTは経験あるが、毎回やっても緊張が解けない
  • 45分のトークは、1年ぶり

 
目次

 

CfP

次のようなCfPを提出しました。

対象レベル:
初級〜

対象者:
Djangoでメールを送信したい方
Djangoでのメールテストにお困りの方

詳細:
以前に比べて各種SNSが利用される世の中になってきていますが、まだまだメールは健在です。

Djangoでは標準機能として、メールの送信が用意されています。そのため、settings.pyに設定を行うことで簡単にメールを送信できます。

一方、設定が漏れるとメールが送信できないなどの問題が発生します。

また、テスト環境でメールを送信する場合も、本番環境の設定をそのまま使うと、テストメールの誤配信が発生するおそれがあります。

この発表ではDjangoが標準で用意しているメールまわりの機能をお伝えし、標準で用意されている機能の再発明を防ぎ、開発時にメールまわりで困らなくなることを目指します。

話すこと:

- Djangoのメール機能
  - send_mail()関数やEmailMessageオブジェクトなど
- Djangoのメールバックエンドを自作する方法
  - 例:console.Backendを拡張し、MIMEエンコードされないSubjectを追加で出力する
- Djangoのメール機能を使った時のテストコード
  - django.core.mail.outboxを使った確認
- 管理者用設定でのエラー通知
  - ADMINSやMANAGERSなどを使った通知機能について
  

話さないこと:
- SendGridやAWS SESなどのメールサービスを使う場合の詳細やTips

 
3/4に投稿し、3/8に採用のメールをいただきました。

 

発表方法の調査

去年は Marp というツールで書きました。

今年も使いたいなと思い Marp のサイトを見たところ、

とのことでした。

とはいえ、本番まであまり時間がなく、何か詰まったら危ない感あったので、今回もMarpで書くことにしました。

 

プレゼン小道具の準備

スライド切替器は、昨年同様 R400t を使いました。念のため、予備の単4電池 x 2本も持参しました。

時間配分表も用意しました。カウントダウン・カウントアップの両バージョンです。

ただ、後述の通り、時間配分感覚を前日夕方までつかめていなかったため、手書きで用意するハメになりました。

f:id:thinkAmi:20190521092447p:plain

 

スライド作成・プレゼン練習

流れ

去年の経験より、プレゼンの練習期間として最低1週間は確保する必要がありました。

そのため、GW明けの完成を目指して作業を進めました。

今回、元となる自分のBlog記事がありませんでした。スライドを書きながらネタ出しすることも考えましたが、

  • 最初にネタをすべて出す
  • 後から削る

とした方が楽そうでした。そこで、ネタとなる機能をすべて実装したアプリを作ることから始めました。

その結果

  • GW:だいたいの実装を終える
  • 5/7頃:スライド初版完成

となりました。ただ、この時点のスライドはBlogの延長のようなもので、発表に耐えられるものではありませんでした。

 
次のマイルストーンは、5/14の社内プレゼン練習です。

初版からある程度削った状態でしたが、47分半くらいかかってしまいました。

ありがたいことに、練習後同僚にスライドを1枚ずつじっくり見てもらえました。中でも、「メールの文字コードについては、今はそんなに問題にならないだろう」と判断できたことが大きかったです。

これにより、文字コード部分はAppendixへ移動でき、何とか5分くらい削減できました。

 
5/16に2度目の練習があり、そこでようやく「あ、いける」という感触をつかめました。

 

プレゼン練習で注意したこと

過去の経験から、自分はアドリブ要素が増えるとプレゼンが不安定になると感じていました。

そのため、プレゼン練習もなるべく本番と同じ環境になることを目指しました。

具体的には

  • スクリーンと演台、発表位置を本番に合わせる
    • スクリーンの向かって右側に立つ、等
  • マイクを持つ
    • 後半になるにつれ、マイクが口から離れていくことが分かった

としました。

 
また、練習では同僚の時間をいただくので、有意義な時間になるよう「自分では気づきにくいこと」を見てほしい点として事前に伝えました。

こんな感じです。

  • 分かりづらい表現があるか
  • 説明に過不足があるか
  • 図解してほしい部分はあるか
  • 発表者のしぐさで、気になる部分はあるか
  • 眠くなったのはどこか

このおかげか、ありがたいフィードバックをいくつもいただけました。

 

前日

当日余裕がなくなりそうなので、有給とって前日入りしました。

また、去年は

書籍「理系のための口頭発表術」に、「発表練習は前日夜にはやらない」と書かれていたため、前日朝を最後に練習はやめました。

でしたが、時間配分の把握がイマイチできていなかったため、夕方に1度行いました。

 
夜もどうしようかなーと思っていたところに、別のやることができたため、追加の練習は行いませんでした。

別のことで気分転換できたこともあり、ゆっくり休めました。

 

当日

朝会場近くにいるし時間には間に合うだろうと思いましたが、進む方向を間違えました...。前日のうちに、一度下見をしておくべきでした。

 
幸い大事には至らず、無事に会場入りできました。

 
会場入り後も相変わらず緊張感あったので、お昼休みは会場で

  • ゼリー飲料 2個
  • ベーグル 1個

を食べて過ごしました。

ただ、ゼリー飲料2個はちょっと多かったようで、発表直前までお腹が重くてつらくなることに...

あとは、 @moon_in_nagano にうながされて、緊張をほぐすために会場をうろうろしてました。

 
発表は時間配分表を見ながら進め、3分くらい残して終わったようです。

発表中の様子はどんな感じだったのか覚えてないですが、前半早口・後半持ち直したようです。

発表が終わってからは、しばらく出がらし状態でした。

 

まとめ

自分向けにまとめておきます。

  • 前回準備したことを残しておくと、振り返るのが楽
    • 前回の反省を活かしやすく、経験値もたまりやすい
    • 場数を踏むことで、苦手意識を少しずつ減らせる
  • スライド作成は1週間前までに終わらないと、発表練習の余裕がなくなる
  • 発表練習は、他の人に聴いていただけるとより効果が大きい
  • 前日入りしたら、会場までのルートを把握しておく

 
去年・今年の経験より、発表すると得るものが多いことから、苦手だからと逃げるのはもったいないと感じています。

そのため、機会をいただけたら発表できるよう、今後も取り組んでいきたいと思います。

最後になりましたが、スタッフの方々や会場提供のサイボウズさんをはじめ、いろいろとありがとうございました。

DjangoCongress JP 2019 に参加 & 発表しました #djangocongress

5/18に開催された、DjangoCongress JP 2019に参加 & 発表しました。
DjangoCongress JP 2019

昨年同様、開催場所はサイボウズ株式会社の東京オフィスでした。
東京オフィス アクセスマップ | サイボウズ株式会社

今年も良い感じの雰囲気でした。

f:id:thinkAmi:20190521092444p:plain

 
自分の発表、および全体を通してのメモを残します。誤りがありましたらご指摘ください。

目次  

 

自分の発表「Djangoでのメール送信 - 設定からテストまで」

スライドです。

 
また、発表で使用したソースコードはこちらになります。
https://github.com/thinkAmi/DjangoCongress_JP_2019_talk

ソースコードの動作確認バージョンは

です。

 
参考にした公式ドキュメントは以下の通りです。

 
Djangoのメール送信機能についてまとめたいなー」と思ったのが、CfPを出した動機です。

そのため、発表はメール送信の仕組みに特化することにして、実際の運用やベストプラクティスについてはふれないようにしました。

そのあたりは、ありがたいことにツイートしてくださいましたので、引用します。他にも情報がありましたら教えていただけるとありがたいです。

 
というところで、以降は当日のメモです。

 

Djangoで静的ファイルとうまくやる話

tell-kさん

資料:Djangoで静的ファイルとうまくやる

静的ファイルの扱いについて、いろんな観点からの発表でした。

特に

  • STATIC_MEDIA_ の違いと、考慮すべき点
  • 配信サーバ構成ごとの扱い
  • 認証付静的ファイルの配信

のあたりが参考になりました。

 

現場で使える Django のセキュリティ対策

Akihito Yokoseさん

資料:現場で使える Django のセキュリティ対策 / Django security measures for business (DjangoCon JP 2019) - Speaker Deck

IPAの資料をベースに、Djangoのセキュリティはどうなっているかのセッションでした。

特に、

  • IPA冊子「安全なウェブサイトの作り方」の要約
  • 代表的な脆弱性とその対策
    • Djangoではどのように実装されているか
    • 不足しているところはどのように補えばよいか

など、セキュリティまわりが一通りまとまっていて、ありがたいです。

そのため、「Djangoのセキュリティは?」と質問された時は「これをどうぞ」と言えそうです。

 

Djangoアプリのデプロイに関するプラクティス

Masashi Shibataさん

資料:Djangoアプリのデプロイに関するプラクティス / Deploy django application

Djangoをデプロイするにあたり、用意すべきもの・考慮すべきものがまとまっていました。

ゼロから環境構築をしなくてはいけないときは、これを見つつ、抜けやモレがないか確認しようと感じました。

また、MySQLのBlackholeストレージエンジンの存在を知ることができたり、「個人開発する時は、Google App Engineを使うことが多い」など、参考になるお話もありました。

他に、セキュリティ攻撃を試せる環境も用意されていて、akiyokoさんのセッションと合わせると理解を深めやすいと感じました。

 

Authorization in Django

Hiroki Kiyoharaさん

資料:Authz in Django / GitPitch Slide Deck

以前、 django-keeper をちょっとさわったことがあり、Modelなしでも認可制御ができるところが良かったです。

どんな時に使うのが良いのかなと思いましたが、今回のセッションでいくつかユースケースが述べられていました。

そのため、もし適切なサイズのアプリであれば、利用しても大丈夫そうという気持ちになりました。

 

DjangoによるWebエンジニア育成への道

Yuichi Nakazawaさん *1

資料:DjangoによるWebエンジニア育成への道 - Speaker Deck

afterの終わりあたりに入社したので、へーしゃの歴史が学べてよかったです。

 

LT

サイボウズさん

クイズ形式で、サイボウズさん・PythonDjangoの歴史などが学べました。

 

Django Girls

Django Girlsの紹介でした。

  • 参加者からメンターになられた方の話
  • 参加者のインタビュー

などを聞き、良い雰囲気のコミュニティであることが伝わってくるLTでした。

 

なぜ私はプロポーザルに落ちたのか

moon_in_naganoさん

イベント内であのLTを仕上げるのは、さすがだな & 真似できないなと思いました*2

 

Django Queryset アレパターン

CardinalXaroさん

regist_dt など、細かなところにまで闇っぽい感があるLTでした。

 

Staffやってみて

shimakaze_softさん

思い切ってStaffをやるという行動力はすごいと思いました。

 

パーティ

今回から開催されたパーティですが、とても楽しかったです。

Web上でお世話になってる方に直接ご挨拶したりするなど、交流を深められました。

Djangoという共通の話題があったこともあり、パーティでも話しやすかったです。

 

謝辞

最後に謝辞です。

今年も弊社 (日本システム技研) のみなさんには、プレゼンの練習に付き合っていただき、ありがとうございました。

練習をする中でいろいろと気づき、修正できたのは大きかったです。

 
また、今年も会場の提供をしてくださったサイボウズ株式会社さま、ありがとうございました。Room2のバーっぽい雰囲気が相変わらず良かったです。

 
最後になりましたが、DjangoCongress JP 2019のスタッフ、発表者、参加者のみなさま、ありがとうございました。

今年も楽しい時間を過ごせました!

*1:同じ会社ですが、コミュニティイベントなので、 「さん」付けにします

*2:関係者なのでこのくらいで...

Windows10 + pyinstallerで、Djangoをexe化して配布可能にしてみた

最近、Windows10 + pyinstallerで、

をしました。

pyinstallerのWikiに方法が記載されていますが、いろいろとハマったため、メモを残します。
Recipe Executable From Django · pyinstaller/pyinstaller Wiki · GitHub

 
目次

 

環境

 
なお、最新のPythonは3.7.3です。

ただ、pyinstallerでexe化する際、Python3.7ではpyinstallerにパッチを当てる必要があります。

 
現時点では上記のPRがマージされていないため、今回はPython3.6.8を使いました。

 

Congratulations!を表示するDjangoアプリのexe化

まずは、Congratulations!を表示するDjangoアプリをexe化します。

Python3.6.8をダウンロードします。
Python Release Python 3.6.8 | Python.org

 
次に、Windowsのpyランチャーを使い、仮想環境を作成します。
Pythonの実行方法 - python.jp

# pyランチャーで仮想環境作成
>py -3.6 -m venv env36

 
仮想環境を有効化し、 djangopyinstaller を pip install します。

# 仮想環境を有効化
>env36\Scripts\activate

# インストール
(env36)>pip install django pyinstaller

 
Djangoプロジェクトを作成します。

(env36)>django-admin startproject myproject

 
開発サーバを起動し、Congratulations! が表示されることを確認します。

(env36)>python myprojct\manage.py runserver

 
続いて、pyinstallerを使い、exe化します。

現在のディレクトリ構成です。

(root)
+---env36
+---myproject
|   +---myapp
|   +---myproject

 
上記の (root) ディレクトリでpyinstallerを実行します。

exe化では、manage.pyを実行できるように myproject/manage.py を指定します。

また、出力はexeファイル1つにしたいため、オプション onefile (もしくは F ) を指定します。

他に、exeファイルの名前は name オプションで指定します。今回は myproject.exe です。

(env36)>pyinstaller --name=myproject myproject/manage.py --onefile

 
実行すると、rootの下に、 builddist の2つができます。

今回の myprojct.exedist の中に入ります。

また、rootディレクトリの中に、 myproject.spec ファイルが自動生成されます。

これが今回exe化した時の設定内容になります。exe化する時の設定を変更する場合は、このファイルを修正します。

 
続いて、runserverすると、Congratulations!が表示されます。

(env36)>dist\myproject.exe runserver

f:id:thinkAmi:20190421182558j:plain:w450

 

HttpResposeを返すDjangoアプリをexe化

続いて、自作のDjangoアプリをexe化してみます。

まずはHttpResponseを返すViewのDjangoアプリを作成します。

(env36)>myproject\manage.py startapp myapp

 
続いて、HttpResponseを返すDjangoアプリを作成します。

settings.py

INSTALLED_APPS = [
    'myapp.apps.MyappConfig',  # 追加
    'django.contrib.admin',
    ...
]

 
myapp.views.py

from django.http import HttpResponse


def hello(request):
    return HttpResponse('Hello, world')

 
myproject/urls.py_

from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),  # 追加
]

 
myapp/urls.py

from django.urls import path

from myapp import views

urlpatterns = [
    path('', views.hello, name='hello'),
]

 
続いて、pyinstallerを実行します。

Congratulations!のときと異なり、自動生成された myprojct.spec ファイルを使ってexeを作成します。

なお、 myproject/manage.py を指定すると、上記で変更した myproject.spec の内容が修正されてしまうことに注意します。

(env36)>pyinstaller myproject.spec --onefile

 
ただ、現時点では runserver するといくつかエラーが出ます。

対応方法としては、

  • exe化する
  • runserverして、エラーを出す
  • myproject.specで hiddenimports を修正する

を繰り返すしか方法がないようです。
Pyinstaller によるPython 3.6スクリプトのexeファイル化 - Qiita

 
そのため、今回対応した内容を残しておきます。

初回の runserver のエラーです。

(env36)>dist\myproject.exe runserver
Watching for file changes with StatReloader
Exception in thread Thread-1:
Traceback (most recent call last):
...
  File "lib\site-packages\django\apps\registry.py", line 91, in populate
  File "lib\site-packages\django\apps\config.py", line 116, in create
  File "importlib\__init__.py", line 126, in import_module
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 941, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'myapp'

 
そこで、 myapphiddenimports に追加します。

hiddenimports=[
    'myapp',
],

 
2回目は、 myapp.apps でエラーになったため、 myapp.apps を追加します。

(env36)>dist\myproject.exe runserver
Watching for file changes with StatReloader
Exception in thread Thread-1:
Traceback (most recent call last):
...
  File "lib\site-packages\django\apps\registry.py", line 91, in populate
  File "lib\site-packages\django\apps\config.py", line 116, in create
  File "importlib\__init__.py", line 126, in import_module
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'myapp.apps'

 
3回目は、 myapp.urls のエラーです。これも追加します。

(env36)>dist\myproject.exe runserver
Watching for file changes with StatReloader
Performing system checks...

Traceback (most recent call last):
...
  File "myproject\urls.py", line 21, in <module>
  File "lib\site-packages\django\urls\conf.py", line 34, in include
  File "importlib\__init__.py", line 126, in import_module
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 953, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'myapp.urls'
[2244] Failed to execute script manage

 
 
ここまで対応すると

(env36)>pyinstaller myproject.spec --onefile

73962 INFO: Appending archive to EXE path\to\django_pyinstaller_sample\dist\myproject.exe
73993 INFO: Building EXE from EXE-00.toc completed successfully.

と、exe化が成功します。

 
なお、この時の myprojct.spechiddenimports は以下の通りです。

hiddenimports=[
    'myapp.apps',
    'myapp.urls',
],

 
続いて、runserverして localhost:8000 にアクセスすると、Hello worldが表示されます。

(env36)>dist\myproject.exe runserver

  f:id:thinkAmi:20190421175503j:plain:w300

 

静的ファイル・Model・TemplateViewを使うDjangoアプリのexe化

次に、本格的なDjangoアプリで使われる

  • 静的ファイル
  • Model
  • TemplateView

を使うDjangoアプリをexe化してみます。

 
まずはModelを作ります。

myapp/models.py

from django.db import models


class Fruit(models.Model):
    name = models.CharField('名前', max_length=50)

    def __str__(self):
        return self.name

 
また、Modelの初期データを投入するfixtureを追加します。

myapp/fixtures/initial_data.json

[
  {
    "model": "myapp.Fruit",
    "pk": 1,
    "fields": {
      "name": "りんご"
    }
  },
  {
    "model": "myapp.Fruit",
    "pk": 2,
    "fields": {
      "name": "みかん"
    }
  }
]

 
ViewであるTemplateViewには、exe化した時の環境を確認するため、

  • Pythonのバージョン
  • settings.BASE_DIR

を表示してみます。

myapp/views.py

from django.views.generic import TemplateView
from django.conf import settings
import platform
from myapp.models import Fruit

class FruitTemplateView(TemplateView):
    template_name = 'myapp/fruit.html'
    extra_context = {
        'base_dir': settings.BASE_DIR,
        'python_version': platform.python_version(),
        'fruits': Fruit.objects.all(),
    }

 
templates/myapp/fruit.html

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
    <link rel="stylesheet" type="text/css" href="{% static 'css/myapp.css' %}">
</head>
<body>
<p class="version">Python version: {{ python_version }}</p>
<p>BASE_DIR: {{ base_dir }}</p>

<div>
    Fruit Model:
    <ul>
        {% for fruit in fruits %}
            <li>{{ fruit }}</li>
        {% endfor %}
    </ul>
</div>

<button id="show">say</button>

<script src="{% static "js/myapp.js" %}"></script>
</body>
</html>

 

versionを赤字で表示します。

static_files/css/myapp.css

.version {
    color: red;
}

 
ボタンをクリックしたときにalertを出すJavaScriptを作成します。

static_files/js/myapp.js

const btn = document.getElementById('show');

btn.addEventListener('click', () => {
    alert('hello');
});

 
プロジェクトのURLディスパッチャに、静的ファイル分を追加します。

myproject/urls.py

from django.contrib import admin
from django.urls import path, include
from django.contrib.staticfiles.urls import staticfiles_urlpatterns  # 追加

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

urlpatterns += staticfiles_urlpatterns()  # 追加

 
アプリのURLディスパッチャには、今回作成したViewを追加します。

myapp.urls.py

urlpatterns = [
    path('', views.hello, name='hello'),
    path('fruit', views.FruitTemplateView.as_view(), name='fruit'),  # 追加
]

 
settins.pyには、静的ファイルの設定を追加します。
Pyinstaller compiles but 404 error on Django Static javascript files - Stack Overflow

myproject/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [

# 追加
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static_files'),
)

 
あとはマイグレーション & fixtureによる初期データ投入を行います。

# マイグレーションファイルを作成する
(env36)>python myproject\manage.py makemigrations

# マイグレーション
(env36)>python myproject\manage.py migrate

# データ投入
(env36)>python myproject\manage.py loaddata initial_data
Installed 2 object(s) from 1 fixture(s)

 
ここまででDjangoアプリが作成できました。

続いてexe化の設定ファイルを修正します。

datas に、テンプレートと静的ファイルのパスをタプルで指定します。

myproject.spec

datas=[
    # (<myproject.specのディレクトリから見た各パス>, <exe化後に、Djangoアプリのrootから見た各パス>)
    ('myproject/templates', 'templates'),  # テンプレート
    ('myproject/static_files', 'static_files'),  # 静的ファイル
],

 
設定も終わったため、exe化と runserverします。

# exe化
(env36)>pyinstaller myproject.spec --onefile

# runserver
(env36)>dist\myproject.exe runserver

 
localhost:8000/fruit にアクセスすると

  • Pythonのバージョン
  • settings.BASE_DIR
  • Modelの内容

が表示されました。また、JavaScriptも動作しています。

f:id:thinkAmi:20190421190400p:plain

なお、 settings.BASE_DIR は一時ディレクトリのため、起動ごとに変更されることに注意します。

 

一時ディレクトリ外のSQLiteを使用するDjangoアプリをexe化

ここまでの方法では、SQLiteが一時ディレクトリにあるため、runserverするたびに初期化されてしまいます。

そのため、 %USERPROFILE% 直下にSQLiteを移動して使うよう変更します。
%USERPROFILE% env variable for python - Stack Overflow

myproject/settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(os.environ['USERPROFILE'], 'db.sqlite3'),
    }
}

 
また、fixtureを適用できるよう、exe化の設定ファイルを修正します。

myproject.spec

datas=[
    ('myproject/templates', 'templates'),
    ('myproject/static_files', 'static_files'),
    ('myproject/myapp/fixtures', 'myapp/fixtures'),  # 追加
],

 
他に、現在のSQLiteとは別のものを使っていることを確認するため、fixtureに追加します。

myproject/myapp/fixtures/initial_data.json

{
  "model": "myapp.Fruit",
  "pk": 3,
  "fields": {
    "name": "ぶどう"
  }
}

 
準備ができたので、exe化とlodadataとrunserverをして結果を確認します。

# exe化
(env36)>pyinstaller myproject.spec --onefile

# loaddata
(env36)>dist\myproject.exe loaddata initial_data
Installed 3 object(s) from 1 fixture(s)

# runserver
(env36)>dist\myproject.exe runserver

 
意図した通りに表示されました。

f:id:thinkAmi:20190421190340p:plain

 
また、 %USERPROFILE%\db.sqlite3 も作成されています。

>dir %USERPROFILE%\db*
...
2019/04/21  15:18           135,168 db.sqlite3

 
以上で、Djangoアプリをexe化できました。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_pyinstaller-sample

GitLab CI + docker-reviewを使って、Markdownをtextlintしてからpdf化するCI環境を作ってみた

最近、Markdownからpdfを作る機会がありました。

pdf化を都度行うのは手間だったため、何か良い方法がないかを探したところ、 Markdown > Re:VIEW > pdf という経路でpdfを作成できそうでした。

ただ、手動でpdfを生成するのが手間だったため、GitLab CIを使って、push後にMarkdownをtextlintしてからpdf化するCI環境を作ってみました。

 

目次

 

環境

項目 内容
CI環境 GitLab.comのCI
Lintツール textlint
Lint辞書 自作
Markdownからpdf化の方法 Markdown > Re:VIEW > pdf
MarkdownからRe:VIEWへの変換 md2review
Re:VIEWからpdfの変換 review-pdfmaker
pdfのレイアウト調整 Re:VIEWなどに詳しくないため、今回は行わない

 
また、Gitリポジトリディレクトリ構成は以下の通りです。

pdf化する対象のMarkdowntarget.md です。それ以外のファイルについては後述します。

$ tree
.
└── docs
    ├── catalog.yml
    ├── config.yml
    ├── prh.yml
    ├── sty
    │   ├── README.md
    │   ├── gentombow.sty
    │   ├── jsbook.cls
    │   ├── jumoline.sty
    │   ├── plistings.sty
    │   ├── review-base.sty
    │   ├── review-custom.sty
    │   ├── review-jsbook.cls
    │   ├── review-style.sty
    │   ├── reviewmacro.sty
    │   └── techbooster-doujin-base.sty
    ├── style-web.css
    ├── style-web.scss
    ├── style.css
    ├── style.scss
    └── target.md

 
また、対象のMarkdownファイル target.md は以下のような感じです。

(Pythonコードについては、実際はトリプルバッククォートで囲みましたが、はてなブログに貼り付ける都合上、トリプルシングルクォートにしてあります)

# タイトル

Pythonコードを書きます。

'''
print('Hello world')
'''


## りんごの時期

以下の表を参照のこと。

|項目|時期|
|---|---|
|夏あかり|お盆過ぎ|
|シナノドルチェ|9月中頃|
|シナノゴールド|10月過ぎ|

 

GitLab CIの設定ファイル .gitlab-ci.yml の作成

GitLabでCIを動かすために .gitlab-ci.yml が必要なので作成します。

 
今回は、以下のようなPipelineを作成します。

No 対象ブランチ 条件 ジョブの内容
1 全ブランチ GitLabへpush時 対象のMarkdownにtextlintを実行
2 指定ブランチ No.1成功時 Markdownをpdf化

 

ステージを定義

ジョブを順番に実行するため、ステージを2つ定義します (lintbuild)。

stages:
  - lint
  - build

 

ジョブを定義

2つのジョブ markdown_lintpdf_build を定義します。

また、ジョブの stage にて、各ジョブがどのステージで動作するかを指定します。

markdown_lint:
  stage: lint

pdf_build:
  stage: build

 

ジョブ「対象のMarkdownにtextlintを実行」の詳細を定義

ジョブ markdown_lint は、対象のMarkdownにtextlintを実行します。

以下を参考に、今回は textlinttextlint-rule-prh を使うことにします。
textlint + prhで表記ゆれを検出する | Web Scratch

 
textlintはNode.jsがあれば動作するようなので、Dockerの公式リポジトリから一番軽そうなイメージ node:11.12.0-alpine を使います。

before_script でtextlintのインストールを行い、 script でtextlintを実行します。

ジョブの全体は以下の通りです。

markdown_lint:
  image: node:11.12.0-alpine
  stage: lint
  before_script:
    - npm install -g textlint textlint-rule-prh
  script:
    - cd docs
    - textlint target.md

 

ジョブ「Markdownをpdf化する」の詳細を定義

ジョブ pdf_build は、Markdownファイルをpdf化します。

今回、Markdown > Re:VIEW > pdfの変換では、以下のツールを使います。

 
Dockerイメージは、必要なファイルがすべて含まれている vvakame/review を使います。ありがとうございます。
https://github.com/vvakame/docker-review

 

あとは、 before_script にて必要なものをインストールし、 script にて変換を行います。

pdf_build:
  image: vvakame/review
  stage: build
  before_script:
    - apt-get update -y
    - apt-get -y install build-essential ruby-dev
    - gem install md2review
  script:
    - cd docs
    - md2review target.md > target.re
    - review-pdfmaker config.yml

 
なお、変換後のpdfをダウンロードできるようにするため、 artifacts を定義します。

今回は、中間でできる target.re ファイルやpdfなど、Git管理外ファイルをダウンロードするため、 untracked: true とします。

artifacts:
  untracked: true

 
また、pdfをビルドするブランチを masterfeature/* に制限するため、 only も定義しておきます。

  only:
    - master
    - /^feature\/.*$/

 
設定ファイル .gitlab-ci.yml の全体は以下の通りです。

stages:
  - lint
  - build

markdown_lint:
  image: node:11.12.0-alpine
  stage: lint
  before_script:
    - npm install -g textlint textlint-rule-prh
  script:
    - cd docs
    - textlint target.md

pdf_build:
  image: vvakame/review
  stage: build
  before_script:
    - apt-get update -y
    - apt-get -y install build-essential ruby-dev
    - gem install md2review
  script:
    - cd docs
    - md2review target.md > target.re
    - review-pdfmaker config.yml
  artifacts:
    untracked: true
  only:
    - master
    - /^feature\/.*$/

 

textlint向けの定義

以下を参考に、textlint向けの定義を行います。
textlint + prhで表記ゆれを検出する | Web Scratch

今回は、 doc ディレクトリの中に、 prh.ymltextlintrc を作成します。

 

prh.yml

まずは prh.yml に、textlintでチェックする内容を記載します。

今回は「Python」という単語の表記ゆれはエラーとなるように設定します。

version: 1
rules:
  - expected: Python
    specs:
      - from: python
        to:   Python
      - from: PYTHON
        to:   Python

 

.textlintrc

textlint実行時に prh.yml を参照するよう設定します。

{
  "rules": {
    "prh": {
      "rulePaths": [
        "./prh.yml"
      ]
    }
  }
}

 

review-pdfmaker向けの設定

review-pdfmaker向けの設定については、以下のリポジトリを参考にしました。また、一部ファイルはそのまま利用させていただきました。ありがとうございました。
https://github.com/TechBooster/ReVIEW-Template

 

config.ymlの設定

review-pdfmaker を実行する時の定義ファイル config.ymldoc ディレクトリの中に作成します。

ReVIEW-Templateを元に、以下の内容を記載します。

# この設定ファイルでサポートするRe:VIEWのバージョン番号。
# major versionが違うときにはエラーを出す。
review_version: 3.0

# ブック名(ファイル名になるもの。ASCII範囲の文字を使用)
bookname: my_sample_book
# 記述言語。省略した場合はja
language: ja

# 書名
booktitle: {name: "サンプル本", file-as: "sample book"}

# 著者名。「, 」で区切って複数指定できる
aut: [{name: "thinkAmi", file-as: "thinkAmi"}]

# 以下はオプション
# a-edt, edt: 編集者
edt: ["my editor"]
# a-pbl, pbl: 出版社(発行所)
pbl: thinkami_pub.

# 刊行日(省略した場合は実行時の日付)
date: 2019-3-24
# 発行年月。YYYY-MM-DD形式による配列指定。省略した場合はdateを使用する
# 複数指定する場合は次のように記述する
# [["初版第1刷の日付", "初版第2刷の日付"], ["第2版第1刷の日付"]]
# 日付の後ろを空白文字で区切り、任意の文字列を置くことも可能。
history: [["2019-3-24 v0.0.1"]]
# 権利表記(配列で複数指定可)
# rights: (C) 2016 Re:VIEW Developers
rights: (C) 2019 thinkAmi

# デバッグフラグ。nullでないときには一時ファイルをカレントディレクトリに作成し、削除もしない
debug: null

# HTMLファイルの拡張子(省略した場合はhtml)
htmlext: html
#
# CSSファイル(配列で複数指定可、yamlファイルおよびRe:VIEWファイルを置いたディレクトリにあること)
stylesheet: ["style.css"]

# ePUBのバージョン (2か3)
epubversion: 3
#
# HTMLのバージョン (4か5。epubversionを3にしたときには5にする)
htmlversion: 5

# 目次として抽出する見出しレベル
toclevel: 3

# 採番の設定。採番させたくない見出しには「==[nonum]」のようにnonum指定をする
#
# 本文でセクション番号を表示する見出しレベル
secnolevel: 3

# 本文中に目次ページを作成するか。省略した場合はnull (作成しない)
toc: true

# 表紙の後に大扉ページを作成するか。省略した場合はnull (作成しない)
titlepage: true

# 奥付を作成するか。デフォルトでは作成されない。trueを指定するとデフォルトの奥付、ファイル名を指定するとそれがcolophon.htmlとしてコピーされる
colophon: true

# EPUBにおけるページ送りの送り方向、page-progression-directionの値("ltr"|"rtl"|"default")
direction: "ltr"

epubmaker:
  # HTMLファイルの拡張子
  htmlext: xhtml
  stylesheet: ["style.css","epub_style.css"]

# LaTeX用のスタイルファイル(styディレクトリ以下に置くこと)
texstyle: ["reviewmacro"]

# B5の設定(10pt 40文字×35行) - 紙版
texdocumentclass: ["review-jsbook", "media=print,paper=b5,serial_pagination=true,hiddenfolio=nikko-pc,openany,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,head_space=30mm,headsep=10mm,headheight=5mm,footskip=10mm"]

pdfmaker:
  # 奥付を作成するか。trueを指定するとデフォルトの奥付、ファイル名を指定するとそれがcolophon.htmlとしてコピーされる
  colophon: true

webmaker:
    stylesheet: ["style.css","style-web.css"]

 

pdf化するコンテンツの設定 (catalog.yml)

pdfに含まれるコンテンツの設定を行います。

本文部分のRe:VIEWファイルは、 target.md が変換された target.re になります。そのため、 target.reCHAPS に指定します。

PREDEF:

CHAPS:
  - target.re

APPENDIX:

POSTDEF:

 

デザインまわりのファイルのコピー

今回デザインまわりは何も修正しないため、リポジトリ TechBooster/ReVIEW-Template のものをそのままコピーしました。

コピーするのは以下です。

 

以上で準備が終わりました。

 

動作確認

masterブランチ

今までの作業を master ブランチにコミットし、GitLabにpushします。

すると、以下の画像のように、Lintとpdfの生成ができました。

右側にあるダウンロードボタンを押すことで、Re:VIEWとpdfのファイルがzip化されたものがダウンロードできます。

f:id:thinkAmi:20190326223859p:plain

 

featureブランチ

次に、Lintエラーが出るように以下を追加した target.md をfeatureブランチにcommitします。

- python
- PYTHON

 
GitLabにpushしてCIを確認すると、Lintチェックエラーになりました。

f:id:thinkAmi:20190326224408p:plain

また、後続のpdf作成ジョブは実行されていませんでした。

f:id:thinkAmi:20190326224054p:plain  
 

lint-onlyブランチ

最後に、Lintエラーが出るように以下を追加した target.mdlint-only ブランチにcommitします。

- Lintだけします
  - python

GitLabにpushしてCIを確認すると、Lintチェックエラーになりました。

f:id:thinkAmi:20190326224738p:plain

 
また、今までと異なり、後続ジョブ pdf_build がありません。

f:id:thinkAmi:20190326224755p:plain  

以上で、GitLab CI + docker-reviewを使って、Markdownをtextlintしてからpdf化するCI環境ができました。

#stapy #glnagano みんなのPython勉強会 in 長野#3に参加しました

3/23にギークラボ長野で開かれた「みんなのPython勉強会 in 長野 #3」に参加しました。
みんなのPython勉強会 in 長野 #3 - connpass

今回は、トークセッションともくもく会の2本立てでした。資料は上記のconnpassにまとまっています。

長野県内はもちろん、東京・名古屋、そしてはるばる石川から参加された方など、大勢の参加者で会場が埋まりました。

以下、簡単な感想とメモです。

 

イントロダクション

阿久津 剛史さん

資料:みんなのPython勉強会 in 長野 #3, Intro

みんなのPython勉強会について、過去の長野巡業からコミュニティの紹介がありました。毎年長野巡業があり、ありがたい限りです。

 

Pythonを知ろう!

辻 真吾さん

資料:みんなのPython勉強会スライド の中にある みんなのPython勉強会 in 長野#3

Pythonの概要から最近の話題までがまとめられていました。

特に、あまり追っていなかった、PEP572の概要とPython運営委員会の話もあり、理解が深まりました。

なお、運営委員会の選挙結果は、PEP8100にまとまっているようです。
PEP 8100 -- January 2019 steering council election | Python.org

 

PythonRaspberry Pi、SORACOMでお手軽IoTをしましょう

知野 雄二さん

資料:Python、Raspberry Pi、SORACOMでお手軽IoTをしましょう

SORACOMを中心に、Raspberry PiAWSGCPなどのクラウドとどうつないでいくかという発表でした。

IoT方面は疎いので、何か作りたいなーと思ったら参考にしたり質問してみたいと思いました。

 

社内WebシステムのPython活用秘話

滝野 優紀さん

弊社事例でした。

いろいろWeb化されているので、リモートワークもしやすいというのは確かに感じています。自分が入社した時よりも、最近はもっと気軽にリモートワークできている雰囲気がありますし。

なお、事例の中で触れられていませんでしたが、ソースコードは社内GitLabで公開されています*1

 

神エクセル課題解決の第一歩、統計ExcelCSV

前川 道博さん

信州デジタルコモンズ/統計オープンデータへのいざない まわりについて、統計データの公開に対する自治体の違い、公開されている神Excel、開発したアプリなどの紹介がありました。

アプリのソースコードは、以下のリポジトリにて公開されています。 Tanukium/msemi: msemi 2018

 
お話を聞いて、自治体まわりではデータが公開されているだけでもマシなんだなと改めて思いました。

また、公開されるオープンデータの形式は CSV が標準というのを知ることができてよかったです。

 

PandasとDjango Girls Tutorial Extensionsについて

nikkieさん

最近Pandasをさわっていたこともあり、トーク中で紹介されていた Pandasクックブック が良さそうに感じ、買いました。
朝倉書店| pandasクックブック ―Pythonによるデータ処理のレシピ―

また、Django Girls Tutorial Extensionsがあることも知ることができてよかったです。

なお、日本語翻訳版については、現在本家へPR中とのことです。

 

 

LTと懇親会

食べながらだったためメモは残せませんでしたが、いろいろな分野のお話を聞けました。

  • Pythonもくもく会 in 名古屋 #3 - connpass をはじめとした、名古屋支部の様子とこれからについて
    • わいわい会よさそう
  • デコレータとDiscordについて
    • Discordに色々なチャンネルがあることを初めて知った
  • 公開されている犯罪統計データExcelを扱うお話
    • ここでも神Excel...
  • USB接続のGoogle Edge TPUを使って、リアルタイム顔認識のデモ
    • USB接続のGoogle Edge TPU、ガジェットに加わるといいなぁ |ω・)チラッ
  • 将棋まわりのいろいろなライブラリを開発しているお話
    • 将棋倶楽部24のいろいろなデータを活用しようとしてるのがすごい

 

その他

ここ最近自分が技術的に悩んでいたことを相談してみたところ、さくっと回答をいただけました。

他にも、懇親会などでいろいろとお話できたのが良かったです。

独学で詰まった時には外に出ていろいろとお話するのが大事だと、改めて感じました。

 

最後になりましたが、企画・運営・参加をされたみなさま、ありがとうございました。

*1:自分は特に不満なく使っているのでコントリビュートできていない...

Python3.4 & Django1.8な個人アプリを、Python3.7 & Django 2.1 へとアップデートした

以前、食べたリンゴの割合をグラフ化するHerokuアプリを作りました。
Python + Django + Highcharts + Herokuで食べたリンゴの割合をグラフ化してみた - メモ的な思考的な

 
Python3.4 & Django1.8な環境で作成した2015年以降、食べたりんごの種類を記載していたYAMLファイル以外は、ノーメンテナンスで過ごしてきました。

ただ、最近はパフォーマンスが悪化し、見るのがつらい状況でした。

 
メンテナンスをしようかと思いましたが、

  • Djangoにさわり始めた頃のアプリのため、Djangoプロジェクト構成があまりよくない
  • テストコードなし
  • Windowsの開発環境を無くした

などがあり、あまり気が進まない状況でした。

 
そんな中、Django2.1系のPythonサポートを見たところ、Python3.4系では動作しないことに気づきました。
FAQ: インストール | Django documentation | Django

 
さすがに今後使えなくなるのはマズイため、Python3.7系 & Django2.1系へとアップデートしました。

今回はその時のメモになります。

 
目次

 

環境

 

アップデート方針

PythonDjangoを最新化するに伴い、各種ライブラリや開発環境もアップデートすることにしました。

主な作業は以下でした。

  • 開発環境のDBとしてDockerを利用
  • テストコードを作成
  • 基盤を最新版 (Python 3.7.2 & Django 2.1.5) へアップデート
    • 合わせて、各種ライブラリも最新化
  • Djangoっぽいアプリケーション構成へと修正
  • jQuery + getJsonでの非同期データ取得を、Fetch API + async/await へと修正
  • パフォーマンスの改善

 
以降、各項目の内容を記載していきます。なお、記載する順番は上記と異なります。

 

開発環境のDBとしてDockerを利用

このアプリでは、DBとしてHeroku Postgresを使っています。また、集計処理ではPostgreSQLの独自関数を使っています。

そのため、開発環境にもPostgreSQLを用意する必要がありました。ただ、開発環境にはすでにPostgreSQLがいる & その環境を壊したくありませんでした。

そこで今回は、開発環境のPostgreSQLをDockerで用意することにしました。

 
まずは本番環境であるHeroku Postgresのバージョンを調べたところ、最新は10系でした。
Version support and legacy infrastructure | Heroku Postgres | Heroku Dev Center

 
そこで、PostgreSQL 10.6 のDockerイメージを使い、開発環境を構築しました。

# デフォルトのポート 5432はすでに使われているので、別のポート(19876)をDocker上の 5432 につなげる
$ docker run --name ringo_pg -p 19876:5432 -e POSTGRES_USER=ringo -e POSTGRES_PASSWORD=postgres -d postgres:10.6

# コンテナ起動
docker start ringo_pg

# データベースを作成
psql -U ringo -W -p 19876 -h localhost -c "CREATE DATABASE ringo_tabetter_py;"

 
次に、Heroku Postgresのデータをローカルに取り込みます。

# ローカルに ringo-tabetter のGitリポジトリを作成
$ git clone git@github.com:thinkAmi/dj_ringo_tabetter.git .

# settings.pyのDATABASESのポート番号を修正するのを忘れずに!!

# ローカルのDBに対し、マイグレーション
$ python manage.py migrate

# Heroku Postgresのデータを、ローカルの output.sql ファイルとしてダウンロード
# usernameやdbnameは、Heroku Postgresのページにて確認
$ pg_dump --host=ec2-xxx.compute-1.amazonaws.com --port=5432 --username=xxx --password --dbname=yyy > output.sql

# リストア
$ psql -d ringo_tabetter_py -h localhost -p 19876 -U ringo -f output.sql

参考:sql - How can I get a plain text postgres database dump on heroku? - Stack Overflow

 
ローカルで runserver したところ、問題なく動作しました。

 

テストコードを作成

Django1系から2系にアップデートすることもあり、まずはテストコードを整備しようと考えました。

ただ、ツイートを読み込んで集計・表示するだけのアプリという性格上、テストコードは正常系だけにしました。

また、テストについては

としました。

 
なお、テストメソッド名を日本語にすると、PyCharmでは怒られている気分になります。

f:id:thinkAmi:20190215000307p:plain:w300

 
そこで、以下の設定を行い、ちょっと怒られているだけにしてみました。

Editor > Inspections > Internationalization - Non-ASCII characters のチェックを外す

f:id:thinkAmi:20190215000325p:plain:w300

 
もし、より良い方法をご存知でしたら、教えていただけるとありがたいです。

 

テンプレートを表示するだけのViewのテスト

ステータスコードを見ているだけです。

テスト対象のURLを @pytest.mark.parametrize で渡していますが、単に使いたかっただけですので、深い意味はありません。

class TestViewForHighcharts:
    """ HighchartsのViewテスト """

    @pytest.mark.parametrize('url, expected_status_code', [
        (reverse('highcharts:total'), 200),
        (reverse('highcharts:total_by_month'), 200)
    ])
    def test_it(self, client, url, expected_status_code):
        actual = client.get(url)
        assert actual.status_code == expected_status_code

 

JSONをレスポンスするViewのテスト

どんな形のJSONが返ってくるのか忘れていたため、JSON文字列自体をハードコーディングしました。

また、使い回しが聞くように、 pytest.fixture で定義しておきました。

@pytest.fixture
def total_apples_expected():
    for i in range(3):
        TweetsFactory(name='フジ')
    for i in range(2):
        TweetsFactory(name='シナノドルチェ')
    for i in range(5):
        TweetsFactory(name='シナノゴールド')

    return '''[
  {
    "name": "シナノドルチェ",
    "y": 2,
    "color": "AntiqueWhite"
  },
  {
    "name": "シナノゴールド",
    "y": 5,
    "color": "Gold"
  },
  {
    "name": "フジ",
    "y": 3,
    "color": "Red"
  }
]'''

 
あとは、レスポンスボディに対して、期待したJSONかのassertするテストを書きました。

エラーが起きたらassertで落ちるだろうと思い、ステータスコードチェックは省略しました。

@pytest.mark.django_db(transaction=True)
class TestTotalApples:
    """ total_apples() のテスト """

    def test_get(self, client, total_apples_expected):
        actual = client.get('/api/v1/total/')
        assert actual.content.decode('utf-8') == total_apples_expected

 
なお、テスト対象のURLは、上記のような reverse() は使わずにハードコーディングしています。

テスト中のURLはどちらで書くのが一般的なのか、ご存知でしたら教えていただけるとありがたいです。

 

Djangoカスタムコマンドのテスト

Djangoカスタムコマンドについては、2種類のテストを書きました。

1つは、Djangoコマンド内部で呼ばれている各メソッドのテストです。

fixtureやfactory-boy、pytestプラグインなどを使ってテストを書きました。

Status = namedtuple('Status', ('id', 'text', 'created_at'))


@pytest.fixture
def cultivars():
    return [
        {'Name': 'シナノゴールド', 'Color': 'Gold'},
        {'Name': 'シナノドルチェ', 'Color': 'Red'},
        {'Name': '王林', 'Color': 'Yellow'},
    ]


@pytest.mark.freeze_time("2019-01-01")
@pytest.mark.django_db(transaction=True)
class TestGatherTweets:
    def test_save_with_transaction_該当ツイート無し_LastSearchありの場合(self, cultivars):
        from apps.tweets.management.commands.gather_tweets import Command
        from apps.tweets.models import Tweets, LastSearch

        sut = Command()
        sut.cultivars = cultivars
        sut.last_search = LastSearchFactory()

        # Twitterの仕様では、新しいものから順に取得できる(添字が小さいほど、最近の投稿になる)
        statuses = [
            Status(id=3, text='リンゴ 今日は `シナノゴールド` を食べた', created_at=datetime.now()),
            Status(id=2, text='[リンゴ] 今日はシナノゴールドを食べた', created_at=datetime.now()),
            Status(id=1, text='[りんご] 今日は `シナノゴールド` を食べた', created_at=datetime.now()),
        ]
        sut.save_with_transaction(statuses)

        assert Tweets.objects.count() == 0

        actual = LastSearch.objects.all()
        assert len(actual) == 1
        assert actual.first().prev_since_id == 3

 
もう1つは、Djangoカスタムコマンド自体を実行して、期待する動作をしているかのテストです。
testing - How to test custom django-admin commands - Stack Overflow

こちらはテストデータを用意するのが手間でした。そのため、モックを使って、想定したメソッドが呼ばれているかをチェックするようなテストを書きました*1

def test_call_command_Djangoコマンドを直接呼ぶ(self):
    from apps.tweets.management.commands import gather_tweets

    with patch.object(
            gather_tweets, 'Apple', return_value=Mock()) as apple, \
        patch.object(
            gather_tweets.Command, 'get_last_search') as mock_get, \
        patch.object(
            gather_tweets.Command, 'gather_tweets', return_value='foo') as mock_gather, \
        patch.object(
            gather_tweets.Command, 'save_with_transaction') as mock_save:

        # 作成したDjangoカスタムコマンドを直接呼ぶ
        call_command('gather_tweets')

        # コマンドの内部で呼ばれるはずのメソッドが、想定通り呼ばれているかをチェック
        mock_get.assert_called_with()
        mock_gather.assert_called_with()
        mock_save.assert_called_with('foo')  # mockで foo を返しているので、それが使われるか

 

TwitterやSlack APIのテスト

このアプリでは以下を行っていました。

  • Twitter APIを使って、ツイートを取得
  • Slack APIを使って、エラー情報をポスト

 
ただ、pytestを実行すると毎回APIを呼んでしまうのは良くなさそうです。

そのため、以前の記事を参考にしつつ、pytestのコマンドラインオプションで指定された場合のみ、テストを実行するようなfixtureを conftest.py に書きました。
Python + pytestにて、pytestに独自のコマンドラインオプションを追加する - メモ的な思考的な

import pytest

def pytest_addoption(parser):
    parser.addoption('--twitter', action='store', type=int,
                     help='Twitterのテストを実行する(要: Twitterの設定)。値は取得を開始する status_id。'
                     '最新の status_id だとテストがコケるので、3ツイートよりも前の status_id をセットする'
                     )

@pytest.fixture(scope='session')
def twitter(request):
    status_id = request.config.getoption('--twitter')
    if not status_id:
        pytest.skip()
    return status_id

 

基盤を最新版へアップデート

テストコードができたので、PythonDjangoを最新版へアップデートします。

手元の環境には pyenv が入っていたので、Pythonのバージョンを変更した上で、再度 venv で仮想環境を作成しました。

その後、各ライブラリの最新版をインストールしました。

 

Djangoを1.8.5から2.1.5へアップデートした後の対応

Django1系から2系への切替なので、いくつか不具合が発生しています。

テストコードを実行しつつ、その不具合を1つずつ潰していきます。

コミットはこのあたりです。
https://github.com/thinkAmi/dj_ringo_tabetter/commit/ed3d5ee86fa0f67d5129331ed29097a725ee6873

 

urls.pyの修正

Django1.8系では次のような書き方でした。

urlpatterns = patterns('',
                       url(r'^v1/total/$', views.total_apples),
                       url(r'^v1/month/$', views.total_apples_by_month),
                       ) 

 
Django2.1.5では、

  • patterns()url() が廃止
  • app_name が必要

なため、以下のように修正しました。

app_name = 'api'

urlpatterns = [
    path('v1/total/', views.total_apples, name='total'),
    path('v1/month/', views.total_apples_by_month, name='total_by_month'),
]

 

また、Djangoプロジェクトで admin ページのURL設定も変更となっていました。

# 1.8.5
url(r'^admin/', include(admin.site.urls)),

# 2.1.5
path('admin/', admin.site.urls),

 
なお、 include() まわりは、過去記事を参照して修正しています。
Django2.0のプロジェクトのurls.pyにおける、include()での引数namespaceについて調べてみた - メモ的な思考的な

 

settings.pyの修正

修正量が多いような気がしたので、

  • 別環境に、キレイなDjango 2.1.5 プロジェクトを作成
  • 上記で作成した settings.py に、Django1.8.5のsettingsの内容を反映

という手段を取りました。

テストがあることと、たいしたアプリではないことから、思い切ってやれました。

大きな変化は、

  • MIDDLEWARE_CLASSES が廃止、 MIDDLEWARE へと変更
    • MIDDLEWARE の先頭に、django.middleware.security.SecurityMiddleware を指定
  • AUTH_PASSWORD_VALIDATORS が新設

あたりでした。

 

Whitenoise v4.0の破壊的変更に対応

DjangoをHerokuで使う時の静的ファイルの配信に、 Whitenoise を使っていました。

ただ、v4.0より破壊的変更が入りました。
http://whitenoise.evans.io/en/stable/changelog.html#v4-0

 
そのため、v4.0以降でも動作するよう、以下の修正を行いました。

  • ミドルウェアwhitenoise.middleware.WhiteNoiseMiddleware を追加
  • settings.pyで STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' に差し替え
  • wsgi.pyの以下の設定を削除
from whitenoise.django import DjangoWhiteNoise
application = DjangoWhiteNoise(application)

 

render_to_response() を render() に差し替え

実行ログを見ていると、以下のような RemovedInDjango30Warning が出ていました。

RemovedInDjango30Warning: render_to_response() is deprecated in favor of render(). It has the same signature except that it also requires a request.

 
そのため、 render_to_response() の箇所を render() に変えました。

両者の関数の違いは、 request オブジェクトの有無が大きいようです。
Django - Why should I ever use the render_to_response at all? - Stack Overflow

 

docstring や type hints の整備

細かいところですが、今までの書き方では情報が不足していたため追加しました。

なお、APIの戻り値など、動的に型を生成しているところは、type hints を記述しませんでした。

こんな感じです。

from typing import List

def render_json_response(request, data: List[dict], status=None) -> HttpResponse:
    ...

 

リファクタリング

テストがパスすることを確認しながら今までの作業を行ってきました。

そこで次は、リファクタリングをして、よりメンテナンスしやすい作りにします。

主な作業は以下のコミットにまとまっていますが、やったことを簡単にメモしていきます。
https://github.com/thinkAmi/dj_ringo_tabetter/commit/71d11ea7f3ada8b39c678451c010630031abe6ce

 

表示パフォーマンスの改善

動作が重くて仕方なかったため、まずはパフォーマンス改善に着手しました。

怪しいと思っていた箇所は2つです。

  • DB側で集計処理をするところ
  • DBから取得したデータをHighchartsで処理するところ

 
計測してみたところ、DBの集計処理は特に問題ありませんでした。一方、Highchartsの方は問題でした。

問題があったのは、以下の作りです。

$.getJSON('/api/v1/total', function(res){
    $.each(res, function (i, json) {
        chart.series[0].addPoint({
            name: json['name'],
            y: json['quantity'],
            color: json['color']
        });
    });
});

取得したJSONデータに含まれる配列を $.each() して、 addPoint() でチャートに追加していました。

 
そこで、 $.each()addPoint() するのではなく、取得したJSONを直接設定するように変更したところ、劇的に改善しました。

series: [{
    data: jsonData
}]

 

libs.cultivars を apps に移動

libs.cultivars は各Djangoアプリから呼び出すために作ったライブラリです。

ただ、Djangoプロジェクトの作法に従い、Djangoアプリケーションとして扱うようにしました。

以下の方法で、新規 Djangoアプリケーションを作成し、その中に移動しました。
windows - Django, can't create app in subfolder - Stack Overflow

# ディレクトリを作る
mkdir ./apps/cultivars

# startappする
$ python manage.py startapp cultivars ./apps/cultivars

 

Djangoっぽいアプリ化

Djangoアプリとしては、他にも以下の点が気になったので、修正しました。

 
また、趣味の範囲ですが、以下も行いました。

  • JSONを返すViewをクラスベースView化
    • Viewを継承して作成

 

トップページにアクセスした時のリダイレクトを追加

トップページにアクセスした場合、今まではエラーになっていました。URLを忘れてしまった時など、意外とつらかったです。

そこで、トップページにアクセスした場合は、Highchartsの集計ページへリダイレクトするよう、Djangoプロジェクトの urls.py に追加しました。

urlpatterns = [
    ...
    # トップにアクセスした時は、Highchartsの合計ページへリダイレクト
    path('', RedirectView.as_view(url=reverse_lazy('highcharts:total'))),
]

 

GitHubのSecurity alertへ対応

GitHubを見たところ、CVE-2017-18342 のSecurity alertが出ていました。
https://github.com/thinkAmi/dj_ringo_tabetter/network/alert/requirements.txt/pyyaml/open

 
PyYAMLのGitHubを見ると、すでにいくつかのissueとコメントがありました。

 
そのコメントより、 yaml.load() ではなく yaml.safe_load() を使えば良いとのことだったため、差し替えを行いました。

 
とはいえ、GitHub上からはalertが消えないため、PyYAMLの次のバージョンのリリースがあるといいなという状況です...

 

django-jinjaへの依存を削除

このアプリを作った当初は Jinja2 テンプレートがDjangoでは扱えなかったため、 django-jinja を追加して使っていました。

 
その後、Djangoでも Jinja2 テンプレートを扱えるようになり、Django2系からは context_processors の指定も可能になりました。
https://docs.djangoproject.com/en/2.1/topics/templates/#django.template.backends.jinja2.Jinja2

 
そこで今回、django-jinjaへの依存を削除してみました。

settings.pyの TEMPLATES では、以下を行いました。

  • BACKEND を差し替え
  • match_extension を削除
  • environment を追加
TEMPLATES = [
    {
        # 差し替え
        # 'BACKEND': 'django_jinja.backend.Jinja2',
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            
            # 削除
            # 'match_extension': '.jinja2',
            
            # 追加
            'environment': 'dj_ringo_tabetter.jinja2.environment',
        }
    },
]

 
次にDjangoプロジェクトディレクトリ (dj_ringo_tabetter/) の中に、 jinja2.py を作成します(上記の environment で指定したファイル)。

そのファイルに、Jinja2の設定を行います。何もしないと static などのテンプレートタグが扱えないためです。

作業としては、必要なテンプレートタグを jinja2.py の env.globals に追加します。

なお、テンプレート側では、 {% load static %} などの記述は不要です。

from django.templatetags.static import static
from django.urls import reverse

from jinja2 import Environment


# Djangoのtemplatetagsを利用できるように設定
# https://docs.djangoproject.com/en/2.1/topics/templates/#django.template.backends.jinja2.Jinja2
def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': static,
        'url': reverse,
    })
    return env

 

$.getJson()から、async/awaitなfetch()へと変更

Fetch API & async/awaitを使うことで、jQueryなしに非同期でJSONを取得する処理が書けます。

基本的にモダンブラウザでしか見ないので、今回修正してしまいます。

修正前

$.getJSON('/api/v1/total', function(res){
    // ...
});

修正後

const renderChart = async (url) => {
    const res = await fetch(url);
    return await res.json();
};

renderChart('/api/v1/month')
    .then(jsonData => {
        // ...
    });

参考:イマドキのJavaScriptの書き方2018 - Qiita

 

その他JavaScriptまわりの修正
  • BootstrapはCSSだけ使うだけでも十分だったため、jQueryへの依存も削除
  • BootstrapとHighchartsはCDNではなく、手元に保存したものを見るように変更

 

Heroku向けの設定変更

いくつかHeroku向けの設定を変更しました。

  • settings.py
    • ALLOWED_HOSTS = ['127.0.0.1', 'ringo-tabetter.herokuapp.com']
      • ローカルとHeroku上、両方ともアクセスできるようにするため
  • runtime.txt
    • python-3.7.2
      • Python3.7.2で動作させるため

 

Herokuへデプロイする準備

MacではHeroku経でプロイする準備をしていなかったため、セットアップを行いました。

まずは、以下の内容に従い、Heroku CLIをインストールします。 https://devcenter.heroku.com/articles/heroku-cli

今回は、homebrewでインストールしました。実行後、以下が表示され、問題なくインストールできました。

To use the Heroku CLI's autocomplete --
  Via homebrew's shell completion:
    1) Follow homebrew's install instructions https://docs.brew.sh/Shell-Completion
        NOTE: For zsh, as the instructions mention, be sure compinit is autoloaded
              and called, either explicitly or via a framework like oh-my-zsh.
    2) Then run
      $ heroku autocomplete --refresh-cache

  OR

  Use our standalone setup:
    1) Run and follow the install steps:
      $ heroku autocomplete

Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completions have been installed to:
  /usr/local/share/zsh/site-functions

 
続いて、Heroku CLIからログインします。

実行後、何かキーを押すとブラウザが開くので、ブラウでログインすればOKでした。

$ heroku login

 

次に、git remoteを追加します。

$ heroku git:remote -a ringo-tabetter
set git remote heroku to https://git.heroku.com/ringo-tabetter.git

 
念のため、git remoteを確認します。

$ git remote -v
heroku  https://git.heroku.com/ringo-tabetter.git (fetch)
heroku  https://git.heroku.com/ringo-tabetter.git (push)
origin  https://github.com/thinkAmi/dj_ringo_tabetter (fetch)
origin  https://github.com/thinkAmi/dj_ringo_tabetter (push)

 
これで準備が整いました。

今回はブランチを別にして開発していたため、以下を参考にHerokuへブランチをpushしました。
https://devcenter.heroku.com/articles/git#deploying-from-a-branch-besides-master

git push heroku feature/migrate-to-django2:master

 
ただ、以前のアプリはCeder-14を使っていたため、Python3.7系のアプリがデプロイできませんでした。
HerokuのCedar-14に、Python3.7系アプリをデプロイしたらエラーになった - メモ的な思考的な

 
そこで、上記の記事にある通り、HerokuのStackをアップデートしたところ、無事にデプロイできました。

 
以上が今回行った作業となります。

Heroku上のアプリが寝ている時以外は快適であり、個人的に満足しています。
https://ringo-tabetter.herokuapp.com/

f:id:thinkAmi:20190214235021p:plain

 

今回やらなかったこと

今のところはまだいいかなと考え、以下の内容は行いませんでした。

  • プロジェクトディレクトリ名を config などに修正すること
  • settings.pyを、開発用・本番用などに分割すること

 

ソースコード

GitHubに上げました。今回の修正したブランチ feature/migrate-to-django2 は残してあります*2
https://github.com/thinkAmi/dj_ringo_tabetter

*1:モックを使いたかっただけかもしれません...

*2:PyYAMLのCVE-2017-18342への対応は、気づくのが遅れたため、このブランチには含まれていません

HerokuのCedar-14に、Python3.7系アプリをデプロイしたらエラーになった

昔作成したHerokuアプリのPythonが3.4系だったため、Python3.7系で動作するようにアプリを修正し、Herokuへデプロイしたところ、

$ git push heroku feature/migrate-to-django2:master
Enumerating objects: 171, done.
Counting objects: 100% (171/171), done.
Delta compression using up to 4 threads
Compressing objects: 100% (130/130), done.
Writing objects: 100% (148/148), 420.86 KiB | 2.36 MiB/s, done.
Total 148 (delta 59), reused 1 (delta 1)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Found python-3.4.3, removing
remote: -----> Installing python-3.7.2
remote: -----> Installing pip
remote: -----> Installing requirements with pip
remote:  !     Push rejected, failed to compile Python app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !   Push rejected to ringo-tabetter.
remote:
To https://git.heroku.com/ringo-tabetter.git
 ! [remote rejected] feature/migrate-to-django2 -> master (pre-receive hook declined)

とエラーになりました。

 
その時に対応した内容をメモします。

 

目次

 

環境

 

対応

heroku logs コマンドでログを見ましたが、特に有用な情報はありませんでした。

$ heroku logs
...
app[web.1]: [INFO] Worker exiting (pid: 8)
app[web.1]: [INFO] Worker exiting (pid: 9)
app[web.1]: [INFO] Handling signal: term
app[web.1]: [INFO] Shutting down: Master
app[api]: Build failed -- check your build output: https://dashboard.heroku.com/apps/xxx/activity/builds/xxx

 
check your build output のURLを確認しましたが、pushした時のログと同じで、手がかりにはなりませんでした。

 

そんな中、Herokuアプリの情報を見ていたところ、そのアプリはCedar-14を使っていました。また、 upgrade のリンクもありました。

Herokuのヘルプを見たところ、Cedar-14はUbuntu14.04ベースであり、deprecatedになっていました。
Stacks | Heroku Dev Center

もしかしたら、deprecatedな環境だとPython3.7系は用意されていないのかもしれないと思い、Stackをアップグレードすることにしました。

 
そこで、Dashboardにて upgrade をクリックしたところ、表示が

Stack heroku-18 will replace cedar-14 on the next deploy

に変わりました。

 
次に、再度同じブランチをpushしたところ、問題なく完了しました。

$ git push heroku feature/migrate-to-django2:master
...
remote: Verifying deploy... done.
To https://git.heroku.com/ringo-tabetter.git

 
古いStackを使っていたのがダメだったようです。

ただ、公式ドキュメントによると、Cedar-14でPython3.7.2は使えるようです。何かのタイミングでしょうか... https://devcenter.heroku.com/articles/python-support#supported-runtimes