Google App Engine用フレームワークKayでパフォーマンス測定ツール「Appstats」を使用する

Google App Engineに用意されているパフォーマンス測定ツール「Appstats」をKayで使用する方法を紹介します。

app.yamlとsettings.pyを編集し、「Appstats」を有効にします。

app.yaml

handlers:
…
# 追加。普段のハンドラの上に書くこと
- url: /stats.*
  script: $PYTHON_LIB/google/appengine/ext/appstats/ui.py

- url: /.*
  script: kay/main.py

settings.py

MIDDLEWARE_CLASSES = (
  'google.appengine.ext.appstats.recording.AppStatsDjangoMiddleware', #追加
  'kay.auth.middleware.AuthenticationMiddleware', #追加
  …
)

以上の設定で使用できるようになりました。簡単でしたね。

開発環境で動作を確認するには「http://localhost:8080/stats」にアクセスします。

もう少し、使いやすくしましょう。

Administration Console Custom Pagesの機能を使い、Adminコンソールに追加します。

app.yaml

# 追加
admin_console:
  pages:
  - url: /stats
    name: "Stats"

参考にしたページ

Google App Engine Version 1.3.6の新機能

Google App Engine Version 1.3.6の新機能を開発環境で試しました。

サンプルコードはKay-Frameworkを使用しています。

(1)Results of datastore count() queries and offsets for all datastore queries are no longer capped at 1000.

データストアのクエリーとカウントの1000件の制限を解除されました。

データストアのクエリーとカウントの取得件数が1000件までに制限されていました。
この1000件の制限が解除されました。
CPUクオータとリクエスト時間の許す範囲でデータを取得できます。

QueryとGqlQueryの基底クラス(_BaseQuery)で、取得できる件数の初期値が1000件に設定されています。

class _BaseQuery(object):
  def count(self, limit=1000, **kwargs):

このため引数で制限を指定しない場合は、取得できる件数は従来と同じで1000件までです。

count = models.Comment.all().count() #=>最大1000

1000件以上取得したい場合は、引数で指定します。

count = models.Comment.all().count(99999) #=>最大99999

(2) Users can now serve custom static error pages for over_quota, dos_api_denial and default cases.

エラーページの設定が可能になりました。
参照:Custom Error Responses

app.yaml

error_handlers:
  # 標準のエラーページ
  - file: default_error.html

エラーごとに表示するエラーページを設定することもできます。

app.yaml

error_handlers:
  # 標準のエラーページ
  - file: default_error.html

  # リソースの割り当てを超えたとき
  - error_code: over_quota
    file: over_quota.html

  # DoS Protection
  - error_code: dos_api_denial:
    file: dos_api_denial.html

  # タイムアウトのとき
  - error_code: timeout:
    file: timeout.html

(3)Automatic image thumbnailing is now available in the Images API using get_serving_url().

get_serving_url()で画像のサイズ変更や切り抜きができるようになりました。
参照:get_serving_url(blob_key, size=None, crop=False)

Google App Engine 1.3.6 – Ian Lewis」によると、別のインフラを使うためにGoogleAppEngineのクオータの制限がかからないそうです。

以下のコードは、get_serving_url(blob_key, size=None, crop=False)を使用したサンプルコードです。
アップロードされ画像に対してサイズ変更したり、切り抜きをした画像のURLを作成します。

myapp/urls.py

view_groups = [
  ViewGroup(
    Rule('/', endpoint='index', view='myapp.views.index'),
    Rule('/upload', endpoint='upload', view=('myapp.views.UploadHandler', (), {})),
  )
]

myapp/views.py

from werkzeug import Response
from kay.utils import render_to_response, url_for
from kay.handlers import blobstore_handlers
from google.appengine.ext import blobstore
from google.appengine.api import images

def index(request):
    upload_url = blobstore.create_upload_url(url_for('myapp/upload'))
    blob_key = request.values.get('blob_key')
    d = {'upload_url': upload_url}
    if blob_key:
        from google.appengine.api.images import get_serving_url
        d['blob_key'] = blob_key
        # サイズ変更
        d['resize_url'] = get_serving_url(blob_key, 64)
        # 切り抜き
        d['crop_url'] = get_serving_url(blob_key, 160, True)

    return render_to_response("myapp/index.html", d)

class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
    def post(self):
        # 'file' is file upload field in the form                                                                                                              
        upload_files = self.get_uploads('file')
        blob_info = upload_files[0]
        headers = {'Location': url_for('myapp/index', blob_key=blob_info.key())}
        return Response(None, headers=headers, status=302)

myapp/templates/index.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Top Page - myapp</title>
</head>
<body>

{% if blob_key %}
Upload succeeded.<br/>
<a href="{{ resize_url }}">View resizing</a><br />
<a href="{{ crop_url }}">View cropping</a><br />
{% endif %}

<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
Upload File: <input type="file" name="file"><br>
<input type="submit" name="submit" value="Submit">
</form>

</body>
</html>

(4)Multitenancy is now supported in the datastore, allowing better compartmentalization of user data.

データストアの名前空間をサポートしました。
参照:Multitenancy and the Namespaces Python API – Google App Engine – Google Code

from google.appengine.api import namespace_manager
namespace = namespace_manager.get_namespace()

namespace_manager.set_namespace('A')  # 名前空間を「A」に設定
do_something() #名前空間「A」でデータストアを操作

namespace_manager.set_namespace('B')  # 名前空間を「B」に設定
do_something() #名前空間「B」でデータストアを操作

namespace_manager.set_namespace(namespace)  # 名前空間を元に戻す

以下のコードは、名前空間を使用したサンプルコードです。
名前空間(‘-global-‘, ‘foo’, ‘bar’)のいずれかのカウンターを1増加し、増加した名前空間とカウンターの値を表示します。

myapp/views.py

from werkzeug import Response
from kay.utils import render_to_response, url_for
from google.appengine.ext import db
from google.appengine.api import namespace_manager
import random

class Counter(db.Model):
    """カウンター"""
    count = db.IntegerProperty()

def update_counter(name):
    """nameのカウンターを1増加する"""
    def _update_counter(name):
        counter = Counter.get_by_key_name(name)
        if counter is None:
            counter = Counter(key_name=name);
            counter.count = 1
        else:
            counter.count = counter.count + 1
        counter.put()
        return counter.count
    return db.run_in_transaction(_update_counter, name)

def index(request):
    """
    名前空間('-global-', 'foo', 'bar')のいずれかのカウンターを1増加し、
    増加した名前空間とカウンターの値を表示する
    """
    # 名前空間のリスト
    ns_list = ('-global-', 'foo', 'bar')
    # 名前空間のリストからランダムに取得
    d = {'ns': random.choice(ns_list)}
    # 現在の名前空間
    namespace = namespace_manager.get_namespace()
    try:
        # 編集するデータストアの名前空間を設定
        namespace_manager.set_namespace(d['ns'])
        # カウンターを増加して結果を取得
        d['count'] = update_counter('SomeRequest')
    finally:
        # 名前空間を元に戻す
        namespace_manager.set_namespace(namespace)
    return render_to_response('myapp/index.html', d)

myapp/templates/index.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Top Page - myapp</title>
</head>
<body>
<p>namespace = {{ns}}</p>
<p>count = {{count}}</p>
</body>
</html>

(5)Added a pause queue button to the task queue details page in the Admin Console.

管理画面の「Task Queues」に「Pause Queue」ボタンが追加されました。

(6)Historical graphs have been added to all of the dashboard graphs in the Admin Console.


このボタンが追加された?

(7)New method to allocate datastore ids in a given range: db.allocate_id_range().

与えられた範囲でデータストアのidを割り当てる新しいメソッド db.allocate_id_range() が追加されました。
参照:db.allocate_id_range()

(8)New db method is_in_transaction() determines if a transaction is still open.

トランザクション中かどうかを取得する新しいメソッド db.is_in_transaction() が追加されました。
参照:db.is_in_transaction()

(9)Increased several rate limited quotas for free applications.

無料枠が増えました。

(10) Remote API now supports the Blobstore API.

リモートAPIが新しくBlobstore APIをサポートしました。

(11) Content-range headers are supported on Blobstore downloads.

英語には自信がありません。(汗
間違いがあれば教えてください。

Kayのログインフォームをカスタマイズする(2) 文言の日本語化する

前回の記事「Kayのログインフォームをカスタマイズする(1) 継承元のテンプレートファイルを変更する « 山本隆の開発日誌」の続き。

Google App Engine用フレームワークKayのデータストアを利用した認証を使用したときのログインフォームをカスタマイズする方法を紹介します。

文言の日本語化する

初期値の状態では、ログインフォームの文言は「user name」や「password」のように英語になっています。

ログインフォームの文言を日本語化するには、Kayのメッセージ国際化の機能を使用します。
※参考:12. メッセージ国際化 — Kay v0.10.0 documentation
※参考:Google App Engine用フレームワーク Kay の国際化の機能を有効にする « 山本隆の開発日誌

settings.py

USE_I18N = True

これだけでログインフォームの文言が日本語化されます。

メッセージを変更したい場合は、メッセージ国際化の機能を使って変更します。

たとえばフォームのタイトル「Kay ログインフォーム」を変更したい場合は次のようにします。

  1. カタログファイルの雛型を作成します。
    python manage.py extract_messages myapp
    

    カタログファイルが myapp/i18n/messages.pot に作成されます。

  2. カタログファイルを編集します。

    myapp/i18n/messages.pot に次の行を追加します。

    msgid "Kay Login Form"
    msgstr ""
    

    変更したい文言のmsgidは myproject/kay/auth/templates/loginform.html を参照して確認します。

  3. 雛型から日本語の翻訳を追加します。

    python manage.py add_translations myapp -l ja
    

    日本語の翻訳ファイルが myapp/i18n/ja/LC_MESSAGES/messages.po に作成されます。

  4. 日本語の翻訳ファイルを編集します。

    msgid "Kay Login Form"
    msgstr "MyApp ログインフォーム"
    

    ファイルの文字コードはUTF-8で保存します。

  5. 翻訳ファイルをコンパイルします。

    python manage.py compile_translations myapp
    

    コンパイルされたファイルが myapp/i18n/ja/LC_MESSAGES/messages.mo に作成されます。

ログインフォームにアクセスすると、文言が変わっていること確認できます。

Kayのログインフォームをカスタマイズする(1) 継承元のテンプレートファイルを変更する

Google App Engine用フレームワークKayのデータストアを利用した認証を使用したときのログインフォームをカスタマイズする方法を紹介します。

認証機能を有効にする

Kayでデータストアを利用した認証を使うときは、settings.pyに次のコードを追加します。
※参考:9. 認証の設定 — Kay v0.10.0 documentation

INSTALLED_APPS = (
  'kay.auth',
)
MIDDLEWARE_CLASSES = (
  'kay.sessions.middleware.SessionMiddleware',
  'kay.auth.middleware.AuthenticationMiddleware',
)
AUTH_USER_BACKEND = 'kay.auth.backends.datastore.DatastoreBackend'
AUTH_USER_MODEL = 'kay.auth.models.DatastoreUser'

継承元のテンプレートファイルを変更する

ログインフォームの標準のテンプレートファイルは次の場所にあります。

myproject/kay/auth/templates/loginform.html

このファイルを見ると、先頭に次のコードがあります。

{% extends 'base.html' %}

このことからログインフォームのテンプレートは、base.htmlを継承していることがわかります。

このbase.htmlは、次の場所にあります。

myproject/kay/templates/base.html

このファイルを編集すると、ログインフォームのデザインが変わります。

ただ、フレームワーク内のファイルを変更してしまうと、今後バージョンアップするときなどに、修正したファイルを更新しないように気をつける必要があります。

そこで、settings.TEMPLATE_DIRSを編集して、フレームワーク内のbase.htmlではなく、アプリケーションで用意したbase.htmlを参照するようにします。

settings.py

TEMPLATE_DIRS = (
    'myapp/templates',
)

こうすることで、myapp/templates/base.htmlを参照するようになります。

myproject/kay/templates/base.html を myapp/templates/base.html にコピーして、myapp/templates/base.html を編集します。

ログインフォームにアクセスすると、myapp/templates/base.html のテンプレートが適用されていることを確認できます。

次の記事「Kayのログインフォームをカスタマイズする(2) 文言の日本語化する « 山本隆の開発日誌」へ続く。

Google App Engine用フレームワークKayで@login_required の付いた関数のテスト方法

Google App Engine用フレームワークKayで@login_required の付いた関数をテストする方法。

Kayにはテストでログイン状態の設定を簡単に行うことが出来る機能が用意されています。

「Client.test_login(username=ユーザー名)」で指定したユーザーでログインした状態になります。
「Client.test_logout()」でログアウトした状態になります。

まず、一般的なテストは次のようになります。
(17.4. テスト用 Client を使用する — Kay v0.10.0 documentationより)

from werkzeug import BaseResponse, Client
from kay.app import get_application
from kay.utils.test import (
  init_recording, get_last_context, get_last_template, disable_recording
)
from kay.ext.testutils.gae_test_base import GAETestBase

class MyappTestCase(GAETestBase):
    CLEANUP_USED_KIND = True

    def setUp(self):
        init_recording()
        app = get_application()
        self.client = Client(app, BaseResponse)

    def tearDown(self):
        disable_recording()

テストを一つ追加します。

  def test_get(self):
        #ログインしていないときは、そのまま表示
        #ログインしているときは、リダイレクトして別のページを表示

ログインしていないときのテスト

response = self.client.get(url_for('myapp/user_index'))
self.assertEqual(response.status_code, 200)

ユーザー名「test」のユーザーでログインしている状態でのテスト(testというユーザー名のユーザーがあるものとします。)

self.client.test_login(username='test')
self.assertEqual(response.status_code, 302) #Redirect

テスト全体のコードは次のようになります。

  def test_get(self):
        #ログインしていないときは、そのまま表示
        response = self.client.get(url_for('myapp/user_index'))
        self.assertEqual(response.status_code, 200)

        #ログインしているときは、リダイレクトして別のページを表示
        self.client.test_login(username='test')
        self.assertEqual(response.status_code, 302) #Redirect

このように、ログイン状態に応じた簡単にテストができます。

Google App Engine用フレームワークKayのflash機能を使う

Google App Engine用フレームワークKayのソースコードを眺めていたら、flash機能を発見しました。

flash機能は値を一度だけ取り出すことが出来るセッションのようなもの。
普通のセッション変数は削除するまで値は保持されますが、flashは一度値を取り出すとflashから削除されて、次に取り出すときには値がなくなっています。

一度だけ表示するメッセージなどを格納するとき、メッセージを削除する手間が省けて便利です。

flash機能はミドルウェアとして提供されています。

settings.pyのMIDDLEWARE_CLASSESに登録します。

settings.py

MIDDLEWARE_CLASSES = (
    'kay.utils.flash.FlashMiddleware',
)

flashを使うには、kay.utils.flashをインポートします。

from kay.utils import flash

flashに値を設定するには、flash.set_flash(値)とします。

flash.set_flash('Hello World')

flashから値を取り出すには、flash.get_flash()とします。

message = flash.get_flash()

flashの使用例です。

myapp/views.py

from kay.utils import flash

def index(request):
    message = flash.get_flash()
    return render_to_response('myapp/index.html',
                              {'message': message})

def index2(request):
    flash.set_flash('Hello World')
    return redirect(url_for('myapp/index'))

index2にアクセスするとindexにリダイレクトされ、flashに格納された’Hello World’の文字が表示されます。
その後リロードすると、flashから値が取り除かれているため、’Hello World’は表示されません。

KayのRESTfull APIでデータを追加する

Google App Engine用フレームワークKayを使ったWebアプリケーションに、RESTfull APIを使ってデータを追加する方法を紹介します。

最初に、Kayを使ったWebアプリケーションを作成して、RESTfull APIを使用できるように設定します。

今回の例では、myapp.models.MyModelをRESTfull APIで操作できるようにします。

myapp/models.py

from google.appengine.ext import db

class MyModel(db.Model):
    user_name = db.StringProperty()
    message = db.TextProperty()

myapp/urls.py

from kay.generics.rest import RESTViewGroup

class MyRESTViewGroup(RESTViewGroup):
    models = ['myapp.models.MyModel']

view_groups = [
    MyRESTViewGroup(),
    ViewGroup(
        Rule('/', endpoint='index', view='myapp.views.index'),
    )
]

開発サーバーを起動します。

python manage.py runserver

このWebアプリケーションからRESTfull APIでMyModelの情報を取得するのは簡単です。

MyModelの一覧を取得するコード

# -*- coding: utf-8 -*-
import urllib2
res = urllib2.urlopen('http://localhost:8080/rest/MyModel')
print res.code #=> 200
print res.msg #=> OK
print res.read() #=> MyModelの一覧データ

MyModelの特定のデータを取得するコード

# -*- coding: utf-8 -*-
import urllib2
key = … #モデルのキー
res = urllib2.urlopen('http://localhost:8080/rest/MyModel/%s' % key)
print res.code #=> 200
print res.msg #=> OK
print res.read() #=> モデルのデータ

WebアプリケーションのMyModelに新しいデータを登録します。

データを登録するときのURLは一覧取得の時と同じですが、GETではなくPOSTで送信します。

# -*- coding: utf-8 -*-
import urllib2
req = urllib2.Request('http://localhost:8080/rest/MyModel')

データを登録するには、登録するデータをXML形式で送信します。

xml = '<?xml version="1.0" encoding="UTF-8"?><MyModel><user_name>test</user_name><message>テスト</message></MyModel>'
req.add_data(xml)

リクエストのヘッダにContent-Typeを設定する必要があります。

req.add_header('Content-Type', 'application/xml')

準備が出来たらリクエストを送信します。
登録に成功したら、登録したモデルのキーが返ります。

res = urllib2.urlopen(req)
print res.code #=> 200
print res.msg #=> OK
print res.read() #=> 登録したモデルのキー

登録処理の全体のソースコードは次のようになります。

# -*- coding: utf-8 -*-
import urllib2

req = urllib2.Request('http://localhost:8080/rest/MyModel')

#Content-Typeの設定が必要
req.add_header('Content-Type', 'application/xml')

#登録するデータをXML形式で設定する
xml = '<?xml version="1.0" encoding="UTF-8"?><MyModel><user_name>test</user_name><message>テスト</message></MyModel>'
req.add_data(xml)

res = urllib2.urlopen(req)

print res.code #=> 200
print res.msg #=> OK
print res.read() #=> 登録したモデルのキー

関連ページ

Google App Engine(Python)で動的にCSVファイルを作成してダウンロードさせる

Google App Engine(Python)で動的にCSVファイルを作成してダウンロードさせる方法。

今回の例ではGoogle App Engine用フレームワークKayを使用しています。

CSVファイルを作成するにはcsvモジュールのWriterオブジェクトを使用します。

import csv
…
writer = csv.writer(data)

Writerオブジェクトのコンストラクタの最初の引数は、ファイルライクなオブジェクトをとります。
ここではStringIOを使用します。

import StringIO
data = StringIO.StringIO()

csvファイルのデータは、Writerオブジェクトのwriterow()で書き込みます。
引数には、行データ(バイト列)の配列をとります。
ユニコード文字列はencode()でバイト列にエンコードします。

writer.writerow([u'5560005'.encode('utf-8'), u'大阪府大阪市浪速区日本橋'.encode('utf-8')])
writer.writerow([u'5560006'.encode('utf-8'), u'大阪府大阪市浪速区日本橋東'.encode('utf-8')])
writer.writerow([u'5560004'.encode('utf-8'), u'大阪府大阪市浪速区日本橋西'.encode('utf-8')])

ブラウザでダウンロードさせるには、ヘッダの’Content-Type’に’application/octet-stream’を指定します。

header = Headers()
header.add('Content-Type', 'application/octet-stream');

ヘッダの’Content-Disposition’で、ダウンロードさせるCSVファイルのファイル名をで指定します。

header.add('Content-Disposition', 'attachment', filename='sample.csv')

全体のソースコードは以下のようになります。

views.py

def index(request):
  import csv
  import StringIO
  from werkzeug.datastructures import Headers
  from werkzeug import Response

  data = StringIO.StringIO()
  writer = csv.writer(data)
  writer.writerow([u'5560005'.encode('utf-8'), u'大阪府大阪市浪速区日本橋'.encode('utf-8')])
  writer.writerow([u'5560006'.encode('utf-8'), u'大阪府大阪市浪速区日本橋東'.encode('utf-8')])
  writer.writerow([u'5560004'.encode('utf-8'), u'大阪府大阪市浪速区日本橋西'.encode('utf-8')])

  header = Headers()
  header.add('Content-Type', 'application/octet-stream');
  header.add('Content-Disposition', 'attachment', filename='sample.csv')
  return Response(data.getvalue(), headers=header)

###関連ページ

Google App Engine(Python)用フレームワークKayを使い、動的にExcelファイルを作成してZIP形式で圧縮しダウンロードさせる

Google App Engine(Python)用フレームワークKayを使い、動的にExcelファイルを作成してZIP形式で圧縮しダウンロードさせる方法。

Google App Engine(Python)用フレームワークKayを使い、動的にZIP形式で圧縮してダウンロードさせる」と「Google App Engine用フレームワークKayでExcelファイルをダウンロードさせる」の合わせ技になります。

Excelファイルの作成には、xlwtを使用します。
xlwtはpure pythonなので、Google App Engineで使用することが出来ます。

xlwtからソースをダウンロードして展開します。
xlwtフォルダーをKayのプロジェクトのルートフォルダーにコピーします。
xlwtフォルダー中のdocフォルダーやexamplesフォルダーは不要です

次のようなフォルダー構成になります。

myproject/
 ├ kay/
 ├ myapp/
 └ xlwt/

xlwtでExcelデータを作成します。

wb = xlwt.Workbook()
ws1 = wb.add_sheet('Sheet1')
ws1.write(0, 0, u'セル:A1')
ws1.write(1, 0, u'セル:A2')
ws1.write(0, 1, u'セル:B1')
ws1.write(1, 1, u'セル:B2')
data = StringIO.StringIO()
wb.save(data)

xlwt.save()は引数にファイルライクなオブジェクトをとります。
引数に指定したオブジェクトにExcelデータが書き込まれます。

ZipFileオブジェクトを作成します。

ZipFileオブジェクトのコンストラクタの1番目の引数にファイルライクなオブジェクトを指定します。
引数に指定したファイルライクなオブジェクトに、ZIPデータが書き込まれます。

zipdata = StringIO.StringIO()
zipobj =  zipfile.ZipFile(zipdata, 'w', zipfile.ZIP_DEFLATED)

ZipFileオブジェクトにファイルを登録します。
1番目の引数にファイル名を、2番目の引数にファイルのデータ(バイト列)を指定します。

zipobj.writestr('example.xls', data.getvalue())

ここで、1番目の引数にZipInfoオブジェクトを指定すると、ファイルの情報を細かく設定することが出来ます。

最後に閉じます。

zipobj.close()

以上で、ZIPデータを作成できました。

作成したZIPデータをダウンロードさせるために、HTTPヘッダの設定を行います。

header = Headers()
header.add('Content-Type', 'application/octet-stream');
header.add('Content-Disposition', 'attachment', filename='foo.zip')

最後に、ZIPデータをレスポンスとして返します。

return Response(zipdata.getvalue(), headers=header)

全体のソースコードは以下のようになります。

def index(request):
    import xlwt
    import StringIO
    import zipfile
    from werkzeug.datastructures import Headers
    from werkzeug import Response

    wb = xlwt.Workbook()
    ws1 = wb.add_sheet('Sheet1')
    ws1.write(0, 0, u'セル:A1')
    ws1.write(1, 0, u'セル:A2')
    ws1.write(0, 1, u'セル:B1')
    ws1.write(1, 1, u'セル:B2')
    data = StringIO.StringIO()
    wb.save(data)

    zipdata = StringIO.StringIO()
    zipobj =  zipfile.ZipFile(zipdata, 'w', zipfile.ZIP_DEFLATED)
    zipobj.writestr('example.xls', data.getvalue())
    zipobj.close()

    header = Headers()
    header.add('Content-Type', 'application/octet-stream');
    header.add('Content-Disposition', 'attachment', filename='foo.zip')
    return Response(zipdata.getvalue(), headers=header)

関連ページ

Google App Engine用フレームワークKayのRESTfull APIの機能(RESTViewGroup)でアクセス制限を行う方法 その2

Google App Engine用フレームワークKayのRESTfull APIの機能(RESTViewGroup)でアクセス制限を行う方法の続き。

前回までのまとめ。

  • Google App Engine用フレームワークKayにはRESTViewGroupというRESTfull APIを簡単に実装するための機能が用意されています。
  • RESTViewGroupでアクセス制限を行うにはauthorizeメソッドをオーバーライドします。
  • authorizeメソッドで例外kay.exceptions.NotAuthorizedをraiseすることでアクセスを拒否できます。
  • authorizeメソッドの引数operationをチェックすることで、処理内容を確認できます。
    • operationがとる値は、OP_LIST, OP_SHOW, OP_CREATE, OP_UPDATE, OP_DELETEのいずれか。

authorizeメソッドの定義

from kay.generics import (OP_LIST, OP_SHOW, OP_CREATE, OP_UPDATE, OP_DELETE)
from kay.exceptions import NotAuthorized

class MyRESTViewGroup(RESTViewGroup):
    models = ['myapp.models.MyModel']
    def authorize(self, request, operation, obj=None, model_name=None,
                  prop_name=None):
        '''アクセス制限
           @param request: リクエスト
           @param operation: 処理(OP_LIST, OP_SHOW, OP_CREATE, OP_UPDATE, OP_DELETE)
           @param obj: 処理対象のオブジェクト(OP_SHOW, OP_UPDATE, OP_DELETEのときのみ)
           @param model_name: モデル名
           @param prop_name:
           @raise e: アクセスを許可しないときは kay.exceptions.NotAuthorized を投げる
        '''
        return True

今回は、URLに付与したユーザー名とパスワードで認証する方法を紹介します。
その後、Basic認証による認証方法を紹介します。

次のようなURLによるアクセスで認証します。

http://~/rest/MyMode?user_name=ユーザー名&password=パスワード

URLに付与されたパターメータはrequest.value.get(パラメータ名)で取得することが出来ます。

user_name = request.values.get('user_name')
password=request.values.get('password'))

指定されたユーザー名とパスワードでログインできるかどうかを調べるには、kay.auth.login()を使用します。

from kay.auth import login
result = login(request, username=ユーザー名, password=パスワード)
# ログインに失敗したときはアクセスを認めない
if not result: raise NotAuthorized

ログインに成功した場合、request.userのis_adminやis_anonymous()で、権限を検証することが出来ます。

# 管理者権限がないときはアクセスを認めない
if not request.user.is_admin:
    raise NotAuthorized

全体のソースコードは次のようになります。

myapp/urls.py

from kay.routing import (ViewGroup, Rule)
from kay.generics.rest import RESTViewGroup

class MyRESTViewGroup(RESTViewGroup):
    models = ['myapp.models.MyModel']
    def authorize(self, request, operation, obj=None, model_name=None,
                  prop_name=None):
        '''アクセス制限
           @param request: リクエスト
           @param operation: 処理(OP_LIST, OP_SHOW, OP_CREATE, OP_UPDATE, OP_DELETE)
           @param obj: 処理対象のオブジェクト(OP_SHOW, OP_UPDATE, OP_DELETEのときのみ)
           @param model_name: モデル名
           @param prop_name:
           @raise e: アクセスを許可しないときは kay.exceptions.NotAuthorized を投げる
        '''
        from kay.auth import login
        from kay.exceptions import NotAuthorized
        # ログインできるか
        result = login(request,
                       user_name=request.values.get('user_name'),
                       password=request.values.get('password'))
        # ログインに失敗したときはアクセスを認めない
        if not result: raise NotAuthorized
        # 管理者権限がないときはアクセスを認めない
        if not request.user.is_admin: raise NotAuthorized

        return True

view_groups = [
    MyRESTViewGroup(),
    ViewGroup(
        Rule('/', endpoint='index', view='myapp.views.index'),
    )
]

開発サーバーでアクセスします。

http://localhost:8080/rest/MyModel?user_name=ユーザー名&password=パスワード

管理者権限を持つユーザーのユーザー名とパスワードでアクセスすれば表示されます。

次にユーザー名とパスワードをURLに付与するのではなく、Basic認証で送信するように変更します。

Google App EngineによるBasic認証は「Google App Engine でBasic認証を実装 改 – すぎゃーんメモ」を参考にさせていただきました。

ヘッダーの情報はrequest.headers.get(ヘッダ名)で取得することが出来ます。

auth_header = request.headers.get('Authorization')
if not auth_header: raise NotAuthorized

取得した情報からユーザー名とパスワードを取得します。

(scheme, base64str) = auth_header.split(' ')
if scheme != 'Basic': raise NotAuthorized
(username, password) = base64.b64decode(base64str).split(':')

後は先ほどと同じ。

result = login(request, user_name=username, password=password)
# ログインに失敗したときはアクセスを認めない
if not result: raise NotAuthorized
# 管理者権限がないときはアクセスを認めない
if not request.user.is_admin: raise NotAuthorized

全体のソースコードは次のようになります。

class MyRESTViewGroup(RESTViewGroup):
    models = ['myapp.models.MyModel']
    def authorize(self, request, operation, obj=None, model_name=None,
                  prop_name=None):
        from kay.auth import login
        from kay.exceptions import NotAuthorized
        import base64

        # ヘッダーから認証情報を取得する
        auth_header = request.headers.get('Authorization')
        if not auth_header: raise NotAuthorized

        # 取得した情報からユーザー名とパスワードを取得する
        (scheme, base64str) = auth_header.split(' ')
        if scheme != 'Basic': raise NotAuthorized
        (username, password) = base64.b64decode(base64str).split(':')

        result = login(request, user_name=username, password=password)
        # ログインに失敗したときはアクセスを認めない
        if not result: raise NotAuthorized
        # 管理者権限がないときはアクセスを認めない
        if not request.user.is_admin: raise NotAuthorized
        return True

動作を検証するためのテストコードです。

# -*- coding: utf-8 -*-
import urllib2
import base64

username = 'username'
password = 'password'
url = 'http://localhost:8080/rest/metadata'

base64string =  base64.encodestring('%s:%s' % (username, password))[:-1]
authheader = 'Basic %s' % base64string

req = urllib2.Request(url)
req.add_header("Authorization", authheader)
handle = urllib2.urlopen(req)
print handle.read()