When trying to port Robaccia to App Engine the hard part has been finding anything to do. If you remember from my previous write-up I started with choosing the components to build on, but in the case of App Engine the model classes are already provided and the development kit already provides Django templates, which have become a favorite of mine, so there's little to choose except for a library to dispatch incoming requests. In the intervening years since I first wrote about Robaccia I've written my own library to do URL dispatching (WSGIDispatcher) and I'll use that.
There are other pieces we don't need to construct, for example, the
SDK comes with a development server so we don't need to write
that ourselves. We don't need dbconfig.py
either.
There were two bits of functionality in manage.py
, the first was
to create the tables in the database and the second was to run
a development server, but neither of those are needed if we
are running under App Engine; the datastore doesn't require you to
create tables before operating, and the SDK has it's own development
server.
That doesn't leave us with a lot.
We have our convention of models in model.py
, views in view.py
,
url dispatching via WSGIDispatcher in urls.py
, and finally all of the
templates in a templates
subdirectory.
The little bit of glue we need to add is the same as in
the original Robaccia which is render()
, a convenience
function for rendering templates.
import os import mimetypes from google.appengine.ext.webapp import template def render(start_response, template_file, template_values): contenttype, encoding = mimetypes.guess_type(template_file) if not contenttype: contenttype = "text/html" template_file = os.path.join(os.path.dirname(__file__), "templates", template_file) body = template.render(template_file, template_values) start_response("200 OK", [('Content-Type', contenttype)]) return [body]
One of the differences in robaccia.render()
under App Engine
is that we can't access files with a relative path name like was done in the original
Robaccia, so we will have to make them absolute using this technique:
template_file = os.path.join(os.path.dirname(__file__), "templates", template_file)
The app.yaml
is very simple since we will do all of our dispatching through main.py
:
application: robaccia-test-app version: 1 runtime: python api_version: 1 handlers: - url: .* script: main.py
You would still need to edit app.yaml
if you wanted to include static files, or
use HTTPS on some URIs.
Here is toy blog app to show how things hang together:
model.py
from google.appengine.ext import db class BlogEntry(db.Model): title = db.StringProperty() content = db.TextProperty() created = db.DateTimeProperty(auto_now_add=True) updated = db.DateTimeProperty(auto_now_add=True)
main.py
#!/usr/bin/env python from wsgiref.handlers import CGIHandler from urls import urls def main(): CGIHandler().run(urls) if __name__ == '__main__': main()
urls.py
import view from wsgidispatcher import Dispatcher urls = Dispatcher() urls.add('/blog/', GET=view.index, POST=view.create) urls.add('/blog/{id}/', view.member_get) urls.add('/blog/{id}/edit_form', GET=view.member_edit_form, POST=view.member_update)
view.py
import robaccia import model import cgi def index(environ, start_response): entries = model.BlogEntry.all().order("-created").fetch(20) return robaccia.render(start_response, 'index.html', locals()) def member_get(environ, start_response): id = int(environ['wsgiorg.routing_args'][1]['id']) entry = model.BlogEntry.get_by_id(id) return robaccia.render(start_response, 'entry.html', locals()) def create(environ, start_response): req = dict(cgi.parse_qsl(environ['wsgi.input'].read())) model.BlogEntry(title=req['title'], content=req['content']).put() start_response("303 See Other", [('Location', '/blog/')]) return [] def member_edit_form(environ, start_response): id = int(environ['wsgiorg.routing_args'][1]['id']) entry = model.BlogEntry.get_by_id(id) return robaccia.render(start_response, 'entry_form.html', locals()) def member_update(environ, start_response): id = int(environ['wsgiorg.routing_args'][1]['id']) entry = model.BlogEntry.get_by_id(id) req = dict(cgi.parse_qsl(environ['wsgi.input'].read())) entry.title = req['title'] entry.content = req['content'] entry.put() start_response("303 See Other", [('Location', '/blog/' + str(id) + "/edit_form")]) return []
From here you have the core of a framework and can grow it in any direction you like. For example, you may not find the WSGI interface for pulling out values from the request URI to be very intuitive. We can create a decorator that adds the wsgiorg.routing_args as calling parameters:
def wsgirouting(f): """ Decorator to turn WGSI call into a call the contains environ and start_response then all of the 'wsgiorg.routing_args' as *args and **kwargs. """ def wrapper(environ, start_response): args, kwargs = environ['wsgiorg.routing_args'] return f(environ, start_response, *args, **kwargs) return wrapper
Now our views are a little simpler:
@wsgirouting def member_edit_form(environ, start_response, id): entry = model.BlogEntry.get_by_id(int(id)) return robaccia.render(start_response, 'entry_form.html', locals())
The point of this isn't to create yet another Python web framework, just like the original article that introduced Robaccia, but to give an overview of the pieces that go into a web framework, and in this case, how those pieces interact with App Engine.
If you want to experiment with this code I've added it as a branch to the Robaccia project. To get the code:
svn checkout http://robaccia.googlecode.com/svn/branches/robaccia-app-engine-demo robaccia-app-engine-demo
Posted by Chris Dent on 2009-01-09
Posted by peter keane on 2009-01-09