2014年3月31日月曜日

FilterでHTTP通信のGZIP圧縮に対応する

HTTP/1.1では仕様に電文の圧縮が盛り込まれています。
今回はサーブレットフィルタでGZIP圧縮に対応してみたいと思います。

必要なクラス
圧縮されたHTTPリクエストの解凍と、HTTPレスポンスの圧縮を実現するには、次の5つのクラスが必要です。

  1. GZIPFilter - Filterを実装します。HTTPヘッダを確認し、圧縮/解凍をするかどうか判断します。
  2. GZIPRequest - HttpServletRequestWrapperを継承します。GZIPServletInputStreamを返します。
  3. GZIPServletInputStream - HTTPリクエストを解凍するInputStreamです。
  4. GZIPResponse - HttpServletResponseWrapperを継承します。GZIPServletOutputStreamを返します。
  5. GZIPServletOutputStream - HTTPレスポンスを圧縮するOutputStreamです。
ほか、リクエストをそのままレスポンスするEchoServletを準備します。

2014年3月24日月曜日

PythonでEvernoteへメールを送る - 添付ファイル追加

PythonでEvernoteへメールを送る」で載せたプログラムへ、ファイル添付機能を追加しました。
合わせてクラス設計を見直しました。

# -*- coding: utf-8 -*-

# ---- CONFIGURATIONS ----
EVERNOTE_MAIL_ADDRESS = 'evernote_mail_address'
FROM_ADDRESS = 'from_address'
SMTP_HOST = 'smtp_host'
SMTP_PORT = 25
# ---- CONFIGURATIONS ----

import datetime
import mimetypes
import optparse
import os.path
import smtplib
import sys

from email import encoders
from email.header import Header
from email.mime.audio import MIMEAudio
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

class EverMailClient(object):
 def __init__(self, smtp):
  assert isinstance(smtp, smtplib.SMTP)
  self._smtp = smtp
  
 def send(self, mail):
  assert isinstance(mail, EverMail)
  msg = mail.as_MIME()
  mail_from = msg['From']
  mail_to = msg['To']
  self._smtp.sendmail(mail_from, mail_to, msg.as_string())


class EverMail(object):
 def __init__(self, title, from_address, to_address, body, attached_files):
  assert isinstance(title, EverMailTitle)
  assert isinstance(body, EverMailBody)
  self._title = title
  self._from_address = from_address
  self._to_address = to_address
  self._body = body
  self._attached_files = attached_files
  
 def as_MIME(self):
  msg = MIMEMultipart()
  msg['Subject'] = Header(str(self._title), 'utf-8')
  msg['From'] = self._from_address
  msg['To'] = self._to_address
  msg.attach(self._body.as_MIME())
  for attached_file in self._attached_files:
   msg.attach(attached_file.as_MIME())
  return msg


class EverMailTitle(object):
 def __init__(self, title, notebook, tags):
  self._title = title if title else str(datetime.datetime.now())
  self._notebook = notebook if notebook else ''
  self._tags = tags if tags else []
  
 def __str__(self):
  result = self._title
  if 0 < len(self._notebook):
   result += ' @' + self._notebook
  for tag in self._tags:
   result += ' #' + tag
  return result


class EverMailBody(object):
 def __init__(self, stdin):
  assert hasattr(stdin, 'read')
  self._stdin = stdin
  
 def as_MIME(self):
  body = sys.stdin.read()
  msg = MIMEText(bytes(body))
  msg.set_charset('utf-8')
  return msg

  
class AttachedFile(object):
 def __init__(self, path):
  self._path = path
  
 def as_MIME(self):
  ctype, encoding = mimetypes.guess_type(path)
  if ctype is None or encoding is not None:
   ctype = 'application/octet-stream'
  maintype, subtype = ctype.split('/', 1)
  msg = None
  with open(self._path, 'rb') as f:
   if maintype == 'text':
    msg = MIMEText(f.read(), _subtype=subtype)
   elif maintype == 'image':
    msg = MIMEImage(f.read(), _subtype=subtype)
   elif maintype == 'audio':
    msg = MIMEAudio(f.read(), _subtype=subtype)
   else:
    msg = MIMEBase(maintype, subtype)
    msg.set_payload(f.read())
    encoders.encode_base64(msg)
  file_name = os.path.basename(self._path) 
  msg.add_header('Content-Disposition', 'attachment', filename=file_name)
  return msg


class EverMailOptionParser(optparse.OptionParser):
 def __init__(self):
  optparse.OptionParser.__init__(self)
  self.add_option('--notebook', '-n', action='store')
  self.add_option('--tag', '-t', action='append')

    
def create_smtp_client():
 smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
 smtp.set_debuglevel(1)
 return smtp
 
 
if __name__ == '__main__':
 option_parser = EverMailOptionParser()
 (options, args) = option_parser.parse_args()
 note_title = args[0] if args and 0 < len(args) else ''
 title = EverMailTitle(note_title,
                       options.notebook,
                       options.tag)
 body = EverMailBody(sys.stdin)
 attached_files = []
 if 2 <= len(args):
  attached_files = [AttachedFile(path) for path in args[1]]
 mail = EverMail(title,
                 FROM_ADDRESS,
                 EVERNOTE_MAIL_ADDRESS,
                 body,
                 attached_files)
 
 smtp = None
 try:
  smtp = create_smtp_client()
  client = EverMailClient(smtp)
  client.send(mail)
 finally:
  if smtp: smtp.close()

複数ファイルを添付するとうまくいかないところが問題です。
また時間のあるときに対応したいと思います。

2014年3月21日金曜日

Emacsのブックマーク機能

Google+で流れてきたEmacsのチュートリアルを日本語に訳してみました。

Emacsのブックマーク機能を使う

このページはEmacsに搭載されたブックマーク機能のチュートリアルです。
Emacsのブックマーク機能は、ブラウザのそれによく似ています。
Emacsのブックマークは、しばしば必要になるファイルを簡単に開けるようにします。

ブックマークを使う
ファイルをブックマークへ追加する
追加したいファイルを開いてください。
続いて、bookmark-setを呼ぶ[C-x r m]と、名前を入力するプロンプトが表示されます。

ブックマークファイルを開く
ブックマークを開くには、bookmark-bmenu-listを呼びます[C-x r l]。
ブックマーク一覧が表示されます。
この一覧でEnterを入力するかクリックすれば、ファイルを開きます。
また、oを入力すれば分割されたウィンドウでファイルを開きます。

ブックマークを保存する
ブックマークを保存するには、bookmark-saveを呼びます(ブックマーク一覧でsを入力する)。
もし保存しなければ、新たに追加されたブックマークは現在のセッションでのみ有効です。
つまり、Emacsを再起動すると表示されなくなってしまいます。

ブックマークを削除または名前を変更する
ブックマーク一覧を開いた状態で、Dでマークすればブックマークを削除します。
また、rを入力すればブックマークの名前を変更します。

ブックマークのコマンドとショートカットを忘れないようにするには
ブックマークコマンドは、メニューのEdit->Bookmarksにあります。
コマンドやショートカットを忘れてしまったときは、メニューを見てください。
メニューはEmacsのコマンドを覚えるための偉大なる道です。

また、ブックマークコマンドはすべてbookmark-で始まります。
本当に覚えなければいけないのは、bookmark-bmenu-listだけです。
いったんブックマーク一覧を開いてしまえば、describe-mode[F1 m]を呼ぶことでコマンドとショートカットの一覧を見ることができます。

起動時にブックマークを表示する
起動時にブックマークを表示するには、次のelispを初期化ファイルへ追加してください。
(setq inhibit-splash-screen t)
(require 'bookmark)
(bookmark-bmenu-list)
(switch-to-buffer "*Bookmark List*")

ブックマークファイルの場所
emacs 24.xでは、ブックマークファイルは~/.emacs.d/bookmarksにあります。emacs 23.xでは、~/.emacs.bmkにあります。
デフォルトの場所は、変数bookmark-default-fileによって制御されます。describe-variable[F1 v]を呼び、見てみてください。
デフォルトの場所は、次のように設定できます。
(setq bookmark-default-file  (concat user-emacs-directory "bookmarks"))
ブックマークファイルの読み込みは、次のようにします。
(bookmark-load bookmark-default-file t)

ファイルを早く開くほかの方法
最近開いたファイルを開くのも便利な機能です。参考:Emacs: Open File Fast: recentf-mode
ブックマークの1つの問題は、ファイルを直接開くキーがないことです。しかし、Emacs Lisp: Hotkeys to Open File Fastで実現できます。

参考
Emacs: Using Bookmark Feature
Emacsのブックマーク機能

2014年3月17日月曜日

PythonでEvernoteへメールを送る

Evernoteへメールを送ることでノートを追加できるのはご存知かと思います。
最近、古いネットブックをUSBブートのLinuxで使用しています。ネットブック上で作成したファイルをEvernoteへアップロードしたいと思うことがあるのですが、ブラウザから操作するのは厳しいと感じています。
Pythonからメールでファイルを送るようにすればいいのではないかと思い、標準入力から読み込んだ内容をEvernoteへ送るプログラムを書きました。

プログラム
# -*- coding: utf-8 -*-
# ever_mail.py

# ---- CONFIGURATIONS ----
EVERNOTE_MAIL_ADDRESS = 'evernote_mail_address'
FROM_ADDRESS = 'from_address'
SMTP_HOST = 'smtp._host'
SMTP_PORT = 25
DEBUG = True
# ---- CONFIGURATIONS ----

import datetime
import email.header
import optparse
import smtplib
import sys

from email.mime.text import MIMEText

class EverMailClient(object):
    def __init__(self, smtp):
        assert isinstance(smtp, smtplib.SMTP)
        self._smtp = smtp

    def send(self, msg):
        assert isinstance(msg, MIMEText)
        mail_from = msg['From']
        mail_to = msg['To']
        self._smtp.sendmail(mail_from, mail_to, msg.as_string())


class EverMailTitle(object):
    def __init__(self, title, notebook, tags):
        self._title = title if title else str(datetime.datetime.now)
        self._notebook = notebook if notebook else ''
        self._tags = tags if tags else []

    def __str__(self):
        result = self._title
        if 0 < len(self._notebook):
            result += ' @' + self._notebook
        for tag in self._tags:
            result += ' #' + tag
        return result


class EverMailOptionParser(optparse.OptionParser):
    def __init__(self):
        optparse.OptionParser.__init__(self)
        self.add_option('--notebook', '-n', action='store')
        self.add_option('--tag', '-t', action='append')


def create_smtp_client():
    smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    if DEBUG:
        smtp.set_debuglevel(1)
    return smtp

def create_message_from_stdin(title):
    assert isinstance(title, EverMailTitle)
    body = sys.stdin.read()
    msg = MIMEText(bytes(body))
    msg['Subject'] = email.header.Header(str(title), 'utf-8')
    msg['From'] = FROM_ADDRESS
    msg['To'] = EVERNOTE_MAIL_ADDRESS
    msg.set_charset('utf-8')
    return msg


if __name__ == '__main__':
    option_parser = EverMailOptionParser()
    (options, args) = option_parser.parse_args()
    note_title = args[0] if args and 0 < len(args) else ''
    title = EverMailTitle(note_title,
                          options.notebook,
                          options.tag)
    msg = create_message_from_stdin(title)
    smtp = None
    try:
        smtp = create_smtp_client()
        client = EverMailClient(smtp)
        client.send(msg)
    finally:
        if smtp: smtp.close()


使用例
$ cat your_important_file | python ever_mail.py TITLE -nNOTEBOOK -tTAG -tTAG

工夫したところは、optparseモジュールを使ってオプションの解析を実装したところです。
暇があれば、画像の添付なども対応したいと思います。

参考
15.5. optparse — コマンドラインオプション解析器
Eメールを送信するだけでEvernoteに簡単送信!

2014年3月10日月曜日

DIに1年間はまってみて気づいたこと

2013年は、個人的にDIを推進しました。
1年間活動してみた結果、気づいたことをまとめておきます。

DIコンテナは使いませんでした。自作のファクトリクラスでインスタンス化するようにしました。理由は2つあり、1つはDIコンテナを組み込む権限がプロジェクト内で与えられていなかったから、もう1つは自分の担当範囲で使用するだけなので、シンプルな仕組みで十分だったからです。

気づいたこと
クラスを設計するときの考え方が変わりました。新しいクラスを追加するとき、「そのクラスが機能を実現するにはどんなクラスが必要か」ということを考えるようになりました。こうすることで、クラスが果たすべき責務が明確になり、クラス設計が改善されました。

反面、注意すべきことも見えてきました。「依存する側」と「依存される側」という観点でクラスをどんどん分割していきますが、やり過ぎると何もしないクラスができてしまいます。望ましい設計について、「2回測って1回で切る」とどこかで読んだことがあります。自分なりに解釈すると、1回目は機能をどんどん分割していく設計、2回目は無駄なクラスを統合していく設計とするとうまくいくような気がしました。

自作のファクトリにも問題点が見えてきました。小さいアプリケーション(2画面/5人日)だったのでファクトリクラスを1つしか用意しなかったのですが、巨大なクラスになりかけていました。ファクトリを分割し、ファクトリ同士のDIも必要になると思いました。ただ、そこまで複雑になるようであれば、DIコンテナの採用を検討するべきだと思います。

2014年3月3日月曜日

Pythonで単体テストをするには

pythonでunittestをするには」にて、
しかし、複数のテストファイルを作成した後でまとめてテストを実行する手段がないようなので、以下のようなコードを書いてみました。
という記載がありますが、Python 2.6にてまとめてテストを実行する手段が追加されましたので、勝手にフォローさせていただきます。動作確認にはPython 3.3を使用しました。

テストプログラム
テストプログラムのディレクトリ構成とプログラムは以下のとおりです。
test
├test1.py
├test2.py
└testsub
 ├__init__.py
 └testsub1.py

test/test1.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()

class TestCase2(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()

suite = unittest.TestSuite()
suite.addTest(TestCase1('test_success'))
suite.addTest(TestCase2('test_success'))


test/test2.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()



test/testsub/__init__.py
空のファイルです。testsubをモジュールとして認識させるために必要です。

test/testsub/testsub1.py
# -*- coding: utf-8 -*-

import unittest

class TestCase1(unittest.TestCase):
    def test_success(self):
        pass

    def test_fail(self):
        self.fail()



フォルダ配下のテストをすべて実行する
pythonの-mオプションのパラメータとしてunittestを渡します。詳細な出力を得るために、-vオプションも追加します。
~/py/test $ python3 -m unittest -v
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok
test_fail (test1.TestCase2) ... FAIL
test_success (test1.TestCase2) ... ok
test_fail (test2.TestCase1) ... FAIL
test_success (test2.TestCase1) ... ok
test_fail (testsub.testsub1.SubTestCase1) ... FAIL
test_success (testsub.testsub1.SubTestCase1) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test1.TestCase2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test1.py", line 17, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test2.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/test2.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (testsub.testsub1.SubTestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/cygdrive/c/home/myamamo/py/test/testsub/testsub1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 8 tests in 0.004s

FAILED (failures=4)


モジュールを指定してテストする
上のオプションに加え、モジュール名を追加するとモジュールのテストになります。
~/py/test $ python3 -m unittest -v test1
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok
test_fail (test1.TestCase2) ... FAIL
test_success (test1.TestCase2) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

======================================================================
FAIL: test_fail (test1.TestCase2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 17, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=2)


クラスを指定してテストする
さらにクラス名を追加します。
~/py/test $ python3 -m unittest -v test1.TestCase1
test_fail (test1.TestCase1) ... FAIL
test_success (test1.TestCase1) ... ok

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)


テストメソッドを指定してテストする
さらにメソッド名を追加します。
~/py/test $ python3 -m unittest -v test1.TestCase1.test_fail
test_fail (test1.TestCase1) ... FAIL

======================================================================
FAIL: test_fail (test1.TestCase1)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test1.py", line 10, in test_fail
    self.fail()
AssertionError: None

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)


テストスイートを指定してテストする
テストスイートを指定することもできます。要領はクラスやメソッドを指定する時と同様です。
~/py/test $ python3 -m unittest -v test1.suite
test_success (test1.TestCase1) ... ok
test_success (test1.TestCase2) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK



一括して実行できるだけでなく、任意のテストメソッドを選択することもできるので、テンポよく開発が進められると思いました。