Growl 通知テストランナー
[追記1 17:45]
@IanMLewis が pynotify に対応してくれてます。仕事はやい。
次は id:nullpobug が Snarl に対応してくれるのを期待。
[追記2 22:15]
上の追記1で期待アゲしたように、id:nullpobug が Snarl 対応してくれましたよ!
------------
BPStudy#29 にて id:t-wada さんが TDD のデモされてる際、autotest の結果を Growl 通知させてました。お、これいいなーと思ってたんですが、autotest で Growl 通知って Ruby とかじゃ普通な感じなんですかね?
Python でも autotest 的なものがないものかと思っていたところ、id:nullpobug が Modipyd いいよー、とオススメしてくれました。
ならばと思い、Growl 通知する簡単なテストランナーを作ってみましたよ。既に誰かが作ってるんでしょうけど。
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
これではプロジェクトとアプリケーションの結びつきが強すぎて、以下の弊害がおこる。
- アプリケーションの再利用がしづらい
- 将来プロジェクト名を変えたくなっても変更が難しい
なので、このようにしよう。
from xyz.models import Author
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 を引く、なんていうロジック)のはよろしくない。
なんで?
- コードをユニットテストできない
- コードを再利用できない
じゃあどこに書けばいい?
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 だ。
より詳細な情報は:
別のテクニックとして、ローカル環境では、(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 を使うことで、以下のような情報を取得できる
- どれくらいの数の SQL 文が実行されたか?トータルタイムは?
- テンプレート名、ロギング、クッキー/セッション情報、などなど
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 は次のような再利用可能な機能やアプリケーションを提供する。
- openid サポート
- (vCard, Google, Yahoo などからの)連絡先のインポート
- 通知フレームワークなどなど… より詳細な情報はこちら http://pinaxproject.com/
Pinax は James Tauber による強力なアイデアだ。可能なら是非使ってみてほしい。迅速なウェブアプリケーションの構築の助けになるはずだ。
より詳細な情報はこちら
10. 知っておくべきサードパーティ製アプリケーション
使うべきサードパーティ製アプリケーションがいくつかある。
1. マイグレーション
スキーマやデータのマイグレーションはどうしてる?
syncdb するよね。
最初はモデルの設計がデータベースに反映される。
でも何時間、何日か経ったら…
モデルにフィールドを追加したり、削除したり、設計を変更しなければならなくなる。
変更するよね。ここで、どうやってこの変更をデータベースに反映させるかが重大な問題になる。
取れる方法としては、
- もう一度 syncdb する(メンドくさい)
- 手動で alter 文を DB に発行する(メンドくさい)
このようにデータベースをある状態から別の状態に変更することをマイグレーションと呼ぶ。このマイグレーションを楽にしてくれる、いくつかのサードパーティアプリケーションがある。
- django-evolutions (簡単に使えて、自動で全部やってくれるけど、頑丈じゃない)
- South (より強固だけど、ちょっと使い方を覚える必要がある)
2. テンプレート
Django のテンプレートは、制限がキツすぎると思ったら、次のようなものもある。
- template-utils (テンプレートタグやユーティリティよりも、テンプレートの機能を向上)
- Jinja (Django テンプレートと同じシンタックスを採用し、プロジェクトのプラグインとして使える、そしてより柔軟なロジック記述が特徴の、サードパーティのテンプレートシステム)
3. その他
1. django command extensions 次のようなお役立ちコマンドラインを提供してくれる
-
- 全モデルをロードしてくれる shell_plus
- Werkzeug デバッガを組み込んだ runserver_plus
- 上司に見せるためのモデル図生成
などなど。
より詳しい情報はこちら
http://ericholscher.com/blog/2008/sep/12/screencast-django-command-extensions/
2. Sorl はサムネイルを生成してくれる
など
他のリソースはこちら
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
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 では、
- Stack Stock Books から最新のつぶやき取得
- Datastore に保存した前回のつぶやき情報と比較。更新が無ければ終了。
- 新しい情報ならば、Twitter への投稿文言生成。URL は bit.ly API を利用して短縮。
- 文言が140文字を越えていたら、139文字+'…' として切り詰め処理。
- Twitter へ投稿
- Datastore に最新つぶやき情報を登録
という処理をしています。
GAE/Python で Twitter bot を作る(準備編)
先のエントリで bit.ly API モジュールやら Stack Stock Books API モジュールを作ったので、Google App Engine 上で動作する Twitter bot を作ってみます。
先のエントリとやらはこの辺です。
どんな処理にしましょうか?
ざっくりとこんな感じで。
- Stack Stock Books から最新のつぶやき取得
- Datastore に保存した前回のつぶやき情報と比較
- 新しい情報ならば、Twitter への投稿文言生成
- Twitter へ投稿
- Datastore に最新つぶやき情報を登録
- これらの処理を一定間隔で回す
ということで、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 API の Python モジュールを書き散らかしてみました。
今のところの機能としては
- つぶやき一覧取得(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
こんな感じです。