Sunday, February 5, 2012

Your App Engine app in a python shell

This is highly inspired and almost-bluntly copied from Guido's post/NDB's startup.py file with a couple modifications.

The thing is, sometimes tests and dev_appserver is just not enough. I'm actually a big fan of Ruby on Rails, so I was kinda missing the "rails c" here.

Here comes the shell.py. It'll do something similar to the mentioned "rails console" command, i.e. it'll allow you to poke with your app code (models, handlers, libs, whatever) and database from the python shell. A "couple modifications" that I mentioned earlier are just some initialization tweaks that will point local Datastore stub to the actual database your app is using while being run with:

dev_appserver \
  --use_sqlite \
  --datastore_path =/path/to/app_root/tmp/dev.sqlite3 \
  (...other arguments go here)
Note that I'm running dev appserver with sqlite datastore type (default is NOT a sqlite3 database). Also, specifying the path will save the local dev data between your mac/pc reboots.

Copy the content below into a shell.py which you'd place in the app root, make it executable (chmod +x shell.py) and just run it from a terminal: ./shell.py

#!/usr/bin/env python -i

import os

from google.appengine.api import apiproxy_stub_map
# from google.appengine.api import datastore_file_stub
from google.appengine.datastore import datastore_sqlite_stub
from google.appengine.api import memcache
from google.appengine.api.memcache import memcache_stub
from google.appengine.api import taskqueue
from google.appengine.api.taskqueue import taskqueue_stub
from google.appengine.api import urlfetch_stub

from google.appengine.api import appinfo

app_root = os.path.dirname(__file__)
dbpath = "%s/tmp/dev.sqlite3" % app_root

# load app.yaml config
app_ext_info = appinfo.LoadSingleAppInfo(file('%s/app.yaml' % app_root, 'r'))
app_id = 'dev~%s' % app_ext_info.application

# set the app ID to make stubs happy, esp. datastore
os.environ['APPLICATION_ID'] = app_id

# Init the proxy map and stubs
apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()

# sqlite Datastore
opts_for_ds = {
  'require_indexes': True, 
  'verbose': True
}
ds_stub = datastore_sqlite_stub.DatastoreSqliteStub(app_id, dbpath, **opts_for_ds)
apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', ds_stub)

# Memcache
mc_stub = memcache_stub.MemcacheServiceStub()
apiproxy_stub_map.apiproxy.RegisterStub('memcache', mc_stub)

# Task queues
tq_stub = taskqueue_stub.TaskQueueServiceStub()
apiproxy_stub_map.apiproxy.RegisterStub('taskqueue', tq_stub)

# URLfetch service
uf_stub = urlfetch_stub.URLFetchServiceStub()
apiproxy_stub_map.apiproxy.RegisterStub('urlfetch', uf_stub)


# pretty printing
import pprint
pp = pprint.PrettyPrinter(indent=4).pprint

# load the app stuff, like models
from models import *

# It is now possible do something like this: (assuming NDB and a Page class)
#
# >>> pp(Page._properties)
# {   'active': BooleanProperty('active', indexed=False, default=True),
#     'body': TextProperty('body'),
#     'children': KeyProperty('children', repeated=True),
#     'level': IntegerProperty('level', required=True),
#     'locale': StringProperty('locale', default='en'),
#     'menu_item': BooleanProperty('menu_item', default=False),
#     'pos': IntegerProperty('pos', default=0),
#     'title': StringProperty('title', indexed=False, required=True),
#     'translations': KeyProperty('translations', indexed=False, repeated=True),
#     'updated_at': DateTimeProperty('updated_at', indexed=False, auto_now=True),
#     'url': StringProperty('url', required=True)}
#
# >>> Page.query().fetch()
# ...
#
# >>> from datetime import datetime
#
# >>> memcache.get('test')
# >>> memcache.set('test', datetime.now())
# True
# >>> memcache.get('test')
# datetime.datetime(2012, 2, 5, 18, 22, 44, 754663)

The code above makes a couple assumptions:
  • you're using sqlite version of the local Datastore
  • sqlite database file is in APP_ROOT/tmp/dev.sqlite3. If it's not your case just change the line n.17
  • you have a module "models" in your app. If it's not the case just remove line n.52
Also, you might want to tweak Datastore stub init arguments, lines 31-32.