Content-Type: RST So at first, I had started writing this new blog using Apache's mod_python_, just as my old, messy one was. That... was apparently a bad decision, since mod_python apparently has some fundamental flaws which prevent it from working with Python features such as pickles_. .. _mod_python: http://www.modpython.org/ .. _pickles: http://docs.python.org/library/pickle.html Instead, the people at the #python channel (after telling me to "use a real database" instead of Durus) pointed out WSGI_ to me. I had heard of it before in passing, at the PyCon 2008 and 2009 conferences, but I had never really given it any thought. Even its definition confused me: "It is a specification for web servers and application servers to communicate with web applications (though it can also be used for more than that)." http://www.wsgi.org/wsgi/What_is_WSGI .. _WSGI: http://www.wsgi.org/wsgi/ I mean, seriously. But now, I was sort of forced into learning about it, and I've found it delightfully wonderful and friendly to web development. So, here's a brief introduction from my eyes. Let's start with a simple "Hello world!" .. sourcecode:: python def myApplication(environ, start_response): headers = [ ('Content-Type', 'text/plain') ] content = 'Hello world!' start_response('400 OK', headers) return [content] if __name__ == '__main__': from paste import httpserver httpserver.serve(myApplication, host='127.0.0.1', port='8080') Saving this as, for example, `app.py`, then executing it as any other Python script starts a webserver listening for requests on the localhost, port 8080. Whenever a request is received, it calls the myApplication() method (threaded, of course), and provides the environment (a sort-of request), and a start_response method. The rest is readable enough from my code. The result? Whenever a request is made to 127.0.0.1:8080 (regardless of the path or other arguments provided) it would return a 200 OK status, with plaintext data greeting you with a "Hello, world!" Yeah, that's interesting, but not impressive. However, the amount of control this allows can be interesting. For example, the environ variable (actually a dict) has the 'SCRIPT_NAME' and 'PATH_INFO' keys, which give the path used to navigate up to this point, and the path needed to be handled, respectively. Hmm, I think some apologies are in order to those completely confused by this - I swear not all my entries will be so confusing. So, to make you feel better, here's an intermission: .. figure:: http://blog.opensourcenerd.com/upload/too-cute-4-u Looks sort of like my family's new kitten! Now, back to `serious business`_. .. _`serious business`: http://tinyurl.com/2g9mqh I took advantage of the info to make my own organization system for the pages, based on Zope's system of views. As such, I called my own classes "Views" as well, and made them callable. I think this would be more understandable if I gave you an example, so here's the entirety of my `app.py`: .. sourcecode :: python #!/usr/bin/python from storage import storage_init from paste.request import parse_formvars from paste.session import SessionMiddleware from os import path, chdir import sys import views import rest_directive reserved_locations = { 'browse' : views.BrowseView, 'login': views.LogInView, 'register': views.RegisterView, 'newEntry': views.NewEntryView, 'upload': views.UploadView, 'feed.rss' : views.RSSEntryView, 'blog.css' : views.CSSView, 'blogsource.tar' : views.AllSourceView } class ObjectPublisher(object): def __init__(self): # Make sure PYTHONPATH is right curdir = path.dirname(path.realpath(__file__)) chdir(curdir) sys.path = [curdir+'/'] + sys.path sys.path = [curdir+'/templates/'] + sys.path # Make sure the database has at least *some* idea of wtf it is storage_init() def __call__(self, environ, start_response): fields = parse_formvars(environ) view = self.get_view(environ) response = view() response_body = response.read() start_response(response.http_status, response.getHeaders()) return [response_body] def get_view(self, environ): pathinfo = environ.get('PATH_INFO','') splitpath = pathinfo.strip('/').split('/') page = splitpath[0] session = environ['paste.session.factory']() view = None if page in reserved_locations: view = reserved_locations[page](environ, session, splitpath) else: view = views.EntryView(environ, session, splitpath) return view if __name__ == '__main__': from paste import httpserver app = ObjectPublisher() app = SessionMiddleware(app) httpserver.serve(app, host='127.0.0.1', port='8080') Whew! Let's run through how, for example, a request for `localhost:8080/browse` is processed. When I first run the server, the app object of the ObjectPublisher class is created. Its __init__ method is run, which ensures the Python classpath is correct, and that the current directory is right, and that the Durus database has the basic containers. Later, when the request for `localhost:8080/browse` is made, the __call__ method is run. I get the fields from the environment (the GET and POST inputs), then I call my own fabricated get_view method. It parses the URL, and fetches the session object. This finds the first "chunk" of the url - in this case, "browse" - and sees whether it is found in the reserved_locations dict I created earlier. In this case, it was found, and `reserved_locations["browse"]` maps to the views.BrowseView class, which is then initialized with the environ, session, and the full path. What happens in the view is another issue, but all that matters is that it returns a Response object, from which I can read the info using its read and getHeaders methods, and its http_status attribute. I think it's fairly straightforward, no? At least, it's better than trying to use mod_rewrite to rewrite "/an-entry-name" to "/blog.py/view?entry=an-entry-name". Before I explain how views work, let's have another intermission. .. figure :: http://blog.opensourcenerd.com/upload/is-hug-he-likes Is WSGI. Reader likes. Really. Now, let's continue tracking this request for the browsing entries page, and see where the View takes us: .. sourcecode :: python class BrowseView(ViewBase): template_file = "templates/browse.tmpl" def __init__(self, environ, session, extrapath): super(BrowseView,self).__init__(environ,session,extrapath) conn, root = newDurusConnection() entries = root['entries'] sorted_keys = reversed(sorted(entries.keys(), key = lambda x: entries[x].date)) self.searchList.update( {'title': 'Browse', 'description': 'No description', 'entries': entries, 'sorted_keys': sorted_keys }) Interesting fact: Python code doesn't need very much explanation - unless there's `lambda` involved. What that code above does is gets a database connection, then sorts all the entry *keys* (not the entries themselves) by the date of the entries, and reverses the order. Then, it gives the template's search list the title of the page, the entries, and the sorted keys. I won't go into how the ViewBase class (which BrowseView subclasses) initializes itself, but this is its __call__ method, which BrowseView doesn't override: .. sourcecode :: python def __call__(self): ''' Default __call__ ''' if self.traverseOverrideView: return self.traverseOverrideView() template = Template.Template(file=self.template_file, searchList=self.searchList) content = template.respond() self.response.write(content) self.response.http_status = "200 OK" return self.response The traverseOverrideView is there only for forwards-compatibility in case I make a view for static files, which does recursive traversing (so I don't have to override its __call__). Then, what it does is it composes a template object from the template_file variable using the provided searchList, makes the template render, writes it to the response, sets the status to OK, and returns it! .. figure :: http://www.opensourcenerd.com/resources/easy.png Heh, I've wanted to use this for a while, now. That's really it. All the other views are basically a reworking of this same concept. Of course, there is a little more interesting stuff going on there (creation of objects, different http statuses and content types) but it's all good, I won't badger you with those. Instead, if you're curious (you're not, are you...) I will have a function to view an instant .tar.gz snapshot of the code running on my server. Or I could stop being lazy and set up a Bazaar public branch. .. figure :: http://icanhascheezburger.files.wordpress.com/2007/02/tell-me-when-its-over.jpg Okay okay, I'm done speaking in computer jargon... for now.