Sugar-coating the App Engine remote_api

Since the 1.1.9 release, the Google App Engine SDK has come with a remote_api module which is a wonderful tool to interact with App Engine from your computer. However, using it involves a fair amount of boilerplate work which we can fortunately avoid.

Today, I will share with you a simple script – remote.py – which can do all the necessary staging in order for us to talk with our App Engine back-end at Google. remote.py provides a single function attach(host), which will configure the API to communicate with the specified host. This will allow us to easily write scripts that interact with the live serving application, or if we need to, a newly-deployed version.

To prepare the stage, we manipulate the sys.path so that we can import modules from the SDK and from our own application directory, then make the important ConfigureRemoteApi call. attach will, by default, make requests to APPID.appspot.com (the live serving instance of your app). Being a time-saver, attach will only ask for your password when the server is not on localhost, as user="foo", password="bar" works well enough (although you can obviously modify them to your liking ؟).

#!/usr/bin/env python

import getpass
import os
import sys

## Application specific
SDK_DIR = '/usr/local/google_appengine'
APP_DIR = '/home/username/src/code'
APPID = 'something-awesome'
EMAIL = 'my.email@host.dom'

REMOTE_API_PATH = '/remote_api'

## Extra paths to be inserted into sys.path,
## including the SDK, it's libraries, your APPDIR, and APPDIR/lib
EXTRA_PATHS = [
    SDK_DIR,
    os.path.join(SDK_DIR, 'lib', 'antlr3'),
    os.path.join(SDK_DIR, 'lib', 'django'),
    os.path.join(SDK_DIR, 'lib', 'webob'),
    os.path.join(SDK_DIR, 'lib', 'yaml', 'lib'),
    APP_DIR,
    os.path.join(APP_DIR, 'lib'),
]
sys.path = EXTRA_PATHS + sys.path

from google.appengine.ext.remote_api import remote_api_stub

def attach(host=None):
    def auth_func():
        if host and host.startswith('localhost'):
            return ('foo', 'bar')
        else:
            return (EMAIL, getpass.getpass())
    remote_api_stub.ConfigureRemoteApi(APPID, REMOTE_API_PATH, auth_func, host)
    remote_api_stub.MaybeInvokeAuthentication()
    os.environ['SERVER_SOFTWARE'] = 'Development (remote_api)/1.0'

Now, after with these sweet two lines:

import remote
remote.attach()

You can import any modules from the SDK as well as from your application directory, and use them to communicate with the remote App Engine back-end.

Obviously, you will need to install the /remote_api requests handlers as described in this article if you haven't done so already.

To maximize its utility, I made remote.py an executable script itself. When invoked as __main__, it will attach() the API to the default server (or localhost:8080 if the command line option -l is given), then bring up an interactive Python shell. It is similar to invoking the remote_api_shell.py script from the SDK but you won't have to inject your application directory into the sys.path or specify your APPID or host name on the command line. In fact, consider remote.py a refactored version of that script.

if __name__ == '__main__':
    if len(sys.argv) == 2 and sys.argv[1] == '-l':
        host = 'localhost:8080'
    else: 
        host = None

    attach(host)

    from google.appengine.ext import db
    from google.appengine.api import memcache
    
    BANNER = "App Engine remote_api shell\n" + \
    "Python %s\n" % sys.version + \
    "The db, and memcache modules are imported."
    
    ## Use readline for input completion/history if available
    try:
        import readline
    except ImportError:
        pass
    else:
        HISTORY_PATH = os.path.expanduser('~/.remote_api_shell_history')
        readline.parse_and_bind('tab: complete')
        if os.path.exists(HISTORY_PATH):
            readline.read_history_file(HISTORY_PATH)
        import atexit
        atexit.register(lambda: readline.write_history_file(HISTORY_PATH))

    sys.ps1 = '%s <-- ' % (host or APPID)

    import code
    code.interact(banner=BANNER, local=globals())

Here is the link to the complete source file.

It is worth mentioning that even though you could send remote requests to non-live versions of your app, they all share the same Datastore: only the application code and the OS enviroment variables will be different. ■

Comments