Growl 通知テストランナー

[追記1 17:45]

@IanMLewis が pynotify に対応してくれてます。仕事はやい。
次は id:nullpobugSnarl に対応してくれるのを期待。

[追記2 22:15]

上の追記1で期待アゲしたように、id:nullpobugSnarl 対応してくれましたよ!

------------

BPStudy#29 にて id:t-wada さんが TDD のデモされてる際、autotest の結果を Growl 通知させてました。お、これいいなーと思ってたんですが、autotest で Growl 通知って Ruby とかじゃ普通な感じなんですかね?


Python でも autotest 的なものがないものかと思っていたところ、id:nullpobugModipyd いいよー、とオススメしてくれました。


Modipyd便利ですな - 偏った言語信者の垂れ流し


ならばと思い、Growl 通知する簡単なテストランナーを作ってみましたよ。既に誰かが作ってるんでしょうけど。


https://ae35@bitbucket.org/ae35/growltestrunner/

growlnotify のインストール

こいつを使えばコマンドライン上から Growl 通知させることができます。Growl の .dmg に入ってますので、インストールしちゃって下さい。


以下のように打って、Growl 通知されればOK

  $ growlnotify -n "Growl Text Test Runner" -m test

Growl 設定

一回でも *growlnotiry -n "Growl Text Test Runner"* を実行していれば、「システム環境設定」->「Growl」->「アプリケーション」一覧に "Growl Text Test Runner" というアプリが登録されていると思います。



お好みの表示スタイルを選びます。自分は Music Video にしてます。



表示スタイルの優先度「緊急」を赤、「重要でない」を緑なんかにしてやるといいかもしれません。あとの設定はお好みで。


超簡単な説明

unittest.TextTestRunner を継承して、テスト結果を growlnotify に投げてるだけです。アイコンを作るほうが時間がかかってるんですが、誰かカッコいいアイコン作ってくれませんか…


growltestrunner パッケージを Python パスが通ってる場所に置いて下さい。

使い方

TextTestRunner の代わりに使ってみたり

  #!/usr/bin/env python
  # -*- coding:utf-8 -*-
  import unittest

  from growltestrunner import GrowlTestRunner

  class DemoTest(unittest.TestCase):
      def test_true(self):
          self.assert_(True)

      def test_false(self):
          self.assert_(False)

  if __name__ == '__main__':
      suite = unittest.TestLoader().loadTestsFromTestCase(DemoTest)
      GrowlTestRunner().run(suite)


Modipyd で使ってみたり

  $ pyautotest -r growltestrunner.GrowlTestRunner


テストが実行されるたびに Growl 通知されます。


Django 開発初心者に送る 10 の Tips (超訳?)

実践的な DJango テクニック集として、凄くいい記事だったので、勝手に超訳してみました。

http://zeroandone.posterous.com/top-10-tips-to-a-new-django-developer

1. import にプロジェクト名を書かないこと

例えば "project3" というプロジェクトに "xyz" アプリケーションがある場合、次のようにはしないこと。

  from project3.xyz.models import Author


これではプロジェクトとアプリケーションの結びつきが強すぎて、以下の弊害がおこる。

  1. アプリケーションの再利用がしづらい
  2. 将来プロジェクト名を変えたくなっても変更が難しい


なので、このようにしよう。

  from xyz.models import Author

python パス上にある django プロジェクトならば、このように書ける。

2. MEDIA_ROOT と TEMPLATE_DIRS をハードコードしないこと

settings.py において、MEDIA_ROOT と TEMPLATE_DIRS を次のように書いてはいけない。

   TEMPLATE_DIRS = ( "/home/html/project/templates",)
   MEDIA_ROOT = "/home/html/project/appmedia/"


これではプロジェクトを別サーバに移動したり、別OSに変更する際に書き換えなければならなくなる。


これらの問題は次のテクニックを使うことで、簡単に避けることができる。

  SITE_ROOT = os.path.realpath(os.path.dirname(__file__))
  MEDIA_ROOT = os.path.join(SITE_ROOT, 'appmedia')
  TEMPLATE_DIRS = ( os.path.join(SITE_ROOT, 'templates'),)


自分はこのテクニックを Rob Hudson から学んだ。以下のリンクからより詳細な情報を得ることができる。

http://rob.cogit8.org/blog/2009/May/05/django-and-relativity-updated/

3. テンプレートに静的ファイルをハードコードしないこと

静的ファイル(JavaScript ファイルや css ファイル、画像など)のリンクを次のように書いてはいけない。

(MEDIA_URL を“/appmedia/”とする)

  <link rel="stylesheet" type="text/css" href="/appmedia/wysiwyg/jHtmlArea.css" />
  <script type="text/javascript" src="/appmedia/wysiwyg/jHtmlArea.js"></script>
  <script type="text/javascript" src="/appmedia/scripts/editor.js"></script>
  <script type="text/javascript" src="/appmedia/scripts/comments.js"></script>
  <script type="text/javascript" src="/appmedia/scripts/voting.js"></script>


上の書き方では、静的コンテンツを Amazon S3 を使って別サーバ(http://cdn.xyz.com としようか)に配置したい、なんていう場合に、全テンプレートの "/appmedia/" を http://cdn.xyz.com 書き換えなければいけなくなる。

うわぁ、超メンドくさい。


これはシンプルなテクニック、つまり "/appmedia/" のようなハードコーディングではなく、コンテクスト変数 {{MEDIA_URL}} を使うようにすることで、避けることができる

  <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}wysiwyg/jHtmlArea.css" />
  <script type="text/javascript" src="{{ MEDIA_URL }}wysiwyg/jHtmlArea.js"></script>
  <script type="text/javascript" src="{{ MEDIA_URL }}scripts/editor.js"></script>
  
  <script type="text/javascript" src="{{ MEDIA_URL }}scripts/comments.js"></script>
  <script type="text/javascript" src="{{ MEDIA_URL }}scripts/voting.js"></script>


これで MEDIA_URL を "/appmedia/" から "http://cdn.xyz.com/" に変更すれば、全テンプレートに自動で反映される。いちいち全部のテンプレートを変更するなんていう超メンドくさいことはしなくていい。

どうやればコンテクスト変数を使えるの?

とっても簡単。RequestConext(request) を render_to_response に追加することで、便利なコンテクスト変数(ユーザーや取得ページなど)を、テンプレートに渡すことができる。

  return render_to_response("my_app/my_template.html", {'some_var': 'foo'},
  context_instance=RequestContext(request))

4. コアビジネスロジックを views に書かないこと!

コアビジネスロジックを views に書く(例えば銀行 X に総額 P を追加して、銀行 Y から P を引く、なんていうロジック)のはよろしくない。

なんで?
  1. コードをユニットテストできない
  2. コードを再利用できない
じゃあどこに書けばいい?

models かヘルパー関数に書くほうがいい。

基本的なロジックはどうする?

(モデルの)オブジェクトを検索や取得してきたり、リストを渡したり…なんていう、本当に基本的なロジックならば views に書いてもいいと思う。

5. 本番サーバに適用するとき、DEBUG=False (settings.py) に変更するのは面倒

ローカル環境を公開環境に移すとき、DEBUG を False にするのをよく忘れてしまう(DEBUG = False にすると利点が多い)し、退屈な作業だ。


これは以下のように、ホスト名をチェックして DEBUG 変数を設定するようにすればいい。

  import socket
  
  if socket.gethostname() == 'productionserver.com':
    DEBUG = False
  else:
    DEBUG = True

このテクニックを最初に指摘したのは Jokull だ。


より詳細な情報は:

http://nicksergeant.com/blog/django/automatically-setting-debug-your-django-app-based-server-hostname


別のテクニックとして、ローカル環境では、(DEBUG=Trueに設定した)もうひとつの settings ファイルを使う方法もある。

  #Filename settings_debuy.py
  #It contains all variables from settings and it overrides the DEBUG variable to True
  #we use settings_debug.py to run the server locally python manage.py runserver settings=settings_debug.py
  
  from settings import *
  DEBUG = True
  
  #you can also add other things which helps you to do the debugging easily
  #like Debug toolbar etc...

6. サードパーティ製のアプリケーションに依存するテンプレートタグは一度だけ load する

サードパーティ製アプリケーションのテンプレートタグを使うには、次のようにテンプレート内で load する必要がある。

  {% load template_tags %}


サードパーティ製のテンプレートタグを利用するテンプレート全てに同様の記述をする必要があるが、これは DRY の原理に反する。これを改善しよう。

  from django import template
  template.add_to_builtins('project.app.templatetags.custom_tag_module')

このコードを、起動時に読み込まれるファイル(settings.py や urls.py、各アプリの models.py など)のどれかに書いておくだけ。


上記コードが起動時にテンプレートタグを読み込んでくれるので、 {% load template_tags %} を使わなくても、好きなテンプレートでそのテンプレートタグを使うことができる。

7. Urls.py

project/urls.py に全 URL を書かない

例えば

  urlpatterns = patterns('',
  url(r'^askalumini/question/$','.....registerInstitution',name='iregister'),
  url(r'^askalumin/answer/$','someview.....',name='newmemberurl'),
  url(r'^institution/member/$','someview.....',name="dashboardurl"),
  url(r'^institution/faculty/$','editInstitute',name="editinstituteurl"),
  url(r'^memeber/editprofile/$','editProfile',name="editprofileurl"),
  url(r'^member/changepassword/$','changePassword',name="changepasswordurl"),
  url(r'^member/forgotpassword/$','forgotPassword',name="forgotpasswordurl"),
  url(r'^member/changepicture/$','changePicture',name="changepictureurl"),
  url(r'^member/logout/$','memeberlogout',name="logouturl"), ,
  )


代わりに次のように、複数アプリケーションに分離する(プロジェクトの /urls.py に記述する)。
これによって、アプリケーションを変更作業なしに、異なるプロジェクトで再利用することができる。

  urlpatterns = patterns('',
  # Example:
  (r'^$', include('institution.urls')),
  (r'^institution/', include('institution.urls')),
  (r'^askalumini/', include('askalumini.urls')),
  (r'^member/', include('member.urls')),
  )


それから urls.py を各アプリケーションに作成し、対応する URL を追加する(以下は askalumini アプリを例として挙げている)。

  urlpatterns = patterns('askalumini.views',
  url(r'^$','askHome',name='askaluminiurl'),
  url(r'^questions/(?P<questionno>\d+)/$','displayQuestion',name='askquestiondisplay'),
  url(r'^askquestions/$','askQuestion',name='askquestionurl'),
  url(r'^postcomment/$','postComment',name="askquestioncomment")
URL の定義には url 関数を使用する

URL を定義するのに、URL に名前を付けられる url 関数を使用しているのに気付いたと思う。上で定義した全 URLには、最後に URL 名を指定している(name="....." のように)。この名前によって、views や templates、そして models において、URL のハードコーディングをしないで済むような、効果的な指定ができるようになる。


各アプリにおいて URL 名のユニークさを保つために、URL の命名は "" のような命名規約に沿うようにしている(上のコード参照)。

URL をハードコードしない

なぜって?


urls.py の URL 構造を変更する場合、URL をハードコーディングした全ての箇所を書き換える必要が出てくるからだ。(超メンドくさい…)

views.py では

以下のように URL をハードコードする代わりに、

  HttpResponseRedirect("/askalumini/questions/54")


URL 名を用いて reverse ルックアップ関数を使用して URL を生成する。

  from django.core.urlresolvers import reverse
  HttpResponseRedirect(reverse('askquestiondisplay',kwargs={'questionno':q.id}))
models.py では

絶対 URL を生成するために、models.py では reverse ルックアップ関数に加え、models.permalink decorator を使用できる。

  @models.permalink
  def get_absolute_url(self):
    return ('profileurl2',(),{'userid': self.user.id})


このデコレータでも、URL を生成するために URL 名を使用する。

templates.py では

いろいろと不便なハードコーディグをする代わりに、 url タグ を使うことで、URL 名を用いて URL を生成することができる。

  {% url askquestiondisplay 345 %}
  <a href="{% url askquestiondisplay 345 %}"> Ask Question </a>

8. デバッグ

a. django-debug-toolbar を使うことで、以下のような情報を取得できる
  1. どれくらいの数の SQL 文が実行されたか?トータルタイムは?
  2. テンプレート名、ロギング、クッキー/セッション情報、などなど


debug-toolbar のより詳細な情報は以下のリンク先で確認できる。

http://github.com/robhudson/django-debug-toolbar/tree/master

b. Werkzeug デバッガを使えば、エラー画面で Python シェルを開くことができ、迅速なデバッグ作業に役立つ。

詳細な情報は以下のリンクから。

http://blog.dpeepul.com/2009/07/14/python-shell-right-on-the-django-error-page/

c. デバッグ作業に pdb を使う。

詳細はこちら。

http://ericholscher.com/blog/2008/aug/31/using-pdb-python-debugger-django-debugging-series-/

9. 可能なら利用できるように Pinax について知っておく

Django を使う最大の利点のひとつとして、アプリケーションの再利用性がある。

Django は他のフレームワークのような一枚岩な設計を志向せずに、再利用性を促進させている。これは、他のフレームワークではなく Django を選ぶ際の重要な理由のひとつであり、Pinax はそのような設計方針に対する成果のひとつだ。


Pinax って何?


近年では、世界のどのウェブサイトでも登録、OpenIDのサポート、グループ、ユーザープロフィールなどといった機能が要求される。ほとんどのサイトで、これらの機能のロジックを実装しなければならない。独立した再利用可能な機能を提供するプラットフォーム(django のプロジェクト)があり、またその上で開発してもらえれば、これらの機能は、様々なサイト間で再利用することができる。そのようなプラットフォームは、迅速なウェブサイト構築を可能にし、開発者は、彼らのサイト独自の部分の開発に注力することができる。


Pinax はそんな再利用可能なアプリケーションを集めた(Django の)プラットフォームだ。Pinax は次のような再利用可能な機能やアプリケーションを提供する。

  1. openid サポート
  2. (vCard, Google, Yahoo などからの)連絡先のインポート
  3. 通知フレームワークなどなど… より詳細な情報はこちら http://pinaxproject.com/


Pinax は James Tauber による強力なアイデアだ。可能なら是非使ってみてほしい。迅速なウェブアプリケーションの構築の助けになるはずだ。


より詳細な情報はこちら

http://uswaretech.com/blog/2009/03/create-a-new-social-networking-site-in-few-hours-using-pinax-platform-django/

10. 知っておくべきサードパーティ製アプリケーション

使うべきサードパーティ製アプリケーションがいくつかある。

1. マイグレーション

スキーマやデータのマイグレーションはどうしてる?


syncdb するよね。


最初はモデルの設計がデータベースに反映される。


でも何時間、何日か経ったら…


モデルにフィールドを追加したり、削除したり、設計を変更しなければならなくなる。
変更するよね。ここで、どうやってこの変更をデータベースに反映させるかが重大な問題になる。
取れる方法としては、

  • もう一度 syncdb する(メンドくさい)
  • 手動で alter 文を DB に発行する(メンドくさい)


このようにデータベースをある状態から別の状態に変更することをマイグレーションと呼ぶ。このマイグレーションを楽にしてくれる、いくつかのサードパーティアプリケーションがある。

  • django-evolutions (簡単に使えて、自動で全部やってくれるけど、頑丈じゃない)
  • South (より強固だけど、ちょっと使い方を覚える必要がある)
2. テンプレート

Django のテンプレートは、制限がキツすぎると思ったら、次のようなものもある。

  1. template-utils (テンプレートタグやユーティリティよりも、テンプレートの機能を向上)
  2. Jinja (Django テンプレートと同じシンタックスを採用し、プロジェクトのプラグインとして使える、そしてより柔軟なロジック記述が特徴の、サードパーティのテンプレートシステム)
3. その他

1. django command extensions 次のようなお役立ちコマンドラインを提供してくれる

    1. 全モデルをロードしてくれる shell_plus
    2. Werkzeug デバッガを組み込んだ runserver_plus
    3. 上司に見せるためのモデル図生成

などなど。


より詳しい情報はこちら

http://ericholscher.com/blog/2008/sep/12/screencast-django-command-extensions/


2. Sorl はサムネイルを生成してくれる

など


他のリソースはこちら

  1. http://stackoverflow.com/questions/550632/favorite-django-tips-features

reST モード覚え書き

Sphinx のおかげで reST がまた見直されてるぞ!

ということで Emacs 用 reStructuredText モードのコマンドキーマップ

セクションデコレーション

セクションのタイトル装飾
C-c C-a
rst-adjust
C-c C-=
rst-adjust
C-=
st-adjust (Mac OSX じゃ動かないよ)
ドキュメント内のセクション階層を表示
C-c C-h
rst-display-decorations-hierarchy
ドキュメント内の装飾を均一化
C-c C-s
rst-straighten-decorations

セクションの移動と選択

カーソルのあるサブセクションを選択
C-c C-m
rst-mark-section
セクション間を進む/戻る
C-c C-n
rst-forward-section
C-c C-p
rst-backward-section

テキストブロックの操作

範囲内のパラグラフをリスト化
C-c C-b
rst-bullet-list-region
範囲内のパラグラフを番号付リスト化
C-c C-e
rst-enumerate-region
リストを番号リストに変換
C-c C-v
rst-convert-bullets-to-enumeration
範囲内をラインブロック化
C-c C-d
rst-line-block-region
範囲内の全リストが一貫しているか確認(?)
C-c C-w
rst-straighten-bullets-region
範囲を左右にシフト(リストを考慮して)
C-c C-l
rst-shift-region-left
C-c C-r
rst-shift-region-right
コメント/アンコメント
C-c C-c
comment-region

目次関連

目次バッファ表示
C-c C-t
rst-toc
目次挿入
C-c C-i
rst-toc-insert
ドキュメント内の目次更新(カーソルの場所はそのまま)
C-c C-u
rst-toc-update
目次上のカーソルのセクションに移動
C-c C-f
rst-goto-section

Emacs からのドキュメント変換

ドキュメントにプレコンフィギュアドなツールセットコマンドを実行する
C-c 1
rst-compile
C-c 2
rst-compile-alt-toolset
選択範囲を pseudo-xml に変換
C-c 3
rst-compile-pseudo-region
ドキュメントを PDF に変換してビューアを起動
C-c 4
rst-compile-pdf-preview
ドキュメントを S5 スライドに変換してビューアを起動
C-c 5
rst-compile-slides-preview

GAE/Python で Twitter bot を作る(後編)

準備編 の続きは明日書くとか言っときながら、間が空いてしまいましたが後編を。

残るは肝心のハンドラ部分を実装していきます。

views.py の作成

main.py で指定したように、

  • /:TOP画面。ハンドラは MainHandler
  • /ssbbot/mumble/update/:cron がここを叩いて更新処理を走らせる。ハンドラは BotHandler

という感じで views.py を実装して行きましょう。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import logging

from google.appengine.ext import webapp

import twitter
import ssb
import bitly

from models import MumbleModel

def my_twitter_api_init(self,
                        username=None,
                        password=None,
                        input_encoding=None,
                        request_headers=None):
    """Monkey patche for twitter.Api.__init__ method.
    Just change _cache do not use FS cache.
    """
    import urllib2
    from twitter import Api
    self._cache = None
    
    self._urllib = urllib2
    self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
    self._InitializeRequestHeaders(request_headers)
    self._InitializeUserAgent()
    self._InitializeDefaultParameters()
    self._input_encoding = input_encoding
    self.SetCredentials(username, password)
twitter.Api.__init__ = my_twitter_api_init


class MainHandler(webapp.RequestHandler):
    def get(self):
        """
        """
        self.response.out.write("Stack Stock Books つぶやき Bot")

class BotHandler(webapp.RequestHandler):
    """Bot 更新用ハンドラ"""
    def get(self):
        """
        """
        # 最新の SSB つぶやき取得
        ssbapi = ssb.Api()
        mumble = ssbapi.get_mumbles(include_books=False)[0]
        logging.debug("mumble: %s", mumble)

        key_name = MumbleModel.key_format % dict(mumble_id=mumble.mumble_id)
        mm = MumbleModel.get_by_key_name(key_name)

        if mm is not None and mm.time >= mumble.time:
            # 更新情報なし
            logging.info("No update: mumble_id: %s", mumble.mumble_id)
            self.response.out.write("No update")
            return

        # 書籍情報取得
        book = ssbapi.get_book(mumble.book_id)
        mumble.book = book
        
        # 投稿文言生成
        twitterapi = twitter.Api(username="TWITTER_USER", password="TWITTER_PWD")
        user = ssbapi.get_user(mumble.user_id)
        status = self._mumble_string(user, mumble)
        if len(status) > 140:
            status = status[:139] + u'\u2026'

        # Twitter 更新
        result = twitterapi.PostUpdate(status)
        logging.info("status: %s\nresult: %s", status, result)

        # 更新情報を登録
        mm = MumbleModel(key_name=key_name,
                         mumble_id=mumble.mumble_id,
                         time=mumble.time)
        mm.put()
        self.response.out.write("OK")

    def _mumble_string(self, user, mumble):
        url = mumble.uri
        api = bitly.Api()
        api.set_account("BITLY_LOGIN", "BITLY_APIKEY")
        try:
            url = api.shorten(url)
        except bitly.BitlyResponseError:
            pass
        
        return u"%(url)s %(nick)s『%(title)s』%(body)s" \
               % dict(nick=user.nick,
                      title=mumble.book.title,
                      body=mumble.body,
                      url=url)

BotHandler では、

  1. Stack Stock Books から最新のつぶやき取得
  2. Datastore に保存した前回のつぶやき情報と比較。更新が無ければ終了。
  3. 新しい情報ならば、Twitter への投稿文言生成。URL は bit.ly API を利用して短縮。
  4. 文言が140文字を越えていたら、139文字+'…' として切り詰め処理。
  5. Twitter へ投稿
  6. Datastore に最新つぶやき情報を登録

という処理をしています。

完成!

さて、これで簡単な Twitter bot ができました。ファイル構成は以下のようになっていると思います。

./ssbbot/
|-- app.yaml
|-- bitly.py
|-- cron.yaml
|-- main.py
|-- models.py
|-- simplejson
|-- ssb.py
|-- twitter.py
`-- views.py


あとはこれを GAE にデプロイすれば完成です。

GAE/Python で Twitter bot を作る(準備編)

先のエントリで bit.ly API モジュールやら Stack Stock Books API モジュールを作ったので、Google App Engine 上で動作する Twitter bot を作ってみます。


先のエントリとやらはこの辺です。

どんな処理にしましょうか?

ざっくりとこんな感じで。

  1. Stack Stock Books から最新のつぶやき取得
  2. Datastore に保存した前回のつぶやき情報と比較
  3. 新しい情報ならば、Twitter への投稿文言生成
  4. Twitter へ投稿
  5. Datastore に最新つぶやき情報を登録
  6. これらの処理を一定間隔で回す


ということで、URL 的にはこんな感じに。

  • /:紹介文でも表示しましょう
  • /ssbbot/mumble/update/:cron がここを叩いて上記処理を走らせる

必要なモジュールのソースをダウンロード

以下のモジュールのソースを取得します。

そして、先のエントリで自作したモジュール。


これらのモジュールのソースコードを ssbbot ディレクトリ以下に配置します。

./ssbbot/
|-- bitly.py
|-- simplejson
|-- ssb.py
`-- twitter.py


ちなみにキモの python-twitter は 0.6 を使用していますが、Api クラスにおいてファイルキャッシュを使っているために、素のままでは GAE で使えません。今回は以下のような Monkey patch を twitter.Api を利用する前に書いて、その場しのぎを…

def my_twitter_api_init(self,
                        username=None,
                        password=None,
                        input_encoding=None,
                        request_headers=None):
    """Monkey patche for twitter.Api.__init__ method.
    Just change _cache do not use FS cache.
    """
    import urllib2
    from twitter import Api
    self._cache = None
    
    self._urllib = urllib2
    self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
    self._InitializeRequestHeaders(request_headers)
    self._InitializeUserAgent()
    self._InitializeDefaultParameters()
    self._input_encoding = input_encoding
    self.SetCredentials(username, password)
twitter.Api.__init__ = my_twitter_api_init

アプリケーション設定ファイルの作成

ssbbot ディレクトリに、アプリケーション設定ファイル app.yaml を以下のような内容で追加します。

application: YOUR_APP_ID
version: 1
runtime: python
api_version: 1

handlers:
- url: /ssbbot/.*
  script: main.py
  login: admin

- url: /.*
  script: main.py

今回は /ssbbot/以下の URL は cron (と管理者)だけが叩けるようにしたいので、login: admin で、管理者権限を持つユーザーのみがアクセスできるようにしています。

スケジュールタスク設定ファイルの作成

更新処理を cron で処理を走らせたいので、ssbbot ディレクトリにスケジュールタスク設定ファイル cron.yaml を以下のように追加します。

cron:
- description: update mumble job
  url: /ssbbot/mumble/update/
  schedule: every 30 mins

ここでの間隔は30分にしていますが、あまり間隔が空いてしまうと更新データの取りこぼしが出てしまいますし(今回のアプリでは最新の一件しか投稿しませんw)、かと言って短か過ぎると、利用 API や GAE の制限に引っかかったり迷惑をかけてしまうので程々にします。

models.py の作成

Twitter に投稿されたつぶやき情報を保持するため、MumbleModel という Datastore モデルを作成します。


ssb モジュールの Mumble クラスは多くのプロパティを持ちますが、今回は既に投稿済みかどうかを比較するためだけに使用するので、保持するプロパティは mumble_id と time、そしてデータストアに登録した日時だけにしています。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from google.appengine.ext import db

class MumbleModel(db.Model):
    """
    >>> mumble = Mumble()
    >>> mumble.mumble_id = 123
    >>> mumble.time = datetime(2009, 8, 3, 22, 34, 36)
    >>> mumble.put()
    >>> db.put(mumble)
    """
    key_format = "mumblemodel/%(mumble_id)s"
    
    mumble_id = db.IntegerProperty(required=True)
    time = db.DateTimeProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)

main.py の作成

ハンドラに行く前に、メイン処理を書いちゃいます。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

from views import *

def main():
    app = webapp.WSGIApplication([
        (r'/', MainHandler),
        (r'/ssbbot/mumble/update/', BotHandler),
        ], debug=True)
    run_wsgi_app(app)

if __name__ == '__main__':
    main()


よし、これで残りは views.py に肝心のハンドラを書くだけです。


明日あたりに続くっ!

GAE ログ画面の日時を JST にする Greasemonkey スクリプト

タイトル通りですが、初めての Greasemonkey ユーザースクリプト作ってみました。


最近の Web 開発だと、ASP.NET で楽してたり携帯 Web 開発ばっかりだったもので、JavaScript なんて遥か遠い記憶しかありません。フォーマット出力どうやるの?とか parseInt("08") でなんで 0 が返ってくるの?なんていうダサい四苦八苦をしながら書いたので、お洒落な JavaScripter になれるように是非とも突っ込み下さいませ。


Google App Engine のログ画面(Logs)の日時表示が UTC-7 で(これってどこかで設定とか変更とかできたりするもんなんでしょうか?)、すげー不便だなーと思ってたので、JST に表示変換するだけのものです。本当は他の管理画面(Admin Logs とか Cron Jobs とか)でも変換できるようにしたかったんですが、とりあえず。

// ==UserScript==
// @name           GAE Logs Datetime JST
// @namespace      http://tiwtter.com/ae35
// @description    GoogleAppEngine の Logs を UTC-7 から JST に変換
// @include        https://appengine.google.com/logs?*
// @version        0.1.1
// ==/UserScript==
(function(){

    function main(){
        var allLogs = document.evaluate(
        	'//li[@class="ae-log"]//h5/span',
        	document,
        	null,
        	XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
        	null);
        for (var i = 0; i < allLogs.snapshotLength; i++) {conv(allLogs.snapshotItem(i));}
    }

    function conv(o){
        // 08-05 10:37PM 38.425
        var m = o.innerHTML.match(/(\d{2})-(\d{2}) (\d{2}):(\d{2})(\w{2}) (\d{2})\.(\d+)/);
        if (m == null) return;

        var _month, _day, _hour, _minute, _ampm, _sec, _msec;
        _month 	= RegExp.$1;
        _day 	= RegExp.$2;
        _hour 	= RegExp.$3;
        _minute = RegExp.$4;
        _ampm 	= RegExp.$5;
        _sec 	= RegExp.$6;
        _msec 	= RegExp.$7;
        if (_ampm == 'PM') _hour = parseInt(_hour, 10) + 12;

		var s = "";
		s += new Date().getFullYear();
		s += "/";
		s += _month;
		s += "/";
		s += _day;
		s += " ";
		s += _hour;
		s += ":";
		s += _minute;
		s += ":";
		s += _sec;
		var d = new Date(s);
		// UTC-7 -> UTC+9
		d.setTime(d.getTime() + 16 * 60 * 60 * 1000);

        o.innerHTML = fmtdate(
			d.getFullYear(), d.getMonth(), d.getDate(),
			d.getHours(), d.getMinutes(), d.getSeconds(),
			_msec);
    }

    function fmtdate(year, month, day, hour, minute, sec, msec){
        var s = "";
        s += zeropadding(month + 1);
        s += "-";
        s += zeropadding(day);
        s += " ";
        s += zeropadding(from24to12(hour));
        s += ":";
        s += zeropadding(minute);
        s += ampm(hour);
        s += " ";
        s += zeropadding(sec);
        s += ".";
        s += msec;
        return s;
    }

    function zeropadding(val){
        return (parseInt(val, 10) < 10) ? "0" + val: val;

    }
    function from24to12(hour){
        return (hour > 12) ? (hour - 12) : hour;
    }

    function ampm(hour){
        return (hour < 12) ? "AM": "PM";
    }

    document.body.addEventListener("DOMNodeInserted",
    function(event) {
        var elems = event.target.getElementsByTagName('span');
        for (var i = 0; i < elems.length; i++) {conv(elems[i]);}
    }, false);

    main();

})();


ダウンロードはこちら
http://bitbucket.org/ae35/gaelogsdatetimejst/downloads/GAELogsDatetimeJST.user.js

Python で Stack Stock Books API モジュール的なもの

読書管理に Stack Stock Books を利用しています。まあ読書管理してても、積まれてる本の数は全然減らないんですが…


で、なんか面白そうな本ないかなーって時に、ここのつぶやき(参加ユーザーの読書メモ的なもの)をザザーっと眺めたりするのが好きなんです。このつぶやき一覧を Stack Stock Books の API で取得できるので、bit.ly の機能限定 API モジュール に続いて、Stack Stock Books APIPython モジュールを書き散らかしてみました。


今のところの機能としては

  • つぶやき一覧取得(include_authors は未実装)
  • 利用者情報

だけ。


例によって simplejson を使ってます。


ssb.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import urllib
import urllib2
from datetime import datetime
import logging
import simplejson as json

#logging.basicConfig(level=logging.DEBUG)

class SSBError(Exception):
    """Base class for SSB errors
    """
    pass

class SSBRequestError(SSBError):
    """SSB API request error class
    """
    def __init__(self, code, msg):
        self.code = code
        self.msg = msg
    def __str__(self):
        return "SSB API request error: %s (code: %s)" % (self.msg, self.code)

class SSBResponseError(SSBError):
    """SSB API response error class
    """
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return "SSB API response error: %s" % self.msg

class Mumble(object):
    """つぶやきクラス
    """
    def __init__(self,
                 mumble_id=None,
                 body=None,
                 time=None,
                 user_id=None,
                 book_id=None,
                 book=None):
        self.mumble_id = mumble_id
        self.body = body
        self.time = time
        self.user_id = user_id
        self.book_id = book_id
        self.book = book

    @property
    def uri(self):
        return "http://stack.nayutaya.jp/book/%(isbn10)s/mumble/%(mumble_id)s" \
               % dict(isbn10=self.book.isbn10, mumble_id=self.mumble_id)

    def get_mumble_id(self):
        return self._mumble_id
    def set_mumble_id(self, val):
        self._mumble_id = val
    mumble_id = property(get_mumble_id, set_mumble_id,
                         doc="The unique id of this mumble.")

    def get_body(self):
        return self._body
    def set_body(self, val):
        self._body = val
    body = property(get_body, set_body,
                    doc="The text of this mumble.")

    def get_time(self):
        return self._time
    def set_time(self, val):
        try:
            sdate, stime = val.split()
            args = [int(arg) for arg in sdate.split("-") + stime.split(":")]
            self._time = datetime(*args)
        except:
            self._time = None
    time = property(get_time, set_time,
                    doc="The time this mumble was posted.")

    def get_user_id(self):
        return self._user_id
    def set_user_id(self, val):
        self._user_id = val
    user_id = property(get_user_id, set_user_id,
                       doc="The unique id of the user who posted this mumble.")

    def get_book_id(self):
        return self._book_id
    def set_book_id(self, val):
        self._book_id = val
    book_id = property(get_book_id, set_book_id,
                       doc="The unique id of the book mentioned in this mumble.")

    def get_book(self):
        return self._book
    def set_book(self, val):
        self._book = val
    book = property(get_book, set_book,
                    doc="The book object which mentioned in this mumble.")

    def as_dict(self):
        """Mumble インスタンスを辞書として返す
        """
        data = {}
        if self.mumble_id:
            data['mumble_id'] = self.mumble_id
        if self.body:
            data['body'] = self.body
        if self.time:
            data['time'] = self.time.strftime('%Y-%m-%d %H:%M:%S')
        if self.user_id:
            data['user_id'] = self.user_id
        if self.book_id:
            data['book_id'] = self.book_id
        if self.book:
            data['book'] = self.book.as_dict()
        return data

    def as_json_string(self):
        """Mumble インスタンスを JSON 文字列として返す
        """
        return json.dumps(self.as_dict(), sort_keys=True)

    def __str__(self):
        return self.as_json_string()

    @staticmethod
    def new_from_json_dict(data):
        if 'book' in data:
            book = Book.new_from_json_dict(data['book'])
        else:
            book = None
        return Mumble(mumble_id=data.get('mumble_id'),
                      body=data.get('body'),
                      time=data.get('time'),
                      user_id=data.get('user_id'),
                      book_id=data.get('book_id'),
                      book=book)

class Book(object):
    """書籍蔵書クラス"""
    def __init__(self,
                 book_id=None,
                 isbn10=None,
                 isbn13=None,
                 title=None,
                 release_date=None,
                 binding=None,
                 publisher=None,
                 uri=None,
                 image_uri=None):
        self.book_id = book_id
        self.isbn10 = isbn10
        self.isbn13 = isbn13
        self.title = title
        self.release_date = release_date
        self.binding = binding
        self.publisher = publisher
        self.uri = uri
        self.image_uri = image_uri

    def get_book_id(self):
        return self._book_id
    def set_book_id(self, val):
        self._book_id = val
    book_id = property(get_book_id, set_book_id,
                       doc="The unique id of this book.")

    def get_isbn10(self):
        return self._isbn10
    def set_isbn10(self, val):
        self._isbn10 = val
    isbn10 = property(get_isbn10, set_isbn10,
                      doc="The isbn 10 of this book.")
    
    def get_isbn13(self):
        return self._isbn13
    def set_isbn13(self, val):
        self._isbn13 = val
    isbn13 = property(get_isbn13, set_isbn13,
                      doc="The isbn 13 of this book.")
    
    def get_title(self):
        return self._title
    def set_title(self, val):
        self._title = val
    title = property(get_title, set_title,
                     doc="The title of this book.")

    def get_release_date(self):
        return self._release_date
    def set_release_date(self, val):
        """
        try:
            sdate, stime = val.split()
            args = [int(arg) for arg in sdate.split("-") + stime.split(":")]
            self._release_date = datetime(*args)
        except:
            self._release_date = None
        """
        self._release_date = val
    release_date = property(get_release_date, set_release_date,
                            doc="Date when this book was released.")

    def get_binding(self):
        return self._binding
    def set_binding(self, val):
        self._binding = val
    binding = property(get_binding, set_binding,
                       doc="The binding of this book.")

    def get_publisher(self):
        return self._publisher
    def set_publisher(self, val):
        self._publisher = val
    publisher = property(get_publisher, set_publisher,
                         doc="The publisher of this book.")

    def get_uri(self):
        return self._uri
    def set_uri(self, val):
        self._uri = val
    uri = property(get_uri, set_uri,
                   doc="The uri of this book.")

    def get_image_uri(self):
        return self._image_uri
    def set_image_uri(self, val):
        self._image_uri = val
    image_uri = property(get_image_uri, set_image_uri,
                         doc="The image uri of this book.")

    def as_dict(self):
        """Book インスタンスを辞書として返す
        """
        data = {}
        if self.book_id:
            data['book_id'] = self.book_id
        if self.isbn10:
            data['isbn10'] = self.isbn10
        if self.isbn13:
            data['isbn13'] = self.isbn13
        if self.title:
            data['title'] = self.title
        if self.release_date:
            data['release_date'] = self.release_date
        if self.binding:
            data['binding'] = self.binding
        if self.publisher:
            data['publisher'] = self.publisher
        if self.uri:
            data['uri'] = self.uri
        if self.image_uri:
            data['image_uri'] = self.image_uri
        return data

    def as_json_string(self):
        """Book インスタンスを JSON 文字列として返す
        """
        return json.dumps(self.as_dict(), sort_keys=True)

    def __str__(self):
        return self.as_json_string()

    def __eq__(self, other):
        return str(self) == str(other)

    def __ne__(self, other):
        return not self.__eq__(other)

    @staticmethod
    def new_from_json_dict(data):
        """
        """
        return Book(book_id=data.get('book_id'),
                    isbn10=data.get('isbn10'),
                    isbn13=data.get('isbn13'),
                    title=data.get('title'),
                    release_date=data.get('release_date'),
                    binding=data.get('binding'),
                    publisher=data.get('publisher'),
                    uri=data.get('uri'),
                    image_uri=data.get('image_uri'))

class User(object):
    """ユーザークラス"""
    def __init__(self,
                 user_id=None,
                 name=None,
                 nick=None,
                 uri=None,
                 icon_uri=None):
        self.user_id = user_id
        self.name = name
        self.nick = nick
        self.uri = uri
        self.icon_uri = icon_uri

    def get_user_id(self):
        return self._user_id
    def set_user_id(self, val):
        self._user_id = val
    user_id = property(get_user_id, set_user_id,
                       doc="The unique id of this user.")

    def get_name(self):
        return self._name
    def set_name(self, val):
        self._name = val
    name = property(get_name, set_name,
                       doc="The name of this user.")

    def get_nick(self):
        return self._nick
    def set_nick(self, val):
        self._nick = val
    nick = property(get_nick, set_nick,
                       doc="The nickname of this user.")

    def get_uri(self):
        return self._uri
    def set_uri(self, val):
        self._uri = val
    uri = property(get_uri, set_uri,
                       doc="The uri of this user.")

    def get_icon_uri(self):
        return self._icon_uri
    def set_icon_uri(self, val):
        self._icon_uri = val
    icon_uri = property(get_icon_uri, set_icon_uri,
                       doc="The icon uri of this user.")
    
    def as_dict(self):
        """User インスタンスを辞書として返す
        """
        data = {}
        if self.user_id:
            data['user_id'] = self.user_id
        if self.name:
            data['name'] = self.name
        if self.nick:
            data['nick'] = self.nick
        if self.uri:
            data['uri'] = self.uri
        if self.icon_uri:
            data['icon_uri'] = self.icon_uri
        return data

    def as_json_string(self):
        """User インスタンスを JSON 文字列として返す
        """
        return json.dumps(self.as_dict(), sort_keys=True)

    def __str__(self):
        return self.as_json_string()

    def __eq__(self, other):
        return str(self) == str(other)

    def __nq__(self, other):
        return not self.__eq__(other)

    @staticmethod
    def new_from_json_dict(data):
        return User(user_id=data.get('user_id'),
                    name=data.get('name'),
                    nick=data.get('nick'),
                    uri=data.get('uri'),
                    icon_uri=data.get('icon_uri'))


class Api(object):
    """Stack Stock Books API クラス
    """
    USER_ID_TYPE_ID = "id"
    USER_ID_TYPE_NAME = "name"
    
    ORDER_ASC = "mumble_id_asc"
    ORDER_DESC = "mumble_id_desc"
    
    def __init__(self):
        self.base_url = "http://stack.nayutaya.jp/api"

    def get_mumbles(self, include_books=True, include_authors=False,
                    order=ORDER_DESC, page=1):
        """つぶやき一覧取得
        TODO: include_authors は現在未実装
        """
        api_name = 'get_mumbles'

        query_dict = {}
        if include_books:
            query_dict['include_books'] = 'true'
        if order != self.ORDER_DESC:
            query_dict['order'] = self.ORDER_ASC
        if page > 1:
            query_dict['page'] = page
        query = urllib.urlencode(query_dict)
 
        url = "%s/mumbles.json?%s" % (self.base_url, query)
        logging.debug("[%s]: url: %s", api_name, url)

        data = self._get_data(api_name, url)
        return [Mumble.new_from_json_dict(m) for m in data['response']['mumbles']]

    def get_user(self, user_id, user_id_type=USER_ID_TYPE_ID):
        """利用者情報取得
        """
        api_name = 'get_user'
        url = "%s/user/%s/%s.json" % (self.base_url, user_id_type, user_id)
        logging.debug("[%s]: url: %s", api_name, url)

        data = self._get_data(api_name, url)
        logging.debug("[%s]: %s", api_name, data['response']['user'])

        return User.new_from_json_dict(data['response']['user'])

    def _get_data(self, api_name, url):
        """指定 API を実行して結果を返す
        """
        data = {}
        f = urllib2.urlopen(url)
        try:
            if not (200 <= f.code < 300):
                e = SSBRequestError(f.code, f.msg)
                logging.error("[%s] SSBRequestError: %s", api_name, e)
                raise e
            data = json.load(f)
        finally:
            f.close()
        self._check_api_success(api_name, data)
        return data

    def _check_api_success(self, api_name, data):
        """API 処理が失敗したら例外を発生させる
        """
        if data.get('success', True):
            return
        e = SSBResponseError(data.get('success', True))
        raise e


使い方は、

>>> import ssb
>>> api = ssb.Api()
>>> m = api.get_mumbles()[0]
>>> print m.book.title
Python ポケットリファレンス (Pocket Reference)
>>> print m.body
結構いい本
>>> u = api.get_user(m.user_id)
>>> print u.nick
namaco

こんな感じです。


http://bitbucket.org/ae35/python-ssb/