2010年11月13日

GData 3 の discovery の中身

google-api-python-client の discovery.py を確認していきます。 "Client for discovery based APIs"、つまり、GData 3.0 のコアとなる部分です。

この記事は changeset:102:d85997e9954e に基づいています。 読み間違いなどがあればご指摘ください。

使い方の確認

いきなりライブラリのコードを読み始めると抽象度が高くて理解できない場合がほとんどですので、サンプルコードを見ていきます。 samples/buzz/buzz.py でも良いのですが、なんとなく samples/appengine/main.py にします。 これは http://m-buzz.appspot.com/ で動いているものと同等です。 常に最新のコードがデプロイされているのかは分かりません。

ソースディレクトリの中身は次のようになっています。

$ cd samples/appengine

$ ls -l
total 15
lrwxrwxrwx  1 shigeru   16 Nov 13 10:14 apiclient -> ../../apiclient//
-rw-r--r--  1 shigeru  265 Nov 13 10:14 app.yaml
lrwxrwxrwx  1 shigeru   15 Nov 13 10:14 httplib2 -> ../../httplib2//
-rw-r--r--  1 shigeru  471 Nov 13 10:14 index.yaml
-rwxr-xr-x  1 shigeru 3400 Nov 13 10:14 main.py*
lrwxrwxrwx  1 shigeru   12 Nov 13 10:14 oauth2 -> ../../oauth2/
lrwxrwxrwx  1 shigeru   17 Nov 13 10:14 simplejson -> ../../simplejson//
drwxr-xr-x+ 1 shigeru    0 Nov 13 10:14 static/
lrwxrwxrwx  1 shigeru   18 Nov 13 10:14 uritemplate -> ../../uritemplate//
-rw-r--r--  1 shigeru  766 Nov 13 10:14 welcome.html

依存ライブラリへはシンボリックリンクになっており、残りは簡単な AppEngine/Python の構成です。 ということで main.py を確認します。

main() 関数にも書いてあるように、ふたつのハンドラがあります。

  • MainHandler ... 認証トークンが存在する場合には Buzz の API から activitylist を取得します。 認証されていない場合は OAuth 3-way ダンスを開始します。

  • OAuthHandler ... OAuth 3-way ダンスのコールバックです。認可に成功すると、データストアに認証トークンを保存します。

認証に関しては FlowThreeLegged のメソッド名そのままで分かりやすいですね。 MainHandler で一時トークンを flow として保存しておき、 OAuthHandler でその一時トークンを使って認証トークンを credentials として保存します。 中身 は大変かもしれませんが、使う分には簡単です。

さて、メインの Buzz API を叩くコードの該当部分です。

@login_required
def get(self):
  user = users.get_current_user()
  c = Credentials.get_by_key_name(user.user_id())

  if c:
    http = httplib2.Http()
    http = c.credentials.authorize(http)
    p = build("buzz", "v1", http=http)
    activities = p.activities()
    activitylist = activities.list(scope='@consumption',
                                   userId='@me').execute()
    logging.info(activitylist)
    path = os.path.join(os.path.dirname(__file__), 'welcome.html')
    logout = users.create_logout_url('/')
    self.response.out.write(
        template.render(
            path, {'activitylist': activitylist,
                   'logout': logout
                   }))

やっていることは次の5点ですが、大事なのは3と4です。

  1. ログインユーザーの認証トークンをデータストアから取得する。
  2. HTTP トランスポート (httplib2.Http()) を用意する。
  3. build() でサービスを生成する。 (from apiclient.discovery import build)
  4. Buzz の activitylist を取得する。
  5. activitylist をテンプレートに渡す。テンプレートでは for ループを回す。

build() にはサービス名とバージョンを渡します。 これが URL Templates に差し込まれてサービスディスクリプタの URL となります。 要するに これ のことです。 WSDL みたいなものの JSON 表現です。 これをライブラリの中で解析して Service オブジェクトが返されます。

build() に http を渡しているのは、認証情報 (OAuth で使う Authorization ヘッダー) を生成するためです。 これによって、 Service オブジェクトはユーザーに関する操作を自由に受け付けることができます。 ということで p.activities() で DAO (Data Access Object) のようなものを生成し、 list() メソッドで Activity の一覧を取得します。

サービスに対してどのようなメソッドを使えるのか?は、サービスディスクリプタによって異なります。 これをよしなに処理してくれるのが「discovery」と言えます。 分散システムにおけるサービスの発見をどうするか?という問題は繰り返し扱われてきたわけですが、 Google がいろいろやってきて得られた知見を詰め込んだ、という感じでしょうか。

ライブラリのコード

AppEngine のサンプルコードで見たように、ライブラリの入り口は build() メソッドです。

def build(serviceName, version, http=None,
    discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
  params = {
      'api': serviceName,
      'apiVersion': version
      }

  if http is None:
    http = httplib2.Http()
  requested_url = uritemplate.expand(discoveryServiceUrl, params)
  logging.info('URL being requested: %s' % requested_url)
  resp, content = http.request(requested_url)
  service = simplejson.loads(content)

  fn = os.path.join(os.path.dirname(__file__), "contrib",
      serviceName, "future.json")
  try:
    f = file(fn, "r")
    d = simplejson.load(f)
    f.close()
    future = d['resources']
    auth_discovery = d['auth']
  except IOError:
    future = {}
    auth_discovery = {}

  base = urlparse.urljoin(discoveryServiceUrl, service['restBasePath'])
  resources = service['resources']

  class Service(object):
    """Top level interface for a service"""

    def __init__(self, http=http):
      self._http = http
      self._baseUrl = base
      self._model = model
      self._developerKey = developerKey

    def auth_discovery(self):
      return auth_discovery

  def createMethod(theclass, methodName, methodDesc, futureDesc):

    def method(self):
      return createResource(self._http, self._baseUrl, self._model,
          methodName, self._developerKey, methodDesc, futureDesc)

    setattr(method, '__doc__', 'A description of how to use this function')
    setattr(method, '__is_resource__', True)
    setattr(theclass, methodName, method)

  for methodName, methodDesc in resources.iteritems():
    createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
  return Service()

個人的にはかなり複雑な印象ですが、 service = simplejson.loads(content) のブロックまでは簡単です。 先ほど確認したようにサービスディスクリプタは JSON で記述されていますので、それを HTTP 経由で取得して読み込んでいます。

次に、"contrib/{servieName}/future.json" を読み込みます。 Buzz のファイルはソースツリーの ここ で確認できます。 JSON ファイルはどれも似ていますが、OAuth ダンスの authorize の url がサービスによって違います。 どのサービスにアクセスを許可させるか制御するためです。 実装の方では resourcesauth のふたつのキーに対応するデータを読み込みます。 ファイルを読み込むときにエラーが発生した場合はデータを空の辞書 (dict) にしておきます。 futurecreateMethod() の引数に与えて、 auth_discoveryService インスタンスの auth_discovery メソッドの戻り値とします。 どちらも記述方法、特に変数スコープの扱い、が難しいと感じました。 読んでみれば納得できますが、自分で書くのは大変そうです。

残りは Service クラスの振る舞いに関することです。 コメントに "Top level interface for a service" とあるように、これがサービスのインターフェイスを定義します。 具体的なメソッドは createResource() で定義されます。 ここでメソッドにデフォルトのコメントが割り当てられていることも分かります。

createResource() は、メソッド内で定義されている Resource のインスタンスを返します。 このメソッド定義は長いので省略しますが、 Resource はコメントに "A class for interacting with a resource." とあるように、 リソースとのやり取りを担当します。 ディスクリプタを確認すると CRUD や list などが定義されていることを確認できます。 また、 setattr(method, '__doc__', ''.join(docs)) から分かりますが、きちんと記述されていればメソッドに関するドキュメントも動的に生成します。 生成された API ドキュメントは AppEngine 上のサイト (http://api-python-client-doc.appspot.com/) で公開されています。 本日時点 (2010年11月13日) では次のバージョンになっていました。

v1:
  • buzz
  • moderator
  • latitude
  • customsearch
  • diacritize
v1.1:
  • prediction
v2:
  • translate

リソースに対する API は動的に構築されますので、素のままでは IDE との相性が悪そう、ということは wiki にも書かれています。 定義ファイルを読み込ませたら自動で生成するプラグイン機構などが必要になりそうです。

感想

discovery.py の使い方と中身をザーッと見てきました。 アルファバージョンなので今後の大きな変更も考えられますが、使い方は分かりやすいのではないか、と思います。 Python の書き方の勉強にもなりますね。

当初は samples/appengine のようなものを実装しようと思っていましたが、サンプルで既に存在していたのでソースコードリーディングをしてみました。 完結かつ分かりやすいですね。

コメントを投稿