Managing AppEngine from IPython
When developing a Google Appengine application, it can be a pain to manipulate the data in it, as you don’t have a console where to write python code (like you have in the dev appserver). Fortunately, a remote api exists.
I wanted a bit more, though:
- integration with IPython, to benefit from code completion and other goodies
- ability to use my
.netrcas a credentials provider
So here is an IPython profile that does that (in addition, it wraps the functionality of appcfg). Except for the bulkloader functionality, which is kind of ugly, the original code from appengine was generic enough to allow that kind of manipulation in a relatively clean way.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import IPython.ipapi | |
| from getpass import getpass | |
| from netrc import netrc | |
| from optparse import OptionParser | |
| from google.appengine.ext.remote_api import remote_api_stub | |
| from google.appengine.tools.appcfg import AppCfgApp, StatusUpdate | |
| from google.appengine.tools.bulkloader import RequestManager | |
| ip = IPython.ipapi.get() | |
| class ConnectParser(OptionParser): | |
| def __init__(self): | |
| OptionParser.__init__(self, usage="usage: %connect [options] <app_id>") | |
| self.add_option("-d", "--development", | |
| action="store_true", dest="development", default=False, | |
| help="connect to a dev server") | |
| self.add_option("-a", "--address", | |
| dest="address", default=None, | |
| help="server:port to connect to") | |
| self.add_option("-u", "--username", | |
| dest="username", default=None, | |
| help="username to connect as") | |
| self.add_option("-p", "--password", | |
| dest="password", default=None, | |
| help="password") | |
| class AppCfgParser(OptionParser): | |
| def parse_args(self, args): | |
| options, args = OptionParser.parse_args(self, args) | |
| host = options.server | |
| u,_,p = netrc().authenticators(host) or (None, None, None) | |
| if u is not None: | |
| options.email = options.email or u | |
| options._known_password = p | |
| return options, args | |
| class MyAppCfgApp(AppCfgApp): | |
| def __init__(self, argv): | |
| AppCfgApp.__init__(self, argv, parser_class=AppCfgParser) | |
| if self.options._known_password is not None: | |
| self.password_input_fn = (lambda prompt: self.options._known_password) | |
| def patchBulkLoader(): | |
| def AuthFunction(this): | |
| host = this.host | |
| u,_,p = netrc().authenticators(host) or (None, None, None) | |
| this.email = this.email or u | |
| def inputPassword(prompt): | |
| return p or getpass(prompt) | |
| return this.AuthFunction_orig(password_input_fn=inputPassword) | |
| RequestManager.AuthFunction_orig = RequestManager.AuthFunction | |
| RequestManager.AuthFunction = AuthFunction | |
| patchBulkLoader() | |
| connect_parser = ConnectParser() | |
| def _connect(self, arg): | |
| args = arg.split() | |
| try: | |
| options, args = connect_parser.parse_args(args) | |
| except SystemExit: | |
| return | |
| app_id = args[0] | |
| if options.address is not None: | |
| address = options.address | |
| else: | |
| if options.development: | |
| address = 'localhost:8080' | |
| else: | |
| address = '%s.appspot.com' % (app_id) | |
| if options.development: | |
| # no need for valid credentials with dev_appserver | |
| user, pwd = "foo", "bar" | |
| else: | |
| u,_,p = netrc().authenticators(address) or (None, None, None) | |
| user = options.username or u or raw_input('Username: ') | |
| pwd = options.password or p or getpass('Password: ') | |
| def auth_func(): | |
| return user, pwd | |
| remote_api_stub.ConfigureRemoteDatastore(app_id, '/remote_api', auth_func, address) | |
| def _appcfg(self, arg): | |
| argv = ['%appcfg'] + arg.split() | |
| try: | |
| return MyAppCfgApp(argv).Run() | |
| except KeyboardInterrupt: | |
| StatusUpdate('Interrupted.') | |
| except SystemExit: | |
| return | |
| def defineHelpers(): | |
| ip.expose_magic('connect', _connect) | |
| ip.expose_magic('appcfg', _appcfg) | |
| def main(): | |
| o = ip.options | |
| o.system_verbose = 0 | |
| o.banner = "GAE console v0.1" | |
| o.prompt_in1= r'\C_Green|\#> ' | |
| o.prompt_in2= r'\C_Green|\C_LightGreen\D\C_Green> ' | |
| o.prompt_out= '<\#> ' | |
| o.confirm_exit = False | |
| try: | |
| ip.ex("import os, sys, datetime, google") | |
| defineHelpers() | |
| except Exception, e: | |
| print "Exception: %s" % str(e) | |
| main() |
And this is what it allows:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $ ipython -p gae | |
| GAE console v0.1 | |
| [git/my-app]|1> %connect -d my-app # validate first on development server | |
| [git/my-app]|2> %run bootstrap.py # tweak the datastore | |
| [git/my-app]|3> %connect my-app # now replicate on production server | |
| [git/my-app]|4> %run bootstrap.py # tweak the same | |
| [git/my-app]|5> %appcfg --filename /tmp/my-app.data download_data . | |
| Application: my-app; version: 1. | |
| Downloading data records. | |
| [INFO ] Logging to bulkloader-log-20100916.143830 | |
| [INFO ] Throttling transfers: | |
| [INFO ] Bandwidth: 250000 bytes/second | |
| [INFO ] HTTP connections: 8/second | |
| [INFO ] Entities inserted/fetched/modified: 20/second | |
| [INFO ] Batch Size: 10 | |
| [INFO ] Opening database: bulkloader-progress-20100916.143830.sql3 | |
| [INFO ] Opening database: bulkloader-results-20100916.143830.sql3 | |
| [INFO ] Connecting to my-app.appspot.com/remote_api | |
| [INFO ] Downloading kinds: [u'Foo', u'Bar', u'Baz'] | |
| ... | |
| [INFO ] Have 5 entities, 0 previously transferred | |
| [INFO ] 5 entities (6533 bytes) transferred in 3.9 seconds | |
| [git/my-app]|6> |
Just in case, my ~/.netrc looks like:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| machine my-app.appspot.com | |
| login me@example.com | |
| password xxxxxxxxxxx | |
| machine appengine.google.com | |
| login me@example.com | |
| password xxxxxxxxxxx |
And of course, all those appengine libraries have to be somewhere in your python path. Here is the .pth file for my dedicated virtualenv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /home/yann/apps/google_appengine | |
| /home/yann/apps/google_appengine/lib/yaml/lib | |
| /home/yann/apps/google_appengine/lib/fancy_urllib | |
| /home/yann/apps/google_appengine/lib/antlr3 | |
| /home/yann/apps/google_appengine/lib/cacerts | |
| /home/yann/apps/google_appengine/lib/django | |
| /home/yann/apps/google_appengine/lib/ipaddr | |
| /home/yann/apps/google_appengine/lib/webob |