GAE/PyでLow Level API Keyオブジェクトを触ってみる

Slim3本を読んで「PythonでもデータストアのLow Level APIを触ってみたいなー」と思った。うちのMacはメモリが少ないのでEclipseを起動したくないのです。

オープンソース徹底活用Slim3onGoogleAppEngineforJava

オープンソース徹底活用Slim3onGoogleAppEngineforJava

Slim3本に沿って、まずはKeyオブジェクトから触ってみる。KeyというのはGoogle App Engineのエンティティを一意にする識別子。リレーショナルデータベースでいうところのプライマリーキーのようなもの。
BigTable全体の中で一意になる必要があるため、キーの中に

  • アプリケーションID
  • 親キー
  • kind(テーブル名みたいなもの)
  • ID or キー名

の情報を持っている。

Keyの動作確認コード

  def test_idkey(self):
    key = Key.from_path('mykind', 1)
    print `key`
  
  def test_namedkey(self):
    key = Key.from_path('mykind', 'name')
    print `key`

Keyクラスの「from_path」関数でKeyを生成する。パラメータには「kindとid」または「kindとキー名」を渡す。idは数値でキー名は文字列。

出力

datastore_types.Key.from_path(u'mykind', 1, _app=u'testbed-test')
datastore_types.Key.from_path(u'mykind', u'name', _app=u'testbed-test')

親子関係をもつキー

キーを作成する時に親キーを指定することができる。これでキーの親子関係ができる。

テスト

  def test_childkey(self):
    parent = Key.from_path('parentkind', 1)
    child = Key.from_path('childkind', 1, parent=parent)
    grandchild = Key.from_path('grandchildkind', 1, parent=child)
    print `parent`
    print `child`
    print `grandchild`

出力

datastore_types.Key.from_path(u'parentkind', 1, _app=u'testbed-test')
datastore_types.Key.from_path(u'parentkind', 1, u'childkind', 1, _app=u'testbed-test')
datastore_types.Key.from_path(u'parentkind', 1, u'childkind', 1, u'grandchildkind', 1, _app=u'testbed-test')

GAE/Pyでユニットテスト

GAE/Pythonユニットテストをするための準備
テスト自動化というより、dev_appserverを立ち上げずにLow Level APIの動作を見るのが目的。

Kay frameworkでのテスト

Kay frameworkを使ってるならアプリケーションフォルダに「tests」フォルダを作ってテストコードを保存して以下のコマンドを実行すればテストできる。かんたん!

python manage.py test

プレーンな環境でのテスト

フレームワークなしの環境だとちょっとめんどい。以下のリンクを見ながらやってみる。

unittest2パッケージをインストール

まずはユニットテスト用のパッケージ「unittest2」をインストールします。

wget http://pypi.python.org/packages/source/u/unittest2/unittest2-0.5.1.tar.gz
tar xvzf http://pypi.python.org/packages/source/u/unittest2/unittest2-0.5.1.tar.gz
cd unittest2-0.5.1
sudo python setup.py install
テストランナーのスクリプト

次にテストを実行するスクリプトを作成して適当なフォルダに保存。私はホームディレクトリの直下に保存しました。
rungaetest.py

#!/usr/bin/env python
import optparse
import sys
# Install the Python unittest2 package before you run this script.
import unittest2

USAGE = """%prog SDK_PATH TEST_PATH
Run unit tests for App Engine apps.

SDK_PATH    Path to the SDK installation
TEST_PATH   Path to package containing test modules"""


def main(sdk_path, test_path):
    sys.path.insert(0, sdk_path)
    import dev_appserver
    dev_appserver.fix_sys_path()
    suite = unittest2.loader.TestLoader().discover(test_path)
    unittest2.TextTestRunner(verbosity=2).run(suite)


if __name__ == '__main__':
    parser = optparse.OptionParser(USAGE)
    options, args = parser.parse_args()
    if len(args) != 2:
        print 'Error: Exactly 2 arguments required.'
        parser.print_help()
        sys.exit(1)
    SDK_PATH = args[0]
    TEST_PATH = args[1]
    main(SDK_PATH, TEST_PATH)

直接実行できるようにします。

chmod 755 ~/.rungaetest.py
テストコードの例

テストコードを作成。setupとteardownで何か色々やっていますが、まだ詳しいことはわかっていません。

# testLLApi.py

import unittest
from google.appengine.ext import testbed
from google.appengine.api.datastore import Get, Put, Entity

class LLApiTestCase(unittest.TestCase):

  def setUp(self):
    self.testbed = testbed.Testbed()
    self.testbed.setup_env(app_id='ishida-appengine-py')
    self.testbed.activate()
    self.testbed.init_datastore_v3_stub()

  def tearDown(self):
    self.testbed.deactivate()

  def testPutAndGet(self):
    entity = Entity(kind='person')
    entity['name'] = 'ishida'
    entity['age'] = 31

    key = Put(entity)

    stored = Get(key)
    self.assertEqual('ishida', stored['name'])
    self.assertEqual(31, stored['age'])
テストを実行
~/testgaepython.py /usr/local/google_appengine ./tests

1つめのパラメータはGoogle App Engineをインストールしたディレクトリのパス。2つめはテストコードが保存されたディレクトリのパス。

こんな感じで出力されます。↓

makoto@Mac-mini% ~/rungaetest.py /usr/local/google_appengine ./tests                                [/Users/makoto/work/projects/gae/ishida-makot-py]
testPutAndGet (testLLApi.LLApiTestCase) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.005s

OK

GAE/PyでデータストアのLow Level Apiを直接呼んでみる

インターネットでGAE/PyのデータストアLow Level APIを触ってるようなサンプルを探したけど見当たらなかった。しょうがないのでソースを読んでみたらそれっぽいものを見つけたので動かしてみた。

# testLLApi.py

import unittest
from google.appengine.ext import testbed

from google.appengine.api.datastore import Get, Put, Entity
from google.appengine.api.datastore_types import Key

class LLApiTestCase(unittest.TestCase):

  def setUp(self):
    self.testbed = testbed.Testbed()
    self.testbed.setup_env(app_id='ishida-appengine-py')
    self.testbed.activate()
    self.testbed.init_datastore_v3_stub()

  def tearDown(self):
    self.testbed.deactivate()

  def testPutAndGet(self):
    entity = Entity(kind='person')
    entity['name'] = 'ishida'
    entity['age'] = 31

    key = Put(entity)

    stored = Get(key)
    self.assertEqual('ishida', stored['name'])
    self.assertEqual(31, stored['age'])

眠いので詳しいことはまた今度書く。きっと書く。

GAE/Pyでの日付型

Google App Engineで現在の日付時刻を取得するとUTC時刻が返ってくる。
しかもこの日付にはタイムゾーン情報が入ってない。

from datetime import datetime

now = datetime.now()
now.strftime('%Y/%m/%d %H:%M:%S%z')
# '2011/04/03 15:01:22'
# ↑実際の日本時間より9時間前の時刻
# タイムゾーンなし


dateutilとかを使ってタイムゾーンを指定してあげれば、日本時間を取得することができる。

from datetime import datetime
from dateutil.tz

jst = dateutil.tz.gettz('Asia/Tokyo')
now = datetime.now(jst)
now.strftime('%Y/%m/%d %H:%M:%S%z')
# '2011/04/04 00:05:49+0900'


kay frameworkを使ってるとこんな感じ。

from datetime import datetime
from kay.utils

jst = kay.utils.get_timezone(settings.DEFAULT_TIMEZONE)
now = datetime.now(jst)
now.strftime('%Y/%m/%d %H:%M:%S%z')
# '2011/04/04 00:11:13+0900'

データストアにはタイムゾーン情報を保存できない

上ので日本時間での現在時刻を取得することはできたけど、これをデータストアに保存するとタイムゾーン情報が消えてしまう。
正確に言うと、タイムゾーン情報がついた日時データは、UTC時刻に変換された上で、タイムゾーン情報のない状態で保存される。

now = datetime.now(jst))
ishida = Person(updated = now)
# '2011/04/04 00:11:13+0900'
# 日本時間。タイムゾーンあり。

ishida.put()
ishida = Person.get_by_id(1)
ishida.updated
# '2011/04/03 15:11:13'
# UTC。タイムゾーンなし。


日本時間とUTCが混在するとプログラムはとてもややこしくなる。同じUTCでもタイムゾーンを持つ日付と持たない日付が混在したりするともう手が付けられない。
例えば、タイムゾーンありの日付とタイムゾーンなしの日付は比較することができない。比較しようとすると以下のエラーが発生する。

can't compare offset-naive and offset-aware datetimes

ややこしさを減らすために統一する

日付型の変数が日本時間なのかUTCなのか、タイムゾーンを持ってるか持ってないかを意識してプログラミングしないといけないのだけど、そんなことをしていると複雑度が増して不幸になるので、統一してシンプルにする方向で考えた方がよい。

データストアがタイムゾーンなしのUTCを主張しているので、もうこれには逆らわない方がよい。データストアに保存する日付はタイムゾーンなしのUTCに統一する。そうするとプログラム中で扱う日付は全てタイムゾーンなしUTCに統一できる。

入出力の境界で変換する

統一できるといってもユーザーが入力する日付や、外部から取得したフィードの日付などは日本時間だったりタイムゾーンがついていたりする。
これらは実際の処理に渡す前にタイムゾーンなしのUTCに変換する必要がある。日本時間やタイムゾーンつきの日付を本来の処理に持ち込ませないよう、入力を受け付ける水際で全て変換する。
同様に、ユーザーに時刻を表示する際も、本来の処理が終わった後に日本時間に変換する。