Writing Framework-less Python WSGI Applications for Fun and Profit
After writing WSGI applications over the past year or so, I’ve decided that the best general strategy to approach application design is to think of each request with the following steps:
Every block of the diagram is a WSGI application (middleware) save the adapter and request handler. Here is an overview of each block:
Common Middleware
Common Middleware applies to all requests across the entire site. This may be none or many. For intranet systems, common middleware includes session management (beaker), authentication/login screen, and user tracking. For a public site, common middleware could be gzip compression or localization negotiation. Nearly all sites will have some exception handling middleware that emails you when something AWFUL happens (Ooops!).
This is also a common place to put middleware that handles specific exceptions that triggers actions. This style of programming may be controversial in some circles, but no one can deny the practical benefit of being able to raise HTTPRedirect(’/login’) from any where and have it send the user to the given screen. I could actually write a whole post on designing site-wide middleware. Some day I just might!
Dispatcher
The purpose of the dispatcher is to examine the incoming request and invoke another WSGI application to handle the request. There are quite a few existing dispatching modules floating around the net, and I’ve written a few myself. I’ll explore them later, but I want to make one point: routes is not a dispatcher. Routes would be common middleware that sets an environ variable (routing_args) that would be used by a dispatcher to find a handler. The publicly-available dispatcher I’ve had the most experience with is Ian Bicking’s paste.urlparser. I’ve used it to good effect.
Object-request brokers (ORB’s) also fall into this category. I’ll just say that the ORB should invoke a method on an object with a WSGI interface. Everything should be WSGI all the way until the very final step.
Application Middleware
Now, for some requests, the WSI application that is invoked by the dispatcher is the end of the road. It handles the request and that is that. However, one is missing out on many time saving (and code saving) layers that can be built here.
The most common application middleware I’ve used is secondary dispatching middleware. Let’s say that you’re doing a good old HTML form with submit handler. The best thing to do here is to use an application-level dispatcher that works on HTTP Method. I have a module with 3 WSI applications. The site dispatcher sends the request to the “main” WSGI application. This WSGI application then sends the request to one of the other 2 dispatchers depending on the REQUEST_METHOD. Something like this:
application = MethodDispatch() @application def get(start_response, environ): pass @application def post(start_response, environ): pass
There are countless other ways of deciding different applications to run, but this is just to wet the appetite. I know their are “everything and the kitchen sink” dispatching systems that could take care of this a level up, but I like the locality of this approach. Like all the other categories, there is a lot more I can say about this block. Another time!
Request-specific Middleware
Request middleware applies to a single request made by the browser. I have found that decorators are a great way to introduce middleware at the request level. It’s hard to talk about request-specific middleware without also talking about adapters, so I’ll move on.
Adapters
An adapter breaks the chain from WSGI to another interface. WSGI is great as a standard, but trying to write your application logic using it as an interface is not very fun. Adapters are right at the top of ways I increase my productivity writing these web applications. The idea is simple: What kind of interface works best for this particular kind of request?
If the request is for JSON data. It might be best to have the procedure return a data structure that is turned into JSON and sent back to the browser. If a mako template gets used to render a HTML page, maybe a good interface would be to have a mako object passed into the request handler so that it can populate the template with data. When the request ends, the mako template is automatically rendered.
These adapters should not be generalized solutions to fit everyone’s potential use cases. They are very easy to write yourself. If you have a few ways mako templates can be handled, have a few different mako adapters. In the past, I’ve tried to write complex, catch-all type adapters. I’ve learned that simple, application-specific adapters are definitely the way to go.
Application-specific middleware can be used in tandem with adapters to augment the adapters functionality. The middleware can do something before or after the adapter (and handler) runs, so you can customize to your hearts content. As always though KISS!
To give an example of adapters and application middleware in action, here is a piece of real code:
import app.wsgilib as W reg = W.PathDispatch() application = reg.get_wsgi_app() @reg.default @W.mako('ppayment.tmpl') def main(req, res): pass @reg @W.json def trans_search(req, res): def eq(key, field, values): return ["%s = %%s" % field], [values[key]] def ilike(key, field, values): return ["%s ILIKE %%s" % field], [values[key]] # Tons of SQL/database code snipped res['sql'] = sql res['transactions'] = [dict(c) for c in cursor]
here W.json and W.mako adapt the WSGI interface into a more application-programmer friendly interface. I’m creating webob request objects and custom derived webob response objects as arguments for the request handler. PathDispatch is just a simple dispatcher that selects a registered method based on what’s in PATH_INFO.
When I say simple, I mean “SIMPLE”
class PathDispatch(dict): def __call__(self, func): self['/%s' % func.__name__] = func return func def default(self, func): self[None] = func def lookup(self, path_info): try: return self[path_info] except KeyError: return self[None] def get_wsgi_app(self): return wsgi(self.app) def app(self, req, res): proc = self.lookup(req.path_info) return proc(req, res)
The simplicity of this utility code is key. It should be easy and fun to hack on, transparent and not require a huge cognitive load to understand.
I’ve only scratched the surface. I hope to explore each level in greater detail in future posts!