Google AppEngine Python2.7 で画像の Exif 情報を抽出する

Google AppEngine Python2.7 を使って、アップロードした画像ファイルから Exif 情報を抽出してみる。コードは https://github.com/takatama/gae-webapp2-images で公開している。

Images Python API の使用方法 – Google App Engine — Google Developers で紹介されている例は少し古かったので、webapp2を使うように変更した。

Exif を取得する部分は python – Image exif data in google app engine – Stack Overflow を参考にした。ここに書いてあるように、ローカルで AppEngine 開発用サーバを動かしたとき、取得できる Exif は限られているので注意。

 anImage.rotate(0)
 anImage.execute_transforms(parse_source_metadata=True)
 exif = anImage.get_original_metadata()
 

Google AppEngine で EXIF 情報を抽出するには、Images Python API の get_original_metadata() を使う。そのために、execute_transforms() しておく必要があるが、image に対して何も操作せずに execute_transforms() を呼び出してもエラーになるため、rotate(0) してある。

もっと Python らしいコードが書けるようになりたいな。

開発マシンは Mac OS X 10.7。AppEngine 開発用サーバで images API を使うため、PILをインストールした。備忘録として、開発マシンの設定作業を書き留めておく。

$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.7.4
BuildVersion: 11E53

(1) Google App Engine Python 2.7 のセットアップ
Google App Engine SDK を Downloads – Google App Engine — Google Developersからインストールする。ダウンロードした dmg をダブルクリックして解凍し、アプリケーションフォルダへ移動する。

Mac には最初から Python 2.7 がインストールされているので、すぐにコーディングを始められる。

$python --version
Python 2.7.1

(2) PIL (Python Imaging Library) インストール
Google AppEngine SDK の開発用サーバを使えば、ローカルで動作確認ができる。images API をローカルの開発用サーバで動かすには、PIL をインストールする必要がある。

$sudo easy_install pip
$sudo pip install pil

(3) GitHub for Mac インストール
GitHubで管理するため、Set Up Git · github:help から GitHub for Mac をインストールして使う。公開鍵、秘密鍵の設定なしで使うことができる。使い始めるまでが楽ちん。

simpleauth で Google App Engine を使った oauth 認証

Simple auth on App Engine without passwords で紹介されている simpleauth (Google code) (github) を使って、Twitter、Google、Facebook の oauth 認証を試してみた。simpleauth は Alex Vagin さんが作った Google App Engine / Python2.7用のoauth 認証デモ。GAE の新しい Web フレームワーク webapp2 を活かした設計になっている。

作者が公開しているデモはこちら。Google, Facebook, Yahoo! (OpenID), Twitter, LinkedIn, Windows Liveでの認証ができるようになっている。

下準備として、Twitter でアプリケーションを作成する。
https://dev.twitter.com/apps にアクセスして Create a new application する。
Callback URL に https://simplemash.appspot.com/auth/twitter/callback と入力しておく。URLの最後にスラッシュ (/) を入れないこと。Callback URL を入力しておかないと(コールバックするモードではなく)PINコードを入力するモードになってしまう。
後で使うので、Consumer Key と Consumer Secret を参照できるようにしておく。これらの値が外部に漏れると大変なので厳重に管理すること。

次に Git Bash を起動する。gae-simpleauth を clone した後、example を動かす準備をする。GAE に配備したとき simpleauth ライブラリの参照がうまくいかないので、ちょっと工夫が必要。

$git clone https://code.google.com/p/gae-simpleauth/
$cd gae-simpleauth
$rm example/lib/simpleauth
$cp -r simpleauth/ example/lib/
$cd example
$mv secrets.py.template secrets.py
$vi secrets.py
  SESSION_KEY = 'セッション用のランダムで推測できない文字列を入れる'
  TWITTER_CONSUMER_KEY = 'アプリケーションのConsumer Key'
  TWITTER_CONSUMER_SECRET = 'アプリケーションのConsumer Secret'
$vi app.yaml
  application を Google App Engine のアプリケーションIDに変更。
  version を変えた場合は、Google App Engine のダッシュボードで version を変更するのを忘れずに。

次に Google App Engine Launcher を起動。File>Add exsiting application で \gae-simpleauth\example を追加。
Twitter からのコールバックがあるため、テストサーバでは動かせず、Deployする必要がある。

https://<アプリケーションID>.appspot.com/ にアクセスして「Twitter」ボタンを押すと、Twitterで認証され、/profile ページが表示される。

ちなみに動かしたのは WIndows 7。Windows の vi はコピーが Ctrl + Insert, ペーストが Shift + Insert なことを学んだ。

同様の手順で、Google、Facebook についてもアプリケーションを作成して認証を行う。

Google でアプリケーションを作るには APIs Console にアクセスしてClient ID for web applicationsを取得する。設定でRedirect URIsを https://<アプリケーションID>.appspot.com/auth/google/callback にしておく。secrets.py の GOOGLE_APP_ID, GOOGLE_APP_SECRET に取得した値を入れる。これらの値は厳重に管理すること。

Facebook でアプリケーションを作るには https://developers.facebook.com/apps にアクセスしてFacebookアプリ「Developer」をインストール後、Create New App から作成する。作成の際、携帯電話かクレジットカードで本人認証される。Softbank の iPhone を持っていたので、@softbank.ne.jpのSMSアドレスを入力して本人認証した。secrets.py の FACEBOOK_APP_ID, FACEBOOK_APP_SECRET に取得した値を入れる。これらの値は厳重に管理すること。

Google App Engine の webapp2 で認証してみた

Google App Engine で Python2.7 が使えるようになった。
前のポストで紹介したとおり、チュートリアルで利用される Web アプリケーションフレームワークが webapp2 に変更された。
webapp2 では webapp2_extras を呼び出すことで、セッション管理や国際化といったオプション機能を使うことができる。
Issue 20 – webapp-improved – Sample auth app – Google App Engine’s webapp, take two – Google Project Hosting で紹介されているコードを参考に、認証機能を提供する Auth モジュールの使い方を勉強した。ソースは github で公開している

app.yaml

application: webapp2-auth
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
  script: handlers.app

handlers.py

import webapp2
from webapp2_extras import auth
from webapp2_extras import sessions
from webapp2_extras.auth import InvalidAuthIdError
from webapp2_extras.auth import InvalidPasswordError

authsessions をインポートして利用する。

def user_required(handler):
    """
         Decorator for checking if there's a user associated with the current session.
         Will also fail if there's no session present.
     """
    def check_login(self, *args, **kwargs):
        auth = self.auth
        if not auth.get_user_by_session():
            # If handler has no login_url specified invoke a 403 error
            try:
                self.redirect(self.auth_config['login_url'], abort=True)
            except (AttributeError, KeyError), e:
                self.abort(403)
        else:
            return handler(self, *args, **kwargs)
    return check_login

user_required はauth.get_user_by_session()を使って、現在のsessionに関連付けられたuserがいるかどうかをチェックするデコレーター。
userがいなければwebapp2.Route(name=’login’)で定義されたログイン画面へ遷移する。もしログイン画面が設定されていなければ、403エラーを表示する。
デコレーターについては Life is beautiful: Python入門:デコレータとは を参照のこと。認証を必要とする処理の前に@user_requiredと記述するだけで、認証を要求できる。

class BaseHandler(webapp2.RequestHandler):
    """
         BaseHandler for all requests
         Holds the auth and session properties so they are reachable for all requests
     """
    def dispatch(self):
        """
              Save the sessions for preservation across requests
          """
        try:
            response = super(BaseHandler, self).dispatch()
            self.response.write(response)
        finally:
            self.session_store.save_sessions(self.response)

    @webapp2.cached_property
    def auth(self):
        return auth.get_auth()

    @webapp2.cached_property
    def session_store(self):
        return sessions.get_store(request=self.request)

    @webapp2.cached_property
    def auth_config(self):
        """
              Dict to hold urls for login/logout
          """
        return {
            'login_url': self.uri_for('login'),
            'logout_url': self.uri_for('logout')
        }

BaseHandlerは全てレスポンスをsessionに保持する。それ以外にも、auth と session へ簡便にアクセスするためのメソッドを定義する。

class LoginHandler(BaseHandler):
    def get(self):
        """
              Returns a simple HTML form for login
          """
        return """
            <!DOCTYPE hml>
            <html>
                <head>
                    <title>webapp2 auth example</title>
                </head>
                <body>
                <form action="%s" method="post">
                    <fieldset>
                        <legend>Login form</legend>
                        <label>Username <input type="text" name="username" placeholder="Your username" /></label>
                        <label>Password <input type="password" name="password" placeholder="Your password" /></label>
                        </fieldset>
                        <button>Login</button>
                 </form>
            </html>
        """ % self.request.url

    def post(self):
        """
              username: Get the username from POST dict
              password: Get the password from POST dict
          """
        username = self.request.POST.get('username')
        password = self.request.POST.get('password')
        # Try to login user with password
        # Raises InvalidAuthIdError if user is not found
        # Raises InvalidPasswordError if provided password doesn't match with specified user
        try:
            self.auth.get_user_by_password(username, password)
            self.redirect('/secure/')
        except (InvalidAuthIdError, InvalidPasswordError), e:
            # Returns error message to self.response.write in the BaseHandler.dispatcher
            # return e
            return """
                Could not login. <a href="/login/">retry</a>
            """

/login/にアクセスしたとき、ログイン画面を表示する。
ログイン画面で入力されたユーザー名とパスワードを auth.get_user_by_password() を使って認証し、エラーがでたらログインできなかったことを表示する。

class CreateUserHandler(BaseHandler):
    def get(self):
        """
              Returns a simple HTML form for create a new user
          """
        return """
            <!DOCTYPE hml>
            <html>
                <head>
                    <title>webapp2 auth example</title>
                </head>
                <body>
                <form action="%s" method="post">
                    <fieldset>
                        <legend>Create user form</legend>
                        <label>Username <input type="text" name="username" placeholder="Your username" /></label>
                        <label>Password <input type="password" name="password" placeholder="Your password" /></label>
                    </fieldset>
                    <button>Create user</button>
                </form>
            </html>
        """ % self.request.url

    def post(self):
        """
              username: Get the username from POST dict
              password: Get the password from POST dict
          """
        username = self.request.POST.get('username')
        password = self.request.POST.get('password')
        # Passing password_raw=password so password will be hashed
        # Returns a tuple, where first value is BOOL. If True ok, If False no new user is created
        user = self.auth.store.user_model.create_user(username, password_raw=password)
        if not user[0]: #user is a tuple
            return user[1] # Error message
        else:
            # User is created, let's try redirecting to login page
            try:
                self.redirect(self.auth_config['login_url'], abort=True)
            except (AttributeError, KeyError), e:
                self.abort(403)

ユーザー追加画面を表示する。
ユーザー追加画面で auth.store.user_model.create_user() を呼び出し、ユーザーを生成できれば、ログイン画面へ遷移する。生成できなければ403エラーを表示する。

class LogoutHandler(BaseHandler):
    """
         Destroy user session and redirect to login
     """

    def get(self):
        self.auth.unset_session()
        # User is logged out, let's try redirecting to login page
        try:
            self.redirect(self.auth_config['login_url'])
        except (AttributeError, KeyError), e:
            return "User is logged out"

/logout/にアクセスしたとき、auth.unset_session() を呼び出し、現在のsessionとユーザーの関連付けを外す。

class SecureRequestHandler(BaseHandler):
    """
         Only accessible to users that are logged in
     """

    @user_required
    def get(self, **kwargs):
        user = self.auth.get_user_by_session()
        try:
            return "Secure zone for %s Logout" % (str(user), self.auth_config['logout_url'])
        except (AttributeError, KeyError), e:
            return "Secure zone"

/secure/にアクセスしたとき、デコレーター user_required() でログイン済みかチェックした後、ログインしているユーザーの情報を表示する。

webapp2_config = {}
webapp2_config['webapp2_extras.sessions'] = {
		'secret_key': 'Set_this_to_something_random_and_unguessable',
	}

app = webapp2.WSGIApplication([
		webapp2.Route(r'/login/', handler=LoginHandler, name='login'),
		webapp2.Route(r'/logout/', handler=LogoutHandler, name='logout'),
		webapp2.Route(r'/secure/', handler=SecureRequestHandler, name='secure'),
		webapp2.Route(r'/create/', handler=CreateUserHandler, name='create-user')
	], debug=True, config=webapp2_config)

sessions の secret_key は推測されないものを指定しなければならない。
/login/ にアクセスしたとき、ログイン画面を表示する。
/logout/ にアクセスしたとき、ユーザーをログアウトさせ、ログイン画面に遷移する。
/secure/ にアクセスしたとき、認証を要求する。
/create/ にアクセスしたとき、ユーザー追加画面を表示する。

Google App Engine Launcher を起動して、localhost:8080/create/ にアクセスしてユーザーを追加。ログイン画面に遷移するのでログイン。その後 localhost:8080/secure/ にアクセスすると、ユーザー情報が表示される。
http://localhost:8083/_ah/admin/ にアクセスして Datastore Viewer を使うと、User が保存されていることを確認できる。

Google App Engine で Python 2.7 を使った Hello World

Google App Engine で Python 2.7 が使えるようになっていた。
違いは What’s New in Python 2.7 – Google App Engine — Google Developers にまとまっているんだけれど、手を動かさないと理解できない。
そこで Introduction – Google App Engine — Google Developers に従って Hello World し直してみた。使っている PC は Windows7 64bit。

手順:

  1. Python 2.7 Release から Windows x86 MSI Installer をダウンロードしてインストール。
  2. Downloads – Google App Engine — Google Developers から GoogleAppEngine-1.6.5.msi をダウンロードしてインストール。「Python2.5が見つかった」と表示されるけど気にしない。
  3. Google App Engine Launcherを起動して、Python2.7を利用するように修正(Edit > Preferences > Python Path)。

あとはチュートリアルに従ってソースコードを書いて、Google App Engine Launcher にプロジェクトとして追加し、Runすればよい。

古い helloworld.py (Python2.5) – Using the webapp Framework – Google App Engine — Google Developers

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

class MainPage(webapp.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('Hello, webapp World!')

application = webapp.WSGIApplication(
                                     [('/', MainPage)],
                                     debug=True)

def main():
    run_wsgi_app(application)

if __name__ == "__main__":
    main()

新しい helloworld.py (Python2.7)

import webapp2
class MainPage(webapp2.RequestHandler):
  def get(self):
      self.response.headers['Content-Type'] = 'text/plain'
      self.response.out.write('Hello, webapp World!')

app = webapp2.WSGIApplication([('/', MainPage)],
                              debug=True)

ということで、webapp ではなく webapp2 が使われるにように。main() の記述もやめてスッキリ。

古い app.yaml (Python2.5) – Hello, World! – Google App Engine — Google Developers

application: helloworld
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: helloworld.py

新しい app.yaml (Python2.7)

application: helloworld
version: 1
runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /.*
  script: helloworld.app

違いは3点。runtimeでPython2.7を使うことを宣言する。threadsafeなWebアプリにすれば、複数のリクエストを並列実行して効率的にさばける。
main()を呼び出さないようにしたので、helloworld.pyで定義したhelloworldモジュールのappオブジェクトへ処理させることをここで明示するようになった。