pip uninstall -r requirementsの動作を確認する

Pythonのパッケージ管理システムのpipでは、requirements.txtに必要なパッケージを記述しておくことで、

  • pip install -r requirements.txt で一括インストール
  • pip uninstall -r requirements.txt で一括アンインストール

ができます。

そんな中、pip uninstall -r requirements.txtの動作を確認したのメモを残しておきます。

 

環境

  • Windows10 x64
  • Python 3.5.1 32bit
  • pip 8.1.2

 

確認

依存パッケージやPython拡張モジュールのアンインストール

依存パッケージのあるFlaskと、Python拡張モジュールをビルドする必要のあるPyCryptoに依存しているPySNMPのアンインストールを確認します。

# virtualenv環境を作成
D:\Sandbox\pip_uninstall>virtualenv -p c:\python35-32\python.exe env
...
Installing setuptools, pip, wheel...done.
D:\Sandbox\pip_uninstall>env\Scripts\activate

# インストール前の状態確認
(env) D:\Sandbox\pip_uninstall>pip list
pip (8.1.2)
setuptools (23.0.0)
wheel (0.29.0)

# 依存パッケージのあるFlaskをインストール
(env) D:\Sandbox\pip_uninstall>pip install flask

# 依存パッケージのPyScryptoでビルドが必要な、PySNMPをインストール
(env) D:\Sandbox\pip_uninstall>pip install pysnmp

# インストール後
(env) D:\Sandbox\pip_uninstall>pip list
click (6.6)
Flask (0.11.1)
itsdangerous (0.24)
Jinja2 (2.8)
MarkupSafe (0.23)
pip (8.1.2)
ply (3.8)
pyasn1 (0.1.9)
pycrypto (2.6.1)
pysmi (0.0.7)
pysnmp (4.3.2)
setuptools (23.0.0)
Werkzeug (0.11.10)
wheel (0.29.0)

# requirements.txtファイルに書き出し
(env) D:\Sandbox\pip_uninstall>pip freeze > requirements.txt

# requirements.txtファイルの中身を確認
# インストール前にあった、pip, setuptools, wheelの記載なし
(env) D:\Sandbox\pip_uninstall>type requirements.txt
click==6.6
Flask==0.11.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
ply==3.8
pyasn1==0.1.9
pycrypto==2.6.1
pysmi==0.0.7
pysnmp==4.3.2
Werkzeug==0.11.10

# アンインストール
(env) D:\Sandbox\pip_uninstall>pip uninstall -r requirements.txt
Uninstalling click-6.6:
...
Proceed (y/n)? y
  Successfully uninstalled click-6.6
Uninstalling Flask-0.11.1:
...
Proceed (y/n)? y
...
# 以降、該当するパッケージについて、すべて "y" を入力

# 再度確認
# 記載のなかった、pip, setuptools, wheelだけが残った
(env) D:\Sandbox\pip_uninstall>pip list
pip (8.1.2)
setuptools (23.0.0)
wheel (0.29.0)

requirements.txtに記載されたパッケージは削除されています。

 

ローカルファイルからインストールしたパッケージのアンインストール

ローカルファイルpycrypto-2.6.1-cp35-none-win32.whlのインストールとアンインストールを確認します。

# Flaskをインストール
(env) D:\Sandbox\pip_uninstall>pip install flask

# requirements.txtに書き出しと、ローカルにあるPyCryptoの追加
(env) D:\Sandbox\pip_uninstall>pip freeze > requirements.txt
(env) D:\Sandbox\pip_uninstall>echo pycrypto-2.6.1-cp35-none-win32.whl >> requirements.txt

# requirements.txtの確認
(env) D:\Sandbox\pip_uninstall>type requirements.txt
click==6.6
Flask==0.11.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
Werkzeug==0.11.10
pycrypto-2.6.1-cp35-none-win32.whl

# PyCryptoはインストールされていない
(env) D:\Sandbox\pip_uninstall>pip list
click (6.6)
Flask (0.11.1)
itsdangerous (0.24)
Jinja2 (2.8)
MarkupSafe (0.23)
pip (8.1.2)
setuptools (23.0.0)
Werkzeug (0.11.10)
wheel (0.29.0)

# PyCryptoのインストールと確認
(env) D:\Sandbox\pip_uninstall>pip install -r requirements.txt
...
Successfully installed pycrypto-2.6.1

(env) D:\Sandbox\pip_uninstall>pip list
click (6.6)
Flask (0.11.1)
itsdangerous (0.24)
Jinja2 (2.8)
MarkupSafe (0.23)
pip (8.1.2)
pycrypto (2.6.1)
setuptools (23.0.0)
Werkzeug (0.11.10)
wheel (0.29.0)

# requirements.txtを使ったアンインストール
# "-y"オプションで、アンインストールの確認を省略
(env) D:\Sandbox\pip_uninstall>pip uninstall -r requirements.txt -y
Uninstalling click-6.6:
  Successfully uninstalled click-6.6
Uninstalling Flask-0.11.1:
  Successfully uninstalled Flask-0.11.1
Uninstalling itsdangerous-0.24:
  Successfully uninstalled itsdangerous-0.24
Uninstalling Jinja2-2.8:
  Successfully uninstalled Jinja2-2.8
Uninstalling MarkupSafe-0.23:
  Successfully uninstalled MarkupSafe-0.23
Uninstalling Werkzeug-0.11.10:
  Successfully uninstalled Werkzeug-0.11.10
Uninstalling pycrypto-2.6.1:
  Successfully uninstalled pycrypto-2.6.1

# ローカルからインストールされたものもアンインストールされた
(env) D:\Sandbox\pip_uninstall>pip list
pip (8.1.2)
setuptools (23.0.0)
wheel (0.29.0)

こちらも、requirements.txtに記載されたパッケージは削除されました。

 

参考

pipの使い方 (2014/1バージョン) — そこはかとなく書くよん。

Aipo構築済のサーバを2ndドメインコントローラに昇格したら、Aipoが起動しなくなった

Aipo構築済のサーバを2ndドメインコントローラに昇格ところ、Aipoが起動しなくなったため、対応した時のメモを残します。

目次

 

環境

 

対応の流れ

AipoTomcatやAipoPostgreSQLが動作しない

以下を参考に2ndドメインコントローラとして構築、再起動したところ、Aipoが起動しませんでした。
Windows インターフェイスを使用して追加のドメイン コントローラーをインストールする

 
Aipoマネージャーから起動しても動作せず、サービスを確認しても、AipoTomcatやAipoPostgreSQLが動作しませんでした。

また、イベントビューア(Windowsログ > システム)を見たところ、以下が出力されていました。

項目
ソース Service Control Manager Eventlog Provider
イベントID 7038
レベル エラー

AipoPostgreSQL サービスで、現在構成されているパスワードで .\aipo_postgres としてログオンできませんでした。次のエラーが原因です: ログオン失敗: ユーザー名を認識できないか、またはパスワードが間違っています。

 
AipoTomcatやAipoPostgreSQLのサービスログオンアカウントを確認したところ、

  • AipoTomcat: ローカルシステムアカウント
  • AipoPostgreSQL: .\aipo_postgres

でした。

 
ドメインメンバのWindows Serverをドメインコントローラに昇格させると、

のように、ローカルアカウントは削除されるとともに、アカウント自体はドメインアカウントへと引き継がれることから、このあたりにエラーの原因があると考えました。

   
そこで、サーバのローカルアカウントを確認してみました。

コントロールパネル > ユーザーアカウント > 詳細設定タブの詳細設定ボタンを押したところ、ドメインコントローラではこのスナップインは使用できませんと表示され、ローカルアカウントはいないようでした。

一方、ドメインのユーザを確認すると、aipo_postgresユーザ(所属はDomain Usersのみ)がいました。引き継がれているようです。

これにより、存在しないユーザがサービスログオンアカウントとなっているため、うまく動作しないのではと考えられました。

 
その場合、引き継がれたドメインユーザを使用してサービスを動かせば良さそうですが、以下のような症状が発生することも想定されたため、aipo_postgresユーザにDomain AdminsAdministratorsを追加しました。
6.0.1から6.0.3へのバージョンアップ失敗|無料グループウェア「アイポ」

また、元々ローカルアカウントであったaipo_postgresのパスワードが分からなかったため、任意のパスワードへと変更しました。

 
その後、AipoPostgreSQLサービスのプロパティにて

  • ログオンアカウントを、ドメイン名\aipo_postgres
  • ログオンアカウントのパスワードは、変更後のもの

をそれぞれ設定したところ、サービスが起動するようになりました。

 

AipoPostgreSQLサービスがすぐ止まる

AipoPostgreSQLサービスは一瞬起動しましたが、すぐに停止しました。

イベントビューア(Windowsログ > アプリケーション)を見たところ、以下が出力されていました。

項目
ソース PostgreSQL
イベントID 0
レベル エラー

FATAL: could not remove old lock file "postmaster.pid": Permission denied

HINT: The file seems accidentally left over, but it could not be removed. Please remove the file by hand and try again.

 
メッセージにあったpostmaster.pidについて調べたところ、以下がありました。

 
確認してみたところ、C:\aipo\dpl7020\postgresql\data\postmaster.pid にファイルがありました。

HINTにある通り、このファイルを削除します。

 
再度Aipoを起動してみたところ、イベントビューア(Windowsログ > アプリケーション)で別のエラーが出ていました。

項目
ソース PostgreSQL
イベントID 0
レベル エラー

PANIC: could not open control file "global/pg_control": Permission denied

 
ファイルが開けないエラーだったため、該当のファイルを探します。Windowsの場合、C:\aipo\dpl7020\postgresql\data\global\pg_control にありました。

ファイルのセキュリティタブを確認すると、不明なアカウントが1つありました。一方、ドメインアカウントのドメイン名\aipo_postgresに関するアクセス権は設定されていませんでした。

これらより、AipoPostgreSQLサービスでログオンしているユーザが、必要なファイルにアクセスできていないのではないかと考えました。おそらくですが、ドメインコントローラに昇格した際に失われた、ローカルユーザーアカウントのaipo_postgresが不明なアカウントなのかな、と。

 
この不明なアカウントがどこの階層から発生したのかを調べたところ、C:\aipo\dpl7020\postgresql\dataでした。

そのため、そのディレクトリ以下に対して、フルコントロールのアクセス権を持つドメイン名\aipo_postgresユーザを追加しました。

 
この状態で、再度Aipoマネージャーから起動したところ、Aipoが動作するようになりました。

 

まとめ

  • ドメインアカウントドメイン名\aipo_postgresの所属するグループに、Domain AdminsAdministratorsを追加
  • サービス停止しているのにC:\aipo\dpl7020\postgresql\data\postmaster.pidがあった場合、削除
  • C:\aipo\dpl7020\postgresql\dataディレクトリに、ドメイン名\aipo_postgresアカウントを、フルコントロール権限で追加

 

その他

PostgreSQLのアクセスに関する設定ファイルは、

C:\aipo\dpl7020\postgresql\data\pg_hba.conf

# TYPE  DATABASE    USER        CIDR-ADDRESS          METHOD

# IPv4 local connections:
host    all         all         127.0.0.1/32          trust
# IPv6 local connections:
host    all         all         ::1/128               trust

 
C:\aipo\dpl7020\tomcat\webapps\aipo\WEB-INF\datasource\dbcp-org001.properties

cayenne.dbcp.driverClassName=org.postgresql.Driver
cayenne.dbcp.url=jdbc:postgresql://localhost:5432/org001
cayenne.dbcp.username=aipo_postgres
cayenne.dbcp.password=<任意の文字列>
cayenne.dbcp.maxActive=100
cayenne.dbcp.minIdle=3
cayenne.dbcp.maxIdle=20
cayenne.dbcp.maxWait=10000

となっていました。

PostgreSQLに接続するアカウントと、Windowsサービスのログオンユーザは別のようだったので、これらのファイルは修正しませんでした。
Aipo7、postgresのパスワード|無料グループウェア「アイポ」

Python3 + Flask + PySNMP + Highcharts + Apache2.4で、PX-105のインク残量を取得・表示し、Gmailでインク残量を送信する

以前、Ruby + SinatraでPX-105のインク残量を取得・表示したことがありました。
Ruby + Sinatra + SNMPでPX-105のインク残量を取得・表示する - メモ的な思考的な

 
最近さわっているPythonでも同じことができないかと思い、Python3 + Flask + PySNMP + Highcharts + Apache2.4 + Gmail APIで実装してみました。

 
ソースコードGitHubへアップしてあります。
thinkAmi/printer-status-py

 

環境

  • Windows10 x64
  • Python 3.5.1 32bit
  • Flask 0.11
  • google-api-python-client 1.5.1
  • PySNMP 4.3.2
  • Highcharts
  • Apache 2.4.10
    • 他のアプリ向けのバーチャルホストがすでに設定されている前提
  • mod_wsgi 4.5.2

 

Flaskアプリについて

PX-105からPySNMPを使ってインク情報を取得する

大きな流れはRubyで行った時と同じです。

PySNMPで

g = getCmd(SnmpEngine(),
    CommunityData(config.COMMUNITY, mpModel=0),
    UdpTransportTarget((config.PRINTER_HOST_IPV4, 161)),
    ContextData(),
    ObjectType(ObjectIdentity("{mib_id}{tail}".format(**vars()))))
    
errIndication, errorStatus, errorIndex, varBinds = next(g)

とすると、変数varBindsにGET response値が設定されるため、欲しい情報はvarBinds[0][1]で取り出せます。

ただ、varBinds[0][1]の値はOctetStringオブジェクトであり、そのままでは扱いづらいことから、必要に応じてint()やstr()で変換します。

 
なお、Flaskには以下のようにconfigファイルから設定値を取得できる機能があります。
python - How to import from config file in Flask? - Stack Overflow

ただ、今回は後述の通り、タスクマネージャーから動かすPythonスクリプトでもその設定値を参照するため、使えませんでした。

 
また、ローカル変数の列挙・展開をするため、以下を参考にvars()を使いました。
vars()関数によるローカル変数の列挙 | Python Snippets

 

FlaskでHighchartsを使う

以下を参考に、Highchartsへ値を渡します。
Using Flask to output Python data to High Charts

今回、Highchartsの棒グラフを複数表示するため、chart用オブジェクトのリストをテンプレートへと渡しています。

main.py

from flask import Flask, render_template
from printer import PX105

app = Flask(__name__)
app.config.from_object('config')

class HighCharts(object):
    def __init__(self, chart_id, chart, series, xAxis, plot_options):
        self.chart_id = chart_id
        self.chart = chart
        self.series = series
        self.xAxis = xAxis
        self.plot_options = plot_options


@app.route("/")
@app.route("/px105")
def index():
    p = PX105()
    
    charts = []
    for i, tank in enumerate(p.tanks):
        id = "chart_{}".format(i)
        charts.append(HighCharts(
            chart_id = id,
            chart = {"renderTo": id, "type": "column", "height": 400,},
            series = [{"name": "使用済", "data": [100 - tank.rest_volume], "pointWidth": 40, "color": "gray"},
                      {"name": "インク残量", "data": [tank.rest_volume], "pointWidth": 40, "color": tank.color}],
            xAxis = {"categories": [tank.name]},
            plot_options = { "column": { "stacking": "percent"}},
        ))
        
    return render_template("index.html", charts=charts, title="PX-105")


if __name__ == "__main__":
    app.run(debug = True, host="0.0.0.0", port=8080, passthrough_errors=True)

 
Highchartsで描画する部分のテンプレートは以下の通りです。

templates\index.html

<div id="container">
    {% for chart in charts %}
        <div id={{ chart.chart_id|safe }} class="chart" style="height:400px; width: 200px;"></div>
        
        <script>
            $(document).ready(function() {
                var options = {
                    "title": { "text": null },
                    "legend": { "layout": "vertical" },
                    "yAxis": { "title": { "text": null } },
                    "credits": { "enabled": false },
                    "chart": {{ chart.chart|safe }},
                    "xAxis": {{ chart.xAxis|safe }},
                    "series": {{ chart.series|safe }},
                    "plotOptions": {{ chart.plot_options|safe }},
                };
                $({{ chart.chart_id|safe }}).highcharts(options);
            });
        </script>
    {% endfor %}
</div>

 
static\style.css

div.chart {
    display: table-cell;
}

 
ここまででローカルで実行する準備ができたため、

(env) D:\printer_status_py>python main.py

のようにしてFlaskを起動し、http://localhost:8080/へアクセスして、動作を確認します。

 

Apacheで動作させる

以下を参考に、ApacheでFlaskを動かす設定を行います。

 

app.wsgiファイルの作成

Flaskのドキュメントに従って記述します。

app.wsgi

import sys, os

sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))

from main import app as application

 

Apache用のconf\extra\httpd-vhosts.conf へ追加

すでにApache2.4がバーチャルホストで動作している環境へと追加するため、以下の項目を httpd-vhosts.conf ファイルへと追加します。

...
# 既存のWSGIPythonPathに、今回のアプリとvirtualenv環境を追加
# Windowsなので、区切りはセミコロン(;)
WSGIPythonPath "...;D:\printer_status_py;D:\printer_status_py/env/Lib/site-packages"

<VirtualHost *:80>
    # DNSのCNAMEで追加した名前
    ServerName px105-status
    
    ErrorLog "logs/printer-status-error.log"

    # Flask app
    WSGIScriptAlias / "D:/printer_status_py/app.wsgi"
    <Directory "D:/printer_status_py">
        <Files app.wsgi>
            Require all granted
        </Files>
    </Directory>
</VirtualHost>

 
あとはApacheのサービスを再起動して、http://px105-status/にアクセスした時に、PX-105のインク残量が確認できればOKです。

 

インク残量の定期メール送信

Ruby + Sinatraではrufus-schedulerを使っていたので、Flaskでも同じことができないかを探したところAPSchedulerがありました。Flask用もあり良さそうでした。

ただ、mod_wsgiを使っている環境だとダメそうでした。
python: APScheduler in WSGI app - Stack Overflow

 
そのため、Windowsのタスクスケジューラーを使って、メール送信スクリプトを定期的に叩くようにしました。なお、スクリプトは以前の記事を参考に作成します。
Python3 + google-api-python-clientで、Gmail APIを使ってメールを送信する - メモ的な思考的な

タスクスケジューラのタスクの作成にて設定する内容は以下の通りです。

  • 操作タブのプログラム/スクリプトは、virtualenv環境のpython.exe
  • 操作タブの引数の追加は、定期メール送信のPythonスクリプト(gmai.reminder.py)

をそれぞれフルパスで指定します。

タブ 項目
全般 名前 printer-status (任意)
全般 タスクの実行時に使うユーザーアカウント 任意のアカウント
全般 ユーザーがログオンしているかどうかにかかわらず実行する ●選択
全般 最上位の特権で実行する ■チェックする
トリガー 開始 毎日
トリガー 間隔 1日
操作 操作 プログラムの開始
操作 プログラム/スクリプト D:\printer_status_py\env\Scripts\python.exe
操作 引数の追加 D:\printer_status_py\gmail_reminder.py
操作 開始 空白

 
タスクを作り終えたら、タスクを実行して問題なく動作するかを確認します。

 

ソースコード

GitHubに上げました。
thinkAmi/printer-status-py

 

その他参考

Flask でアプリケーションを作る際のメモ(2015 年版) - Memo

Windows + Python3 + PySNMPで、SNMPのGET Requestを送信する

Windows + Python3でSNMPを扱おうとしたところ、PySNMPが動きそうでした。

そこで、公式ドキュメントや書籍を見て手を動かした時のメモを残します。

なお、SNMPデータの取得元として、今回は昔使ったプリンタPX-105のインク本数を取得してみました。
Ruby + Sinatra + SNMPでPX-105のインク残量を取得・表示する - メモ的な思考的な

 

環境

  • Windows10 x64
  • Python 3.5.1 32bit
  • PySNMP 4.3.2

なお、PySNMPはPyCryptoに依存しています。

そのため、WindowsのPython3.5の環境にインストールするにはVisual Studio 2015、または、Visual C++ Build Tools 2015を事前にインストールしておきます。
Pythonの拡張モジュールインストール時にvcvarsall.batエラーが出たため、Visual C++ Build Tools 2015でビルドした - メモ的な思考的な

 

D:\Sandbox\pysnmp_sample>virtualenv -p c:\python35-32\python.exe env
D:\Sandbox\pysnmp_sample>env\Scripts\activate

(env) D:\Sandbox\pysnmp_sample>pip install pysnmp

(env) D:\Sandbox\pysnmp_sample>pip list
pip (8.1.2)
ply (3.8)
pyasn1 (0.1.9)
pycrypto (2.6.1)
pysmi (0.0.7)
pysnmp (4.3.2)
setuptools (22.0.5)
wheel (0.29.0)

 

GET requestの送信

PySNMPのドキュメントを読んだところ、実装方法として、

の2つがありました。

どちらを試そうかと考えましたが、ドキュメントには

Unless you have a vary specific task, one of high-level APIs might solve your SNMP needs.

と書かれていたため、今回はHigh-level SNMPだけ試してみます。

ドキュメントによると、基本的な流れは、

  • pysnmp.hlapi.getCmd()でコマンドを作成
  • 作ったコマンドをnext()で実行

のようでした。

 
pysnmp.hlapi.getCmd()ではいくつか引数がありました。
GET command — PySNMP

今回設定した内容は

でした。

実装と実行結果は以下の通りです。

import pysnmp.hlapi
OID_MARKER_PROCESS_COLORANTS = "1.3.6.1.2.1.43.10.2.1.6.1.1"

g = pysnmp.hlapi.getCmd(
    pysnmp.hlapi.SnmpEngine(),
    pysnmp.hlapi.CommunityData(SNMP_COMMUNITY, mpModel=0),
    pysnmp.hlapi.UdpTransportTarget((IP, PORT)),
    pysnmp.hlapi.ContextData(),
    pysnmp.hlapi.ObjectType(pysnmp.hlapi.ObjectIdentity(OID_MARKER_PROCESS_COLORANTS))
)
    
errIndication, errorStatus, errorIndex, varBinds = next(g)

print(varBinds)
# => [ObjectType(ObjectIdentity(ObjectName('1.3.6.1.2.1.43.10.2.1.6.1.1')), Integer32(4))]

 
また、ドキュメントには書かれていないものの、この本に書かれていたものも試してみます。
Pro Python System Administration - Apress IT eBooks & Books

from pysnmp.entity.rfc3413.oneliner import cmdgen
OID_MARKER_PROCESS_COLORANTS = "1.3.6.1.2.1.43.10.2.1.6.1.1"

cg = cmdgen.CommandGenerator()
errIndication, errorStatus, errorIndex, varBinds = cg.getCmd(
    cmdgen.CommunityData('my-manager', SNMP_COMMUNITY, mpModel=0),
    cmdgen.UdpTransportTarget((IP, PORT)),
    OID_MARKER_PROCESS_COLORANTS
)

print(varBinds)
# => [ObjectType(ObjectIdentity(ObjectName('1.3.6.1.2.1.43.10.2.1.6.1.1')), Integer32(4))]

同じように動作しました。

 
前者と後者、どちらを使うのが良いのか分からなかったため、PySNMPのソースコードを読んでみました。ただ、いくつも同じようなファイルやソースコードが並んでいて読み解くのが難しかったこともあり、諦めました。

ドキュメントに記載されている分、前者のほうが良いのでしょうか。

 

ソースコード

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

 

Windowsでは扱えないもの

PySNMP以外にもPythonライブラリはありましたが、いずれもWindowsで使うのは難しそうでした。

 

Snimpy

SNMPを扱うためにPySNMPを使っているものの、PyPIにOSとしてPOSIXが必要そうだったため、諦めました。

 

easysnmp

NET-SNMPがベースなため、Windowsでは厳しそうです。

Python3 + google-api-python-clientで、Gmail APIを使ってメールを送信する

前回はPythonの標準ライブラリを使って、Gmailからメールを送信しました。
Python3.5 + smtplib.SMTP_SSL.send_message()で、Gmailからメールを送信する - メモ的な思考的な

 
ただ、Googleアカウント名とパスワードをスクリプト上に書いておく必要があったため、何となく嫌な感じでした。

調べてみたところ、GmailGmail APIを使うことで、Googleアカウントのパスワードの記載が不要になることがわかったため、今回試してみます。

 
目次

 

環境

 
Google製のライブラリgoogle-api-python-clientは現在Python3をサポートしているものの、

という点に注意します。

 

事前準備

Python Quickstartを参考に、事前準備を行います。
Python Quickstart  |  Gmail API  |  Google Developers

 

Google Developers Console で Gmail API を有効化

Quickstartのリンク(リンク先:API を有効にする)より、以下の流れで登録を行います。

認証情報の作成ではどの形を選択すれば良いのか悩みましたが、Quickstartを参考にウィザードで選択する形を取りました。Webアプリ化する場合などは別の方法になるかと思います。

  • 新しいプロジェクトを作成続行My Projectプロジェクトが作成
    • しばらく待つと「プロジェクトが作成され Gmail API が有効化されました。」
  • 認証情報に進むをクリック
  • 認証情報を作成をクリック
  • ウィザードで選択をクリック
    • 使用するAPI: Gmail API
    • APIを呼び出す場所: その他のUI (Windows、CLIツールなど)
    • アクセスするデータの種類: ユーザーデータ
    • 必要な認証情報をクリック
  • OAuth 2.0 クライアント ID を作成
    • 名前: MyGmailSender
    • クライアントIDの作成をクリック
  • OAuth 2.0 同意画面を設定
    • メールアドレス: 自分のGmailアドレス
    • ユーザーに表示するサービス名: MyGmailSender Auth
    • 次へをクリック
  • 認証情報をダウンロード
    • ダウンロードをクリック、client_id.jsonファイルをダウンロード
      • 画面に表示されているClient IDは、jsonファイルの中にも記載あり
    • 完了をクリック

 

アプリケーション環境の準備

Googleのドキュメントでは、ユーザのディレクトリにclient_secret.jsonファイルを保管していましたが、今回はアプリのディレクトリに保管します。

# Python + google-api-python-client環境の構築
>virtualenv -p c:\python35-32\python.exe env
>env\Scripts\activate
(env) >pip install google-api-python-client

# ダウンロードしたJSONファイルをコピーしてリネーム
(env) >copy %USERPROFILE%\Downloads\client_id.json .\
        1 個のファイルをコピーしました。
(env) >ren client_id.json client_secret.json

 

実装

認証部分

認証部分については、Python Quickstartにあるget_credentials()関数を参考にします。
Python Quickstart  |  Gmail API  |  Google Developers

今回はclient_secret.jsonファイルをスクリプトと同じディレクトリに置いたため、その部分は変更しておきます。

また、今回はメールを送信するだけなので、SCOPE定数の値をhttps://www.googleapis.com/auth/gmail.sendへと変更しておきます。
Choose Auth Scopes  |  Gmail API  |  Google Developers

SCOPES = "https://www.googleapis.com/auth/gmail.send"

def get_credentials():
    script_dir =os.path.abspath(os.path.dirname(__file__)) 
    credential_dir = os.path.join(script_dir, ".credentials")

    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir,
                                   "my-gmail-sender.json")

    store = oauth2client.file.Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = oauth2client.client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        credentials = oauth2client.tools.run_flow(flow, store, flags)
        print("Storing credentials to " + credential_path)
    return credentials

 

メッセージの作成

公式ドキュメントを参考に作成します。
Users.messages: send  |  Gmail API  |  Google Developers

 
Request bodyのrawプロパティへ設定する際、エンコードする必要がありますが、公式ドキュメントでは2種類の方法が記載されていました。

エンコードされる内容が微妙に違うため、どちらが正しいのか調べましたが、後者のドキュメントを読んだ際に、

The entire email message in an RFC 2822 formatted and base64url encoded string.

https://developers.google.com/gmail/api/v1/reference/users/messages/send#auth

と、その後のサンプルコードと異なった解説が書かれていたことと、ライブラリのリファレンスや他の言語でもbodyはbase64urlでエンコードすると書かれていたことからも、base64.urlsafe_b64encode()を使うことにしました。

 
そこで、

def create_message():
    message = MIMEText("Gmail body: Hello world!")
    message['from'] = MAIL_FROM
    message['to'] = MAIL_TO
    message['subject'] = "gmail api test"
    message["Date"] = formatdate(localtime=True)
    return {'raw': base64.urlsafe_b64encode(message.as_string())}

としたところ、実行時に

TypeError: a bytes-like object is required, not 'str'

というエラーになりました。

Python3のドキュメントでは、bytes-like objectを引数に取ると書かれており、それが今回の原因と考えられました。
base64.urlsafe_b64encode(s) - 19.6. base64 — Base16, Base32, Base64, Base85 データの符号化 — Python 3.5.1 ドキュメント

message.as_string()の結果が文字列型となるため、これをバイト列型へと変換する必要があるようです。

変換するには、str.encode()を使います。

 
そこで、

def create_message():
...
    message["Date"] = formatdate(localtime=True)
    byte_msg = message.as_string().encode(encoding='UTF-8')
    return {'raw': base64.urlsafe_b64encode(byte_msg)}

のように変更し、

result = service.users().messages().send(
    userId=MAIL_FROM,
    body=create_message()
).execute()

APIを叩いたところ、

TypeError: b'Q29...==' is not JSON serializable

というエラーになりました。JSONシリアライズのところで失敗しているようです。

 
エラーとなったのは先ほどエンコードしたバイト列でした。バイト列はシリアライズできないようです。

Dive Into Python 3 日本語版には、バイト列をシリアライズさせる方法が記載されていますが、シリアライズはライブラリのこのあたりで行われているため手が出せません。

 
そこで、

  1. message.as_string().encode(encoding="UTF-8")でバイト列にエンコード
  2. base64.urlsafe_b64encode()でbase64urlでエンコード
  3. エンコード結果はバイト列のため、str.decode(encoding="UTF-8")で文字列にデコード
  4. 上記3.の結果、base64urlエンコードされた文字列が得られたので、rawプロパティへセット

の実装へと変更し、動作を確認しました。

 
ソースコード全体は以下の通りです。

import httplib2
import os

import apiclient
import oauth2client
import argparse
flags = argparse.ArgumentParser(
    parents=[oauth2client.tools.argparser]
).parse_args()

import base64
from email.mime.text import MIMEText
from email.utils import formatdate
import traceback

# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/gmail-python-quickstart.json
SCOPES = "https://www.googleapis.com/auth/gmail.send"
CLIENT_SECRET_FILE = "client_secret.json"
APPLICATION_NAME = "MyGmailSender"

MAIL_FROM = "example@gmail.com"
MAIL_TO = "example+alias@gmail.com"

def get_credentials():
    script_dir =os.path.abspath(os.path.dirname(__file__)) 
    credential_dir = os.path.join(script_dir, ".credentials")

    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir,
                                   "my-gmail-sender.json")

    store = oauth2client.file.Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = oauth2client.client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        credentials = oauth2client.tools.run_flow(flow, store, flags)
        print("Storing credentials to " + credential_path)
    return credentials


def create_message():
    message = MIMEText("Gmail body: Hello world!")
    message["from"] = MAIL_FROM
    message["to"] = MAIL_TO
    message["subject"] = "gmail api test"
    message["Date"] = formatdate(localtime=True)

    byte_msg = message.as_string().encode(encoding="UTF-8")
    byte_msg_b64encoded = base64.urlsafe_b64encode(byte_msg)
    str_msg_b64encoded = byte_msg_b64encoded.decode(encoding="UTF-8")

    return {"raw": str_msg_b64encoded}


def main():
    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = apiclient.discovery.build("gmail", "v1", http=http)

    try:
        result = service.users().messages().send(
            userId=MAIL_FROM,
            body=create_message()
        ).execute()

        print("Message Id: {}".format(result["id"]))

    except apiclient.errors.HttpError:
        print("------start trace------")
        traceback.print_exc()
        print("------end trace------")


if __name__ == "__main__":
    main()

 

ブラウザでの許可

コマンドラインからの初回実行時、途中でブラウザが起動し、

MyGmailSender Auth が次の許可をリクエストしています:

ユーザーに代わるメールの送信

という画面が表示されます。

ここで許可をクリックすると、The authentication flow has completed.が表示されて使えるようになります。

また、コマンドラインにも以下のような結果が表示されます。

(env) >python gmail_sender.py
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?access_type=offline...

If your browser is on a different machine then exit and re-run this application with the command-line parameter

  --noauth_local_webserver

Authentication successful.
Storing credentials to path\to\.credentials\my-gmail-sender.json

 

ソースコード

GitHubに上げておきました。gmail_sender.pyが今回のファイルです。
thinkAmi-sandbox/google-api-python-client-sample

 

その他

Gmail APIのリミットについて

以下に記載がありました。
Usage Limits  |  Gmail API  |  Google Developers

 

GoogleのOAuth2.0について

以下にまとまっていました。
Using OAuth 2.0 to Access Google APIs  |  Google Identity Platform  |  Google Developers

Python3.5 + smtplib.SMTP_SSL.send_message()で、Gmailからメールを送信する

Python3.5 + SMTPで、Gmailからメールを送信しようと考え調べてみたところ、Web上には色々な書き方がありました。

自分なりにいろいろと手を動かしてみて、Pythonの標準ライブラリであるsmtplib.SMTP_SSL.send_message()に行き着いた時のメモを残します。

ただ、自分なりの調査のため、もし誤りなどがあればご指摘いただけるとありがたいです。

 
なお、MIMETextの設定など、どのような書き方でも変わらない部分は、

import sys
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate

ACCOUNT = "example@gmail.com"
ALIAS = "example+alias@gmail.com"
PASSWORD = "passw0rd"

def create_message(subject):
    msg = MIMEText("Body: Hello world!")
    msg["Subject"] = subject
    msg["From"] = ACCOUNT
    msg["To"] = ALIAS
    msg["Date"] = formatdate(localtime=True)
    return msg

のような定数・関数を用意しています。

実際にテストするときは、定数の値を自分のアカウント情報へと変更します。

 
目次

 

環境

  • Windows10 x64
  • Python 3.5.1 32bit

 
今回、

  • emailパッケージのMIMETextを使って、emailメッセージを構築
  • smtplibモジュールを使って、Gmailからメールを送信

を行います。

 
また、どのコードから送信されたのかを分かりやすくするため、送信処理を行った関数名をメールのSubjectに入れました。
実行中の関数・メソッド名を取得したい - Qiita

ほかに、GitHub上のソースコードを行数付で参照していますが、masterだと行数が変わってしまうおそれがあるため、適当な最近のコミットを指定しています。

 

MIMETextのimportについて

MIMETextのimportでは2つの方法を見かけたため、両方を試してみたところ、

from email.MIMEText import MIMEText
# => ImportError: No module named 'email.MIMEText'

from email.mime.text import MIMEText
# => OK

前者ではImportErrorになりました。

ソースコードを見たところ、Python2.7系ではモジュールがあったものの、Python3.5系では削除されていました。

ちなみに、from email.mime.text import MIMETextは、Python2.7系でも使えそうでした。

 

SMTPオブジェクトを使う実装

starttls()の前後で、ehlo()を呼ぶ実装

公式ドキュメントを読むと、helo()connect()は不要そうだったので、それらのメソッドは使いませんでした。

 
また、終了するときに、close()quit()、どちらを使えば良いのか分かりませんでした。ただ、

  • 公式ドキュメントにはclose()の記載なし
  • ソースコードを読んだところ、quit()の中でclose()を呼んでいた

ということから、quit()を使うのが良さそうでした。

msg = create_message(sys._getframe().f_code.co_name)

s = smtplib.SMTP("smtp.gmail.com", 587)
s.ehlo()
s.starttls()
s.ehlo()
s.login(ACCOUNT, PASSWORD)

s.sendmail(ACCOUNT, ALIAS, msg.as_string())
s.quit()

 

ehlo()を使わない実装

starttls()の前後でehlo()を呼ぶ必要があるのかを調べたところ、

ということから、必要なさそうでした。

msg = create_message(sys._getframe().f_code.co_name)

s = smtplib.SMTP("smtp.gmail.com", 587)
s.starttls()
s.login(ACCOUNT, PASSWORD)

s.sendmail(ACCOUNT, ALIAS, msg.as_string())
s.quit()

print("finished: {}".format(sys._getframe().f_code.co_name))

 

SMTP_SSLオブジェクトを使う実装

starttls()メソッドでSTARTTLSコマンドを使って暗号化をしていました。ただ、Web上のいくつかのサンプルでは、SMTP_SSLオブジェクトを使っていました。
class smtplib.SMTP_SSL - 21.17. smtplib — SMTP プロトコルクライアント — Python 3.5.1 ドキュメント

SMTP_SSLクラスのソースコードを見たところ、

  • SMTPクラスを継承
  • ポートを指定しない場合、SMTP-over-SSL port (465)を使用
  • _get_socket()メソッドをオーバーライドして、SSLを使って通信

でした。

SMTP_SSLを使った場合、以下のような実装となりました。

msg = create_message(sys._getframe().f_code.co_name)

s = smtplib.SMTP_SSL("smtp.gmail.com")
s.login(ACCOUNT, PASSWORD)

s.sendmail(ACCOUNT, ALIAS, msg.as_string())
s.quit()

print("finished: {}".format(sys._getframe().f_code.co_name))

 

sendmail()の代わりにsend_message()を使う実装

MIMETextでFromやToを設定しているのにもかかわらず、sendmail()の引数にもFromやToを渡す必要があります。

良い方法がないかを調べたところ、Python3.2から追加されたsend_message()がありました。
SMTP.send_message - 21.17. smtplib — SMTP プロトコルクライアント — Python 3.5.1 ドキュメント

ソースコードを見たところ、内部でsendmail()を呼んでいたため、これを使うのが良さそうでした。

msg = create_message(sys._getframe().f_code.co_name)

s = smtplib.SMTP_SSL("smtp.gmail.com")
s.login(ACCOUNT, PASSWORD)

s.send_message(msg)
s.quit()

print("finished: {}".format(sys._getframe().f_code.co_name))

最初のものに比べるとかなり短くなりました。

 
なお、SMTPオブジェクトを使う場合でもsend_message()は使えます。

 

ソースコード

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

 

参考

SMTPまわりのPythonでの経緯とかが分かりやすく書かれていました。
プログラミングと慶應通信 : Pythonでgmailの送受信

受信側はこちらのコードが参考になりそうです。
Python でメール送受信(Gmail で SMTP と IMAP を使う場合) - akiyoko blog

なお、GmailにはAPIもあり、Pythonライブラリも用意されています。
Gmail API Client Library for Python  |  API Client Library for Python  |  Google Developers

公式ドキュメントで役立ちそうなのも載せておきます。

Pythonの拡張モジュールからexeファイルを作成後、whlファイルに変換し、別の端末でpip installする

前回は、Python3.5 + Visual C++ Build Tools 2015環境で、Pythonの拡張モジュールをビルドしました。

ただ、Vista世代の端末ではVisual Studio 2015系がインストールできないことから、Python3.5系向けの拡張モジュールがビルドできませんでした。

 
そこで今回は、自分でソースコードからビルドし、それをVista世代端末へ渡してインストールしてみます。

流れとしては、Visual C++ Build Tools 2015を入れた端末で

を行い、Vista世代端末で

  • pipを使って、whlファイルからインストール

となります。

 

環境

今回、ビルドする拡張モジュールとしてPyCryptoを使います。

 

ビルド端末

 

インストール端末

 

ビルド端末での作業

Windowsインストーラーの作成

以下を参考に、Windowsインストーラーを作成します。
5. ビルド済み配布物を作成する — Python 3.5.1 ドキュメント

今回はPyCryptoのソースコードGitHubから取得します。

# 環境作る
D:\Sandbox\pycrypto_build>virtualenv -p c:\python35-32\python.exe env
D:\Sandbox\pycrypto_build>env\Scripts\activate
(env) s:\Sandbox\pycrypto_build>pip list
pip (8.1.2)
setuptools (22.0.5)
wheel (0.29.0)

# GitHubのmasterだと別の理由でビルドエラーになる可能性があるため、リリースされている`v2.6.1`ブランチを使う
(env) D:\Sandbox\pycrypto_build>git clone -b v2.6.1 https://github.com/dlitz/pycrypto.git

# ビルド
(env) D:\Sandbox\pycrypto_build>cd pycrypto
(env) D:\Sandbox\pycrypto_build\pycrypto>python setup.py bdist_wininst
...
creating dist
removing 'build\bdist.win32\wininst' (and everything under it)

# ビルドしたファイルの確認(distディレクトリの中)
# D:\Sandbox\pycrypto_build\pycrypto\dist\pycrypto-2.6.1.win32-py3.5.exe ファイルが作成されているはず
(env) D:\Sandbox\pycrypto_build\pycrypto>cd dist
(env) D:\Sandbox\pycrypto_build\pycrypto\dist>dir /b
pycrypto-2.6.1.win32-py3.5.exe

# ビルドだけで、インストールはされていない
(env) D:\Sandbox\pycrypto_build\pycrypto\dist>pip list
pip (8.1.2)
setuptools (22.0.5)
wheel (0.29.0)

 

whlファイルへの変換

Windowsインストーラー(exe)のままでは、pipでインストールできません。
Windows での Python 2.7, 3.4, 3.5 の拡張モジュールビルド環境 - Qiita

 
そこで、Wheelを使ってexeファイルからwhlファイルへと変換します。
Can I install Python windows packages into virtualenvs? - Stack Overflow

# 変換
(env) D:\Sandbox\pycrypto_build\pycrypto\dist>wheel convert pycrypto-2.6.1.win32-py3.5.exe
d:\sandbox\pycrypto_build\env\lib\site-packages\wheel\pep425tags.py:77: RuntimeWarning: Config variable 'Py_DEBUG' is unset, Python ABI tag may be incorrect
  warn=(impl == 'cp')):
d:\sandbox\pycrypto_build\env\lib\site-packages\wheel\pep425tags.py:81: RuntimeWarning: Config variable 'WITH_PYMALLOC' is unset, Python ABI tag may be incorrect
  warn=(impl == 'cp')):

# ファイルの確認
(env) D:\Sandbox\pycrypto_build\pycrypto\dist>dir /b
pycrypto-2.6.1-cp35-none-win32.whl
pycrypto-2.6.1.win32-py3.5.exe

 

インストール端末での作業

上記で作成したpycrypto-2.6.1-cp35-none-win32.whlを使って、pipでインストールします。

# 環境づくり
D:\Sandbox\pycrypto_install>virtualenv -p c:\python35-32\python.exe env
D:\Sandbox\pycrypto_install>env\Scripts\activate
(env) D:\Sandbox\pycrypto_install>pip list
pip (8.1.2)
setuptools (22.0.5)
wheel (0.29.0)

# ファイルの確認
(env) D:\Sandbox\pycrypto_install>dir /b
env
pycrypto-2.6.1-cp35-none-win32.whl

# PyPIからでは失敗することを確認
(env) D:\Sandbox\pycrypto_install>pip install pycrypto
...
    c:\python35-32\include\pyconfig.h(68): fatal error C1083: Cannot open include file: 'io.h': No such file or directory
    error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\BIN\\cl.exe' failed with exit status 2

# まだインストールされていない
(env) D:\Sandbox\pycrypto_install>pip list
pip (8.1.2)
setuptools (22.0.5)
wheel (0.29.0)

# 先ほど作成したwhlファイルからインストール
(env) D:\Sandbox\pycrypto_install>pip install pycrypto-2.6.1-cp35-none-win32.whl
...
Successfully installed pycrypto-2.6.1

# インストールの確認
(env) D:\Sandbox\pycrypto_install>pip list
pip (8.1.2)
pycrypto (2.6.1)
setuptools (22.0.5)
wheel (0.29.0)

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