当文書はRetrieving Authenticated Google Data Feeds with Google App Engineの邦訳です。
みなさん Google App Engine をどのように使おうか、ワクワクしながら考えをめぐらせている事と思います。中には私たちの Google Data AtomPub API を使って Google の他サービスと連携するようなアプリケーションを作ろうとしている方もいるでしょう。Google のサービスの一部は GData API を準備しており(面白そうな例として YouTube や Google Calendar、Blogger などがあります。完全なリストはここにあります。)、その API を使う事によりユーザ固有のデータを読み出したり編集したりできるのです。
この記事では、特定ユーザの情報を要求したり取得する過程を一通り体験するのに、Google Documents List Data API を使います。Google Accounts の認証には Users API を、アプリケーションのページを生成するために Google App Engine の webapp フレームワークを使います。
Google Data サービスの中にはデータを読み出すのにユーザの許可が要るものがあり、また全ての Google Data サービスはデータの書き込み前にユーザの許可が必要です。Google は ユーザがあなたのアプリケーションに対してサービスにアクセスする許可を与えるために AuthSub を使っています。
AuthSub を使うと、ユーザはまず google.com の安全な(訳注: https 接続の)ページで自分のパスワードを入力し、次にあなたのアプリケーションにリダイレクトされます。あなたのアプリケーションは、ユーザがアカウントマネージメントで許可を取り消すまでの間、サービスにアクセスできるトークンを受け取ります。
この記事では、ユーザのログイン用リンクを準備する方法、複数のリクエストで利用できるセッショントークンを取得する方法、またデータストアにトークンを格納し、ユーザが一度サイトを離れて、再度戻って来た時にトークンを再利用できるようにする方法を学びます。
Google は GData API からデータを取得したりトークンを管理するのを楽にしてくれる GData Python client ライブラリを提供しています。最近リリースしたバージョンでは Google App Engine のアプリケーションから利用できるようになりました。この記事ではこのライブラリを使用しますが、もちろんあなたのアプリケーションに適した別のものを使っても構いません。gdata-python-client ライブラリをダウンロードしてください。
このライブラリを Google App Engine のアプリケーションで使用するには、単純にライブラリのソースファイルをアプリケーションのディレクトリに置き、いつものようにインポートしてください。アップロードする必要があるのは src/gdata と src/atom です。そして、gdata.service.GDataService オブジェクトのインスタンス全てに対して gdata.alt.appengine.run_on_appengine 関数を必ず実行してください。これだけです!
保護された Google Data feeds にアクセスする許可をユーザから得るために、アプリケーションは AuthSub という API を使います。やり方は簡便です - 保護された Google Data feeds へのアクセスをユーザにリクエストする時には、あなたのアプリケーションはまずユーザを google.com 上のセキュアなページにリダイレクトさせ、そこでユーザはサインインし、アクセスを許可したり拒否したりできます。その後、ユーザは、新しく取得したトークンが URL に埋め込まれた形で、あなたのアプリケーションにリダイレクトされて戻って来ます。
あなたのアプリケーションは AuthSub を使う時に、二つの事を指定する必要があります: アクセスしたいフィードのためのベース URL と、ユーザが(訳注: google.com のセキュアページで)許可を与えた後に戻ってくる事になる、あなたのアプリケーション内の リダイレクト URL です。
トークンをリクエストするためのリンクを作成するために、Google Data client ライブラリに含まれる gdata.service モジュールを使いましょう。このモジュールには、ベースのフィード URL と戻りアドレスから、自動的に正しい URL を生成してくれる GenerateAuthSubURL メソッドがあります。下記のコード中では、ユーザの Google Document List フィードへのアクセスをリクエストする URL を生成するために、このメソッドを使用しています。
app.yaml ファイルでは、それぞれのステップ毎に違う URL を使うような URL マッピングを定義しましょう。下記がその例です:
application: gdata-feedfetcher version: 1 runtime: python api_version: 1 handlers: - url: /step1.* script: step1.py - url: /step2.* script: step2.py ...
このサンプルアプリケーションには、容易にアプリケーションが動作するホストネームを変更するためのsettingsモジュールも含まれています。デフォルトではsettingsモジュールは現在のサーバー設定を検出しようとします。
import os port = os.environ['SERVER_PORT'] if port and port != '80': HOST_NAME = '%s:%s' % (os.environ['SERVER_NAME'], port) else: HOST_NAME = os.environ['SERVER_NAME']
アプリで AuthSub を使うための最初のステップを説明するために、下記のような step1.py を作成しましょう。
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.api import users
import atom.url
import gdata.service
import gdata.alt.appengine
import settings
class Fetcher(webapp.RequestHandler):
def get(self):
next_url = atom.url.Url('http', settings.HOST_NAME, path='/step1')
# Initialize a client to talk to Google Data API services.
client = gdata.service.GDataService()
gdata.alt.appengine.run_on_appengine(client)
# Generate the AuthSub URL and write a page that includes the link
self.response.out.write("""<html><body>
<a href="%s">Request token for the Google Documents Scope</a>
</body></html>""" % client.GenerateAuthSubURL(next_url,
('http://docs.google.com/feeds/',), secure=False, session=True))
def main():
application = webapp.WSGIApplication([('/.*', Fetcher),], debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()
この例で、GenerateAuthSubURL に渡されるひとつ目の URL はユーザを我々のアプリケーションに戻すためのもので、ふたつ目は我々のアプリがどのサービスへのアクセス許可を求めているのかを表す Google Documents List feed のベース URL です。このリンクをクリックして、アプリケーションへ許可を与えると、ブラウザはリダイレクトされ、一度しか使えない AuthSub トークンが URL パラメータとして appspot URL の内部に埋め込まれた形で、元のページに戻ってきます。
特定の Google Data サービスへの許可リクエスト URL を作った後、求めるフィードにアクセスするために、我々のアプリに渡ってくるトークンを使用する方法が必要になります。今度は、Google Documents List API 用に渡された初期トークンを取得して、そのトークンを永続的なセッショントークンにアップグレードする必要があります。サービスに対してユーザを URL 'http://gdata-feedfetcher.appspot.com/step1' にリダイレクトさせるように伝えた事を思い出してください。さあ上記の単純なコード例を拡張して、いくつかのタスクを追加しましょう。この新しいバージョンはstep2と呼びます。
ユーザがサインインした Google Data サービス からの戻りリクエストを処理するための機能を書いていきましょう。Google Data サービスからは下記のような URL が渡ってきます:
http://gdata-feedfetcher.appspot.com/?auth_sub_scopes=http%3A%2F%2Fdocs.google.com%2Ffeeds%2F&token=CKC5y...MgH
これは我々の戻り URL に、アプリからユーザデータへのアクセスを許可してくれるサービス用の初期トークンが付加されただけのものです。下記のコードはまず、この URL からサービス名とトークンを切り出します。次に document list サービス用のトークンをアップグレードするようにリクエストします。
これを達成するのにふたつの新しいメソッドを使っています。まず、現在ページの URL を調べて一度しか使えない AuthSub token を取得します。gdata.auth.extract_auth_sub_token_from_url 関数がトークンの切り出しを行ってくれます。この初期トークンをセッショントークンにアップグレードするにはclient.upgrade_to_session_token(auth_token)を使用します。
これで我々のアプリケーションからユーザの Google Documents にアクセスするためのセッショントークンを手に入れ、これを将来再利用するためにデータストアに保存すべきかどうか決める事になります。ユーザがこのアプリにサインインしていれば、この AuthSub トークンを現在ログイン中のユーザと紐付けて保存できます。そうすればユーザは次回アプリを使用した時に許可プロセスを繰り返し実行する必要が無くなります。アプリから現在のユーザが判別できないのであれば、トークンを保存すべきではありません。
この判定を行い、Google Data client にトークンをどのように使用するか伝えるコードはこのようになります:
if session_token and users.get_current_user(): client.token_store.add_token(session_token) elif session_token: client.current_token = session_token
下記はセッショントークンへのアップグレードを行い、サインインしたユーザに対しては、将来再利用するためトークンを保存する方法を示す step2.py のコードです。
注意:Google App Engine では、外部の URL にアクセスするのに URLFetch API を使用しなければなりません。我々の Google Data Python client library では、gdata.service はデフォルトでは URLFetch API を使いません。サービスオブジェクトに URLFetch を使用するように伝えるには、サービスオブジェクトに対して次のようにgdata.alt.appengine.run_on_appengineを呼びます。gdata.alt.appengine.run_on_appengine(client)
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.api import users
import atom.url
import gdata.service
import gdata.alt.appengine
import settings
class Fetcher(webapp.RequestHandler):
def get(self):
# Write our pages title
self.response.out.write("""<html><head><title>
Google Data Feed Fetcher: read Google Data API Atom feeds</title>""")
self.response.out.write('</head><body>')
# Allow the user to sign in or sign out
next_url = atom.url.Url('http', settings.HOST_NAME, path='/step2')
if users.get_current_user():
self.response.out.write('<a href="%s">Sign Out</a><br>' % (
users.create_logout_url(str(next_url))))
else:
self.response.out.write('<a href="%s">Sign In</a><br>' % (
users.create_login_url(str(next_url))))
# Initialize a client to talk to Google Data API services.
client = gdata.service.GDataService()
gdata.alt.appengine.run_on_appengine(client)
session_token = None
# Find the AuthSub token and upgrade it to a session token.
auth_token = gdata.auth.extract_auth_sub_token_from_url(self.request.uri)
if auth_token:
# Upgrade the single-use AuthSub token to a multi-use session token.
session_token = client.upgrade_to_session_token(auth_token)
if session_token and users.get_current_user():
# If there is a current user, store the token in the datastore and
# associate it with the current user. Since we told the client to
# run_on_appengine, the add_token call will automatically store the
# session token if there is a current_user.
client.token_store.add_token(session_token)
elif session_token:
# Since there is no current user, we will put the session token
# in a property of the client. We will not store the token in the
# datastore, since we wouldn't know which user it belongs to.
# Since a new client object is created with each get call, we don't
# need to worry about the anonymous token being used by other users.
client.current_token = session_token
self.response.out.write('<div id="main"></div>')
self.response.out.write(
'<div id="sidebar"><div id="scopes"><h4>Request a token</h4><ul>')
self.response.out.write('<li><a href="%s">Google Documents</a></li>' % (
client.GenerateAuthSubURL(
next_url,
('http://docs.google.com/feeds/',), secure=False, session=True)))
self.response.out.write('</ul></div><br/><div id="tokens">')
def main():
application = webapp.WSGIApplication([('/.*', Fetcher),], debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()
GDataService.upgrade_to_session_tokenを使用して初期トークンをアップグレードした後、client.token_store.add_token(session_token)を呼ぶ事でセッショントークンを現在ログイン中のユーザと紐付けてデータストアに保存する事ができます。セッショントークンを保存しなければ、ユーザはアプリケーションを使用する度ごとに AuthSub 認可リダイレクトのプロセスを実行しなければなりません。現在ログイン中のユーザが取得できないなら、トークンが誰のものか分からないので保存する事ができません。続いて、実際にこのトークンを使用してアプリケーションからユーザのフィードを取得する方法を案内します。
これで、セッショントークンを手に入れ保存したので、アプリケーションでユーザのドキュメントリストを取得できます。最後の仕上げは、Google Docs から実際にユーザのフィードを取得して、我々のサイトに表示する事です!
我々のアプリケーションに、フィードをリクエストし、token required メッセージを処理するような新しいメソッドを追加しましょう。このメソッドはfetch_feedと呼ぶことにし、Fetcher request handler クラスに追加します。この例では、アプリケーションはフィードからデータを読むためにclient.Getを使用します。
Some Google Data feeds require authorization before they can be read. If our app had previously saved an AuthSub session token for the current user and the desired feed URL, then the token will be found automatically by the client object and used in the request. If we did not have a stored token for the combination of the current user and the desired feed, then we will attempt to fetch the feed anyway. If we receive a "token required" message from the server, then we will ask the user to authorize this app which will give our app a new AuthSub token.
Here is the code for the fetch_feed method:
def fetch_feed(self, client, feed_url):
# Attempt to fetch the feed.
if not feed_url:
self.response.out.write(
'No feed_url was specified for the app to fetch.<br/>')
example_url = atom.url.Url('http', settings.HOST_NAME, path='/step3',
params={'feed_url':
'http://docs.google.com/feeds/documents/private/full'}
).to_string()
self.response.out.write('Here\'s an example query which will show the'
' XML for the feed listing your Google Documents <a '
'href="%s">%s</a>' % (example_url, example_url))
return
try:
response = client.Get(feed_url, converter=str)
self.response.out.write(cgi.escape(response))
except gdata.service.RequestError, request_error:
# If fetching fails, then tell the user that they need to login to
# authorize this app by logging in at the following URL.
if request_error[0]['status'] == 401:
# Get the URL of the current page so that our AuthSub request will
# send the user back to here.
next = atom.url.Url('http', settings.HOST_NAME, path='/step3',
params={'feed_url': feed_url})
# If there is a current user, we can request a session token, otherwise
# we should ask for a single use token.
auth_sub_url = client.GenerateAuthSubURL(next, feed_url,
secure=False, session=True)
self.response.out.write('<a href="%s">' % (auth_sub_url))
self.response.out.write(
'Click here to authorize this application to view the feed</a>')
else:
self.response.out.write(
'Something went wrong, here is the error object: %s ' % (
str(request_error[0])))
上記のコードは cgi モジュールを使用しているので、スクリプトのはじめの方の import を書いているところに import cgi と追加してください
これで目的のフィードを取得するメソッドができましたので、Fetcher クラスの get メソッドを書き換えて、AuthSub トークンを取得して保存した後で今回のメソッドを呼び出すようにしましょう。
アプリケーションは取得するべき URL を知る必要もありますので、(訳注: get メソッドに対する)リクエストにどのフィードを取得すべきか示す URL parameter を追加します。次に掲載したgetメソッドのコードではアプリケーションがどの URL を取得するか認識し、また望んだフィードを取得する機能を追加します。
import wsgiref.handlers
import cgi
from google.appengine.ext import webapp
from google.appengine.api import users
import atom.url
import gdata.service
import gdata.alt.appengine
import settings
class Fetcher(webapp.RequestHandler):
def get(self):
# Write our pages title
self.response.out.write("""<html><head><title>
Google Data Feed Fetcher: read Google Data API Atom feeds</title>""")
self.response.out.write('</head><body>')
next_url = atom.url.Url('http', settings.HOST_NAME, path='/step3')
# Allow the user to sign in or sign out
if users.get_current_user():
self.response.out.write('<a href="%s">Sign Out</a><br>' % (
users.create_logout_url(str(next_url))))
else:
self.response.out.write('<a href="%s">Sign In</a><br>' % (
users.create_login_url(str(next_url))))
# Initialize a client to talk to Google Data API services.
client = gdata.service.GDataService()
gdata.alt.appengine.run_on_appengine(client)
feed_url = self.request.get('feed_url')
session_token = None
# Find the AuthSub token and upgrade it to a session token.
auth_token = gdata.auth.extract_auth_sub_token_from_url(self.request.uri)
if auth_token:
# Upgrade the single-use AuthSub token to a multi-use session token.
session_token = client.upgrade_to_session_token(auth_token)
if session_token and users.get_current_user():
# If there is a current user, store the token in the datastore and
# associate it with the current user. Since we told the client to
# run_on_appengine, the add_token call will automatically store the
# session token if there is a current_user.
client.token_store.add_token(session_token)
elif session_token:
# Since there is no current user, we will put the session token
# in a property of the client. We will not store the token in the
# datastore, since we wouldn't know which user it belongs to.
# Since a new client object is created with each get call, we don't
# need to worry about the anonymous token being used by other users.
client.current_token = session_token
self.response.out.write('<div id="main">')
self.fetch_feed(client, feed_url)
self.response.out.write('</div>')
self.response.out.write(
'<div id="sidebar"><div id="scopes"><h4>Request a token</h4><ul>')
self.response.out.write('<li><a href="%s">Google Documents</a></li>' % (
client.GenerateAuthSubURL(
next_url,
('http://docs.google.com/feeds/',), secure=False, session=True)))
self.response.out.write('</ul></div><br/><div id="tokens">')
上記では、self.fetch_feed(client, feed_url)と実行する事でフィードを取得しています。
最終的な実際に動くプログラムは http://gdata-feedfetcher.appspot.com/ で見る事ができます。また、完全なソースコード一式は Google Code でホストしている Google App Engine sample code project で見る事ができます。
AuthSub セッショントークンの寿命は長いですが、ユーザや我々のアプリケーションによって破棄される事があります。ある時点で、データストアに保存されたセッショントークンが破棄されたなら、我々のアプリケーションはもう使えないトークンの後始末をするべきでしょう。トークンの状態は querying the token info URL で調べる事ができます。AuthSub ドキュメント を読めば AuthSub のトークン管理について、さらに詳しい情報が手に入ります。この機能は読者の練習のためにとっておきましょう。楽しんでくださいね :)
Google Data Python client ライブラリを使えば、Google App Engine のアプリケーションで簡単にユーザの Google Data フィードを扱う事ができます。
Google Data Python client library は、殆ど全ての Google Data サービスをサポートしています。さらに詳しくは、ライブラリの getting started guide を読んだり、ソースコードを参照するために プロジェクトページ にアクセスしたり, また gdata-python-client's Google グループ に質問メールを出す事もできます。
いつもの通り、Google App Engine について質問があれば、オンラインドキュメント を読むか、グーグルグループをたずねてみてください。(訳注: 日本語のグループもあります。)
今回の例では、ユーザに成り代わりアプリケーションに許可を与えるために AuthSub を使用しましたが、Google Data APIs は他の認証メカニズムもサポートしています。アプリケーション開発の際にClientLoginの一時的な使用をしたくなるケースもあるでしょう。ClientLogin を使用するには、run_on_appengineコマンドにいくつかのパラメータを追加する必要があります。
client = gdata.service.GDataService()
# Tell the client that we are running in single user mode, and it should not
# automatically try to associate the token with the current user then store
# it in the datastore.
gdata.alt.appengine.run_on_appengine(client, store_tokens=False,
single_user_mode=True)
client.email = 'app_account_username@example.com'
client.password = 'password'
# To request a ClientLogin token you must specify the desired service using
# its service name.
client.service = 'blogger'
# Request a ClientLogin token, which will be placed in the client's
# current_token member.
client.ProgrammaticLogin()
ClientLogin トークンの要求時に CAPTCHA チャレンジを受け取る事がありえます。そうなると、トークンの取得にはこのチャレンジをうまく処理する必要があります。この事や、他の理由により、Google App Engine で ClientLogin を使用する事はお勧めしませんが、いちおう使い方は上記の通りです。