Google App Engine で dropbox API を使うにあたって

dropbox for developers の このページで公開されている python のライブラリを Google App Engine 上で使うに当たって、いろいろと下準備をしないといけなかった。
他の人も公開されているライブラリを Google App Engine 上で使うかもしれないので、下準備について書いておこうと思う。

パッケージのダウンロード

Google App Engine ではこのドキュメントにあるとおり、必要最低限のPython のパッケージモジュールしか Google App Engine にインストールされていない。
このため、dropbox で公開しているライブラリを使うためには、以下のパッケージが必要になる。

  • oauth
  • poster
  • dropbox client library

ブラウザかコマンドで上記のパッケージをダウンロードしてきましょう。

以下は、wget でダウンロードしてきたもの。

$ wget http://pypi.python.org/packages/source/o/oauth/oauth-1.0.1.tar.gz#md5=30ed3cc8c11d7841a89feab437aabf81
$ wget http://pypi.python.org/packages/2.5/p/poster/poster-0.6.0-py2.5.egg#md5=5153835c7b82cb5f1a7a2197c7f0d2a2
$ wget https://www.dropbox.com/static/developers/dropbox-client-python-BETA.tar.gz

oauth と poster は pypiで公開されている正式パッケージをダウンロードする。
現時点では、oauth のバージョンは 1.0.1、poster のバージョンは 0.6.0 が最新バージョンみたい。
dropbox client library のパッケージは、dropbox 本家で公開されているものをダウンロードする。

zip アーカイブ

次に、ダウンロードしたパッケージを zip アーカイブする。
Google App Enginepypi などでダウンロードしてきたパッケージを利用するにはいろいろな方法があると思うが、zip 形式でアーカイブされたパッケージを sys.path.insert でインポートする方法がパッケージの管理が楽なためこの方法で行う。
zip アーカイブされたパッケージをインポートする方法は後で説明する。

ダウンロードしてきた、poster のパッケージである poster-0.6.0-py2.5.egg は既に zip アーカイブされているため、これに関してはこれをそのまま利用する。
oauth、dropbox client library はソースファイルであるため、zip アーカイブする必要がある。

oauth の zip アーカイブ

以下のコマンドを実行。

$ tar xvf oauth-1.0.1.tar.gz
x oauth-1.0.1/
x oauth-1.0.1/._LICENSE.txt
x oauth-1.0.1/LICENSE.txt
x oauth-1.0.1/oauth/
x oauth-1.0.1/oauth/__init__.py
x oauth-1.0.1/oauth/example/
x oauth-1.0.1/oauth/example/client.py
x oauth-1.0.1/oauth/example/._server.py
x oauth-1.0.1/oauth/example/server.py
x oauth-1.0.1/oauth/._oauth.py
x oauth-1.0.1/oauth/oauth.py
x oauth-1.0.1/oauth.egg-info/
x oauth-1.0.1/oauth.egg-info/dependency_links.txt
x oauth-1.0.1/oauth.egg-info/PKG-INFO
x oauth-1.0.1/oauth.egg-info/SOURCES.txt
x oauth-1.0.1/oauth.egg-info/top_level.txt
x oauth-1.0.1/oauth.egg-info/zip-safe
x oauth-1.0.1/PKG-INFO
x oauth-1.0.1/setup.cfg
x oauth-1.0.1/._setup.py
x oauth-1.0.1/setup.py
$ cd oauth-1.0.1
$ sudo python setup.py bdist_egg
running bdist_egg
running egg_info
writing oauth.egg-info/PKG-INFO
writing top-level names to oauth.egg-info/top_level.txt
writing dependency_links to oauth.egg-info/dependency_links.txt
reading manifest file 'oauth.egg-info/SOURCES.txt'
writing manifest file 'oauth.egg-info/SOURCES.txt'
installing library code to build/bdist.macosx-10.6-i386/egg
running install_lib
running build_py
creating build
creating build/lib
creating build/lib/oauth
copying oauth/__init__.py -> build/lib/oauth
copying oauth/oauth.py -> build/lib/oauth
creating build/bdist.macosx-10.6-i386
creating build/bdist.macosx-10.6-i386/egg
creating build/bdist.macosx-10.6-i386/egg/oauth
copying build/lib/oauth/__init__.py -> build/bdist.macosx-10.6-i386/egg/oauth
copying build/lib/oauth/oauth.py -> build/bdist.macosx-10.6-i386/egg/oauth
byte-compiling build/bdist.macosx-10.6-i386/egg/oauth/__init__.py to __init__.pyc
byte-compiling build/bdist.macosx-10.6-i386/egg/oauth/oauth.py to oauth.pyc
creating build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying oauth.egg-info/PKG-INFO -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying oauth.egg-info/SOURCES.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying oauth.egg-info/dependency_links.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying oauth.egg-info/top_level.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying oauth.egg-info/zip-safe -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
creating dist
creating 'dist/oauth-1.0.1-py2.5.egg' and adding 'build/bdist.macosx-10.6-i386/egg' to it
removing 'build/bdist.macosx-10.6-i386/egg' (and everything under it)

dist/ に zip アーカイブされたパッケージである oauth-1.0.1-py2.5.egg が作られる。

dropbox client library の zip アーカイブ

dropbox client library の場合 egg を作成する前に、ソースをちょっといじらないといけない。
ダウンロードしてきたソースを解凍してみよう。

$ tar xvf dropbox-client-python-BETA.tar.gz
x dropbox-client-python/
x dropbox-client-python/bin/
x dropbox-client-python/bin/cli_client.py
x dropbox-client-python/bin/oauth_diff.py
x dropbox-client-python/config/
x dropbox-client-python/config/testing.ini.example
x dropbox-client-python/config/trusted_testing.ini.example
x dropbox-client-python/dropbox/
x dropbox-client-python/dropbox/__init__.py
x dropbox-client-python/dropbox/auth.py
x dropbox-client-python/dropbox/client.py
x dropbox-client-python/dropbox/rest.py
x dropbox-client-python/LICENSE
x dropbox-client-python/Makefile
x dropbox-client-python/setup.py
x dropbox-client-python/tests/
x dropbox-client-python/tests/dropbox_tests/
x dropbox-client-python/tests/dropbox_tests/__init__.py
x dropbox-client-python/tests/dropbox_tests/auth_tests.py
x dropbox-client-python/tests/dropbox_tests/client_tests.py
x dropbox-client-python/tests/dropbox_tests/helpers.py
x dropbox-client-python/tests/dropbox_tests/rest_tests.py
x dropbox-client-python/tests/dropbox_tests/trusted_client_tests.py
x dropbox-client-python/tests/file with spaces.txt
x dropbox-client-python/tests/sample_photo.jpg
x dropbox-client-python/tests/中文.txt

dropbox-client-python/dropbox の配下にある3つのスクリプトdropbox client lirbrary で利用するモジュールである。
これら3つのスクリプトでは simplejson が利用されている。しかし、simplejson は Google App Engine では既に django パッケージで利用されているためか、ダウンロートしたソースの状態で利用するとエラーが発生して dropbox client library がまともに動作しない。なので、これら3つのスクリプトで simplejson を利用しているコードを修正する。
修正はこれら3つのスクリプトに記載された、

import simplejson as json

の部分を、

from django.utils import simplejson as json

に書き換えるだけである。
3つのスクリプトを書き換えたら、以下のコマンドを実行する。

$ sudo python setup.py bdist_egg
running bdist_egg
running egg_info
creating dropbox_client.egg-info
writing requirements to dropbox_client.egg-info/requires.txt
writing dropbox_client.egg-info/PKG-INFO
writing top-level names to dropbox_client.egg-info/top_level.txt
writing dependency_links to dropbox_client.egg-info/dependency_links.txt
writing manifest file 'dropbox_client.egg-info/SOURCES.txt'
reading manifest file 'dropbox_client.egg-info/SOURCES.txt'
writing manifest file 'dropbox_client.egg-info/SOURCES.txt'
installing library code to build/bdist.macosx-10.6-i386/egg
running install_lib
running build_py
creating build
creating build/lib
creating build/lib/dropbox
copying dropbox/__init__.py -> build/lib/dropbox
copying dropbox/auth.py -> build/lib/dropbox
copying dropbox/client.py -> build/lib/dropbox
copying dropbox/rest.py -> build/lib/dropbox
creating build/bdist.macosx-10.6-i386
creating build/bdist.macosx-10.6-i386/egg
creating build/bdist.macosx-10.6-i386/egg/dropbox
copying build/lib/dropbox/__init__.py -> build/bdist.macosx-10.6-i386/egg/dropbox
copying build/lib/dropbox/auth.py -> build/bdist.macosx-10.6-i386/egg/dropbox
copying build/lib/dropbox/client.py -> build/bdist.macosx-10.6-i386/egg/dropbox
copying build/lib/dropbox/rest.py -> build/bdist.macosx-10.6-i386/egg/dropbox
byte-compiling build/bdist.macosx-10.6-i386/egg/dropbox/__init__.py to __init__.pyc
byte-compiling build/bdist.macosx-10.6-i386/egg/dropbox/auth.py to auth.pyc
byte-compiling build/bdist.macosx-10.6-i386/egg/dropbox/client.py to client.pyc
byte-compiling build/bdist.macosx-10.6-i386/egg/dropbox/rest.py to rest.pyc
creating build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying dropbox_client.egg-info/PKG-INFO -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying dropbox_client.egg-info/SOURCES.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying dropbox_client.egg-info/dependency_links.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying dropbox_client.egg-info/requires.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
copying dropbox_client.egg-info/top_level.txt -> build/bdist.macosx-10.6-i386/egg/EGG-INFO
zip_safe flag not set; analyzing archive contents...
creating dist
creating 'dist/dropbox_client-1.0-py2.5.egg' and adding 'build/bdist.macosx-10.6-i386/egg' to it
removing 'build/bdist.macosx-10.6-i386/egg' (and everything under it)

zip アーカイブが正常に完了すれば、dist/ に dropbox_client-1.0-py2.5.egg が作られているはず。

zip アーカイブの配置

作成した zip アーカイブのパッケージを、Google App Engine で動作するアプリケーションに配置する。
zip アーカイブのパッケージの配置に関してはいろいろとあると思うが、ここでは、アプリケーションディレクトリの直下に eggs というディレクトリを作成してそこに配置する。
ダウンロードしてきた poster のパッケージと、zip アーカイブを作成した oauth と dropbox client library のパッケージ配置すると以下のようになる。

myapp/eggs
myapp/eggs/oauth-1.0.1-py2.5.egg
myapp/eggs/poster-0.6.0-py2.5.egg
myapp/eggs/dropbox_client-1.0-py2.5.egg

zip アーカイブのインポート

dropbox client library を利用するスクリプトの py ファイルの先頭に以下のコードを記載する。

# import eggs
import os
import sys
for arch in os.listdir('eggs'):
    sys.path.insert(0, os.path.join('eggs', arch))

このコードは、myapp/eggs にある zip アーカイブされた拡張子 egg ファイルを読み込んで、zip インポートを実行するコードである。

これでこのコード以降、dropbox client library が利用できるようになる。
後は、お好きなように♪

辞書オブジェクト

何かとプログラミングでお世話になる辞書オブジェクトというか連想配列
もはやなくてはならないデータ型の一つですが、
Windows の COM で実装された辞書オブジェクトの動作にはまって
不可解なバグを作ってしまうという醜態をしてしまったので、
他の方も、自分と同じくはまってしまわないよう、情報展開しておこうと思う。


さてそれは何か。


Windows の COM で実装された辞書オブジェクト、いわゆる Scripting::Dictionary は
登録されていないキーを指定して、このオブジェクトにアクセスすると
素数が増えてしまうんです。


とりあえず、コードで書いた方が分かると思うので VB6 で書いてみた。

Option Explicit

Sub Main()
    
    ' 辞書オブジェクトを作成
    Dim oDict As Object
    Set oDict = CreateObject("Scripting.Dictionary") ' VBだとScripting.Dictionaryでオブジェクトを作る
    
    ' 自分で定義したPersonクラスのオブジェクトを作成
    Dim oPerson As Person
    Set oPerson = New Person
    
    ' 以下 oPerson のプロパティを設定
    With oPerson
        ' ....
    End With
    
    ' ここで辞書オブジェクトにPersonオブジェクトを登録
    Call oDict.Add("foo", oPerson)
    
    ' ここで辞書オブジェクトに登録されている要素数をCountプロパティでデバッガに出力
    '   -> Countプロパティから取得できる値は1になる
    Debug.Print "Dictionray Count (Before) : " & oDict.Count
    
    ' ここで辞書オブジェクトに登録されていないキーを指定して取得してみる
    Dim oResult As Person
    Set oResult = GetItem("bar", oDict) ' GetItemメソッド経由で取得する
    
    ' ここで辞書オブジェクトに登録されている要素数を出力
    '   -> Countプロパティから取得できる値は1のはず。。。
    Debug.Print "Dictionray Count (After) : " & oDict.Count

    ' 辞書オブジェクトに登録されていないキーを指定した場合型は何が取得できるのかな。
    Debug.Print "TypeName : " & TypeName(oDict("bar"))
    
End Sub


' アイテムを取得する
Private Function GetItem(ByVal strKey As String, ByRef objDict As Object) As Variant

On Error GoTo Catch
    
    Dim vItem As Variant
    Set vItem = objDict(strKey)
    
    Goto Finally
Catch:
    Debug.Print "Raise Error !! : " & Err.Description
    Set vItem = Nothing
    
Finally:
    
    Set GetItem = vItem
    
End Function


で書いた VB6 のコードを実行させるとこんな感じで出力される。

Dictionray Count (Before) : 1
Raise Error !! : 型が一致しません。
Dictionray Count (After) : 2
TypeName : Empty


なんと、辞書オブジェクトの Count プロパティで取得できる要素数
1 から 2 に増えているでありませんか。

これは、MS のオンラインの MSDN によると仕様のようです。
Item プロパティの解説にこんな感じで載っていた。

項目を変更するときに引数 key で指定したキーが見つからない場合、newitem で指定した項目と関連付けられた、引数 key で指定した新しいキーが作成されます。
また、既存の項目を取得するときに引数 key で指定したキーが見つからない場合は、空の項目と関連付けられた、引数 key で指定した新しいキーが作成されます。


というわけで、COM の辞書オブジェクトでを使う場合は、
Count プロパティで取得できる要素数は動的に変わってしまう可能性があるということになります。

実装する処理によっては、Exists メソッドでキーに該当するアイテムが
辞書オブジェクトに登録されているかどうかチェックしてから、
Item プロパティや a("key1") みたいにアクセスしないといけないですね。

VBAWSH 等で COM の辞書オブジェクトを使う場合は気を付けましょう。



ここで、他の場合はどういう動きをするかどうか気になったので確認してみた。


まずは、.NET Framework の Dictionary オブジェクト。
以下は、C#で書いたコード。

using System;
using System.Collections.Generic;
using System.Text;

namespace DictionaryTest
{
  class Program
  {
    static void Main(string[] args)
    {
      // 辞書オブジェクトを作成
      Dictionary<string, Person> dict = new Dictionary<string, Person>();

      // 自分で定義したPersonクラスのオブジェクトを作成
      Person person = new Person();

      // 以下Personオブジェクトのプロパティ設定
      // ...

      // ここで辞書オブジェクトにPersonオブジェクトを登録
      dict.Add("foo", person);

      // ここで辞書オブジェクトに登録されている要素数をCountプロパティでコンソールに出力
      //   -> Countプロパティから取得できる値は1になる
      Console.WriteLine("Dictionary Count (Before) : {0}", dict.Count);

      
      // ここで辞書オブジェクトに登録されていないキーを指定して取得してみる
      Person result = GetItem("bar", dict); // GetItemメソッド経由で取得する
    
      // ここで辞書オブジェクトに登録されている要素数を出力
      //   -> Countプロパティから取得できる値は1のはず。。。
      Console.WriteLine("Dictionary Count (After) : {0}", dict.Count);

    }

    static Person GetItem(string key, Dictionary<string, Person> dict)
    {
      Person person = null;

      try
      {
        person = dict["key1"];
      }
      catch(Exception e)
      {
        Console.WriteLine("Raise Exception !! : {0}", e);
      }

      return person;
    }
  }
}


実行結果は、以下のとおり。

Dictionary Count (Before) : 1
Raise Exception !! : System.Collections.Generic.KeyNotFoundException: 指定された
キーはディレクトリ内に存在しませんでした。
場所 System.ThrowHelper.ThrowKeyNotFoundException()
場所 System.Collections.Generic.Dictionary`2.get_Item(TKey key)
場所 DictionaryTest.Program.GetItem(String key, Dictionary`2 dict) 場所 C:\Us
ers\xxxx\Desktop\DictionaryTest\DictionaryTest\Program.cs:行 43
Dictionary Count (After) : 1


.NET Framework の辞書オブジェクトは、登録されていないキーでアクセスすると
例外が発生し、要素数は変化しません。


続いて、Python
こちらは、Mac OS XMacPorts でインストールした Python 2.5.5 で確認してみました。

macbook-2:Memo kazupon$ python
Python 2.5.5 (r255:77872, Jun 6 2010, 13:45:39)
[GCC 4.2.1 (Apple Inc. build 5659)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> d = { "key1":1 }
>>> len(d)
1
>>> d['key2']
Traceback (most recent call last):
File "", line 1, in
KeyError: 'key2'
>>> len(d)
1


Python の場合も、辞書オブジェクトに登録されていないキーでアクセスすると
例外が発生し、要素数は変化しないみたいです。


最後に、Ruby

macbook-2:Memo kazupon$ ruby -v
ruby 1.8.7 (2009-06-12 patchlevel 174) [i686-darwin10]
macbook-2:Memo kazupon$ irb
irb(main):001:0> h = { 'key1' => 1 }
=> {"key1"=>1}
irb(main):002:0> h.count
=> 1
irb(main):003:0> h['key2']
=> nil
irb(main):004:0> h.count
=> 1


こちらも、Mac OS XMacPorts でインストールした Ruby 1.8.7 で動作確認してみました。
Ruby の場合は、辞書オブジェクト(Ruby の場合は連想配列と呼びますが。)に登録されていないキーでアクセスすると
例外が発生せず、nil が取得できるようです。
これは、COM の辞書オブジェクトで Empty が取得できるのと似ています。
ただ、要素数は変化しないようです。


というわけで、言語によって辞書オブジェクトは
いろいろな動作をするみたいです。

八戸

2010年にはいってから、何かと故郷の八戸に何回か帰っています。
なんで八戸にちょくちょく帰っているかというと
今、八戸がホットなんです!!

地元では八戸を変えようと、いろんな動きがでてきています。

こうした活動によって、個々のフリーランスが集まってユニットを立ち上げたり、
Twitter を通じて地元の人たちがつながって写真部が結成されたりと、
八戸で何かが変わりつつあります。


保守的な考えに飲まれやすい環境の中で、
自分から変わろうとする人たちが故郷で増えてきているのは、
大変うれしいし、自分もモチベーションがかなり上がります。


そんなホットな八戸で、いっしょに自分も何かできればいいなあと思い、
まずは、最近本業の仕事とは別にいっしょに活動しているクリフトファー
いっしょにセミナーをしてきました。
勉強会の様子は Ustream にあります。ご協力頂いた tripod-studio さんには大変感謝しています。

※注意:セミナーは3時間ですので、動画は長いです。


セミナーを主催するというは初めてでしたので、
まあいろいろと変な点というか、問題点はいろいろあったと思っています。
でも、意外と好評だったので、主催してよかったと感じています。


今後は、こういったセミナーの他、ソフトウェアエンジニアとして何か作りながら
関東でも八戸出身者、興味がある方のネットワーキングをやっていきたいと思います。

2009のおわりに

年の瀬が迫ってきました。
というわけで、2009は自分にとってどういう年だったか振り返ってみました。

これまでに体験した出来事や参加したイベント

参加したイベントや起きた出来事の観点で整理してみました。

列挙してみると、たくさんいろんなことを経験したり体験してきたなあと感じています。社内研修で論文発表したり、海外に行ってきたり、勉強会やセミナーに参加したりと。また、オフ会を通じたいろんな人との交流や、おめでたいことや悲しいことまでも。去年と比べるとありえないぐらいの量だと思っています。

中でも一番は、やっぱりシリコンバレーカンファレンス
シリコンバレーの空気を肌で感じ、そして現地のオフィス環境や現地で働く人の話を通じることで、自分の好きなコード書きながら、現地の人々みたいに幸せに生きていこうと、改めてこれから自分の進む道を再認識させてくれたし、そして、このカンファレンスを通じて知り合った素晴らしい人たちと出会えたし。中でも次のロールモデルになるような人(というより今のロールモデル)と出会えることができてホントよかったです。これを機に、今までにないくらい会社の業務以外で異なるプラットフォームでプログラミングするようになったし、そしてセミナーや交流会といったイベントにも積極的に参加するようになりました。

励んだこと

自発的に励んだことについて整理してみました。

英語は、シリコンバレーカンファレンスで自分の英語力のなさを身を持って経験したせいですね。おかげで、会社の英会話研修や、smart.fm や iPhone アプリ、Twitter で英語でつぶやいたりするようになった感じです。(とはいっても英語力はまだまだですが。。。)


rubyTwitter2MixiVoice という Twitter のつぶやきを mixi ボイスに投稿するという単純なものを作りながら学んだという感じ。いろんな gem に手を出しながらライブラリの使い方を試行錯誤したため、作り終わったのが着手してから2ヶ月後。
時間がかかってしまいましたが、rake といったビルドライブラリ、mechanize といったウェブサイトのアクセスを自動化するライブラリなどなど、いろいろなライブラリについて知ることができたので結果的には良かったと思っています。
ruby を使ってみての感想としては

  • module と iterator は使いこなすと面白そう
  • rspec でプログラムの振る舞いによるテストも気になる
  • 書けば書くほどハマル、そして少ないコード量

という感じです。


objective-c は、iPhone アプリに興味があったということもあり、ケイレキ.jpAPI を公開したということで、これを機に ケイレキ.jp の iPhoneクライアントを作りながら覚えようという感じで始めました。ここ最近始めたばっかりなのでまだまだですが、来年 2010 のケイレキ Connect のパーティがあるんで、それまでにはなんとかタイムラインを表示できるとこまでモノを作らねばと考えています。
objective-c を使ってみての感想としては、

  • ブランケット"[...]"でオブジェクトにメッセージ送信って SmallTalkっぽいなあ
  • リファレンスカウンタのメモリ管理は大変
  • Rumtime API 使えばいろいろと Hack できそう
  • Category で既存フレームワークを簡単に拡張できそう
  • Delegate で Event を実現するのね

といった感じです。


git は、やるきっかけはシリコンバレーのカンファレンスで知り合った人たちのフリーなプロジェクトがきっかけです。ソースの管理を github でしてるんだけど、そのためには git が使えないといけないわけで。使っていくうちに git の柔軟さにやられてしまいました。

これからも続けていくこと

課題

プログラミング言語を学びながら何かを作っているわけですが、途中ヤクの毛刈りにはまってしまい、モノが完成するまで時間がかかってる。
いかに自分のフリーなプロジェクトとはいえ、時間は有限なのでこれでは駄目だ。 redmine とか導入して管理せねば。
英語も最近ちょっと停滞気味。なんとかしないと。

チャレンジ

  • slimetimeriPhone アプリの作成 (無いなら作ろう)
  • android アプリの作成 (今の Java を学ぼう)
  • Webサービスの立ち上げ (Amazon EC2 で立ち上げよう)
  • プロジェクトへの参加 (とりあえず hirashima さんのプロジェクトに打診してみよう)
  • 勉強会での発表 (プレゼン能力を鍛えよう)
  • ボランティアへの参加 (せんべい汁研究所のサポーターズになろう)
  • 転職 (活動再開しよう)

まとめ

シリコンバレーカンファレンスをきっかけに、「自分が変わる」ことができてよかったです。来年はもっと作ってアウトプットしていきたいと思います。

今年もありがとうございました。

Twitter::RESTError エラー

http://twitter4r.rubyforge.org/rdoc/を元に、
twitter4r API サンプルの動作確認していたら Twitter::RESTError のエラーが発生。

具体的には、

require 'rubygems'
gem 'twitter4r', '>=0.3.0'
require 'twitter'
require 'twitter/console'
require 'kconv'

# Create Twitter::Client Instance
client = Twitter::Client.from_config('twitter.yaml', 'envname')

# Get Twitter::User Instance
user = client.user('username')

# Print Twitter::User Attributes
Twitter::User::attributes.each do |attribute|
  puts "#{attribute} : #{Kconv.kconv(user.send(attribute).to_s, Kconv::UTF8)}"
end

のようなユーザ情報を表示するようなコードを実行させたら、
user メソッドで以下のようなエラーが発生した。

/opt/local/lib/ruby/gems/1.8/gems/twitter4r-0.3.1/lib/twitter/client/base.rb:39:in `raise_rest_error': Not Found (Twitter::RESTError)
from /opt/local/lib/ruby/gems/1.8/gems/twitter4r-0.3.1/lib/twitter/client/base.rb:44:in `handle_rest_response'
from /opt/local/lib/ruby/gems/1.8/gems/twitter4r-0.3.1/lib/twitter/client/base.rb:18:in `http_connect'
from /opt/local/lib/ruby/1.8/net/http.rb:543:in `start'
from /opt/local/lib/ruby/gems/1.8/gems/twitter4r-0.3.1/lib/twitter/client/base.rb:14:in `http_connect'
from /opt/local/lib/ruby/gems/1.8/gems/twitter4r-0.3.1/lib/twitter/client/user.rb:37:in `user'
from user_print.rb:11

Not Found という吐き出されたエラー内容から twitter4r が
twitter REST API で URL を正しく設定できていないんではないかと
疑いのもとスタックトレース情報を元に twitter4r のソースをたどっていくと、
user.rb 内で定義されている :info の定数が間違っているのが原因っぽいのが判明。

# twitter4r-0.3.1/lib/twitter/client/user.rb:2

class Twitter::Client
  @@USER_URIS = {
  	:info => '/users/show',
  	:friends => '/statuses/friends.json',
  	:followers => '/statuses/followers.json',
  }

:info だけフォーマットが指定されてない。。。。

:info が '/users/show' となっているので、
user.rb の user メソッドにユーザIDもしくはスクリーン名が指定されると、
user メソッド内で呼び出される、 create_http_get_request メソッドに
'/users/show'(@@USER_URIS[action]) と {id=>'username'}(params)
が指定されてしまいます。

# twitter4r-0.3.1/lib/twitter/client/user.rb:37

def user(id, action = :info, options = {})
  raise ArgumentError, "Invalid user action: #{action}" unless @@USER_URIS.keys.member?(action)
  id = id.to_i if id.is_a?(Twitter::User)
  params = options.merge(:id => id)
  response = http_connect {|conn| create_http_get_request(@@USER_URIS[action], params) }
  bless_models(Twitter::User.unmarshal(response.body))
end


create_http_get_request メソッドでは
twitter に HTTP の GET でアクセスするための
Net::HTTP::Get のインスタンスを作成して呼び出し元に返しています。
その際に、path を指定しているが path は create_http_get_request メソッド
に指定されたパラメータにより '/users/show?id=username' となり、
その path が指定された Net::HTTP::Get のインスタンスが呼び出し元に返されてしまっています。

# twitter4r-0.3.1/lib/twitter/client/base.rb:79

def create_http_get_request(uri, params = {})
	path = (params.size > 0) ? "#{uri}?#{params.to_http_str}" : uri
  Net::HTTP::Get.new(path, http_header)
end


そんでもって、 そのインスタンスを利用して http_connect メソッドで
twitter にアクセスしてユーザー情報を得ようするみたいですが、
そんな指定された場所はありませんと怒られてしまってエラーとなり、
user メソッドでユーザー情報が取得できないという状況のようです。


というわけで、 @@USER_URIS の :info を '/users/show.json' にしてみたら動きました。
こうすることで、create_http_get_request メソッドで
path に '/users/show.json?id=username' となるおかげで、
twitter にアクセスできるようです。

以下は、その実行結果。

id : 17755912
name : kazuya kawaguchi
description : かずぽんです。仕事はソフト開発してます。よろしく。
location : iPhone: 35.689976,139.697830
screen_name : kazu_pon
url : http://d.hatena.ne.jp/kazu_pon/
protected : false
profile_image_url : http://a1.twimg.com/profile_images/274655366/profile_normal.jpg
profile_background_color : 0099B9
profile_text_color : 3C3940
profile_link_color : 0099B9
profile_sidebar_fill_color : 95E8EC
profile_sidebar_border_color : 5ED4DC
profile_background_image_url : http://s.twimg.com/a/1250809294/images/themes/theme4/bg.gif
profile_background_tile : false
utc_offset : 32400
time_zone : Tokyo
following : false
notifications : false
favourites_count : 123
followers_count : 114
friends_count : 168
statuses_count : 1340
created_at : Sun Nov 30 15:33:02 +0000 2008

ちなみに、 my メソッドもこれで動くようになります。

そんなわけで、とりあえず動作するようになったけでなんですが、
'/users/show.json?id=username' でユーザー情報を取得する方法って、
Twitter API 仕様書 第二十版 (2009年4月16日版) や本家の http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
の例をみるとないんですよね。。。
それとも実はある?というより、自分が知らないだけ?。
もし、この方法が正式な方法ではない場合、twitter4r の作者に
なんとかしてもらいたなぁと思います。
自分がパッチを作るという案もありますが。

rubyで単体テスト

 ruby on rails で本格的にアプリ作る前に、ruby単体テストってどんなもんかごにょごにょと書いてみた。

 テストされるコードは以下の適当なコード。

class Foo
  
  def foo
    "foo"
  end
  
  def bar(arg)
    raise ArgumentError
  end
  
  def hoge
    throw :hoge_error
  end
  
  def hogehoge(arg)
    arg
  end
  
  def foo?
    true
  end
  
  def one
    1
  end
  
  def two
    2
  end
  
  def >(obj)
    two > obj.one
  end
  
  def calc_delta(arg)
    arg / 10**6
  end
  
end

テストコードはこんな感じで書いてみた。

require "test/unit"
require "foo"

class TestFoo001 < Test::Unit::TestCase
  
  def setup
    # ごにょごにょと前処理
    @obj1 = Foo.new
    @obj2 = Foo.new
  end
  
  def teardown
    # ごにょごにょと後処理
  end
  
  # assert のサンプル
  def test_assert
    assert(@obj1.foo?, "失敗メッセージ")
  end
  
  # assert_equal のサンプル
  def test_assert_equal
    assert_equal(@obj1.one, @obj1.one, "失敗メッセージ")
  end
  
  # assert_not_equal のサンプル
  def test_assert_not_equal
    assert_not_equal(@obj1.one, @obj1.two, "失敗メッセージ")
  end
  
  # assert_instance_of のサンプル
  def test_assert_instance_of
    assert_instance_of(Foo, @obj1, "失敗メッセージ")
  end
  
  # assert_nil のサンプル
  def test_nil
    assert_nil(nil, "失敗メッセージ")
  end
  
  # assert_not_nil のサンプル
  def test_not_nil
    assert_not_nil(@obj1.nil?, "失敗メッセージ")
  end
  
  # assert_kind_of のサンプル
  def test_assert_kind_of
    assert_kind_of(Foo, @obj1, "失敗メッセージ")
  end
  
  # assert_respond_to のサンプル
  def test_assert_respond_to
    assert_respond_to(@obj1, :foo, "失敗メッセージ")
  end
  
  # assert_match のサンプル
  def test_assert_match
    assert_match(/foo/, @obj1.foo, "失敗メッセージ")
  end
  
  # assert_same のサンプル
  def test_assert_same
    assert_same(@obj1, @obj1)
  end
  
  # assert_not_same のサンプル
  def test_assert_not_same
    assert_not_same(@obj2, @obj1, "失敗メッセージ")
  end
  
  # assert_operator のサンプル
  def test_assert_operator
    assert_operator(@obj1, :>, @obj2, "失敗メッセージ")
  end
  
  # assert_raise のサンプル
  def test_assert_raise
    assert_raise(ArgumentError, "失敗メッセージ") { @obj1.bar }
  end
  
  # assert_nothing_raised のサンプル
  def test_assert_nothing_raise
    assert_nothing_raised(ArgumentError, "失敗メッセージ") { @obj1.foo }
  end
  
  # assert_throws のサンプル
  def test_assert_thorws
    assert_throws(:hoge_error, "失敗メッセージ") { @obj1.hoge }
  end
  
  # assert_nothing_thrown のサンプル
  def test_assert_nothing_thrown
    assert_nothing_thrown("失敗メッセージ") { @obj1.foo }
  end
  
  # assert_in_delta のサンプル
  def test_assert_in_delta
    assert_in_delta(0.05, @obj1.calc_delta(50000.0), 0.00001, "失敗メッセージ")
  end
  
  # assert_send のサンプル
  def test_assert_send
    assert_send([@obj1, :hogehoge, 4], "失敗メッセージ")
  end
  
  # assert_block のサンプル
  def test_assert_block
    assert_block("失敗メッセージ") { @obj1.foo? }
  end
  
end

MS な言語で NUnit単体テストコードを書いてた自分にとっては、
見慣れない assertion 機能があってちょっと困惑気味。
assert_send と assert_block ってどうんな風に使うのかな?
とりあえず、課題としておこう。