7c9d165e0cd1 webui

Bundle webpy with the extension.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Tue, 13 Oct 2009 18:34:55 -0400
parents a5bfe965d4bc
children 96a4cc60e57d
branches/tags webui
files bundled/webpy/.gitignore bundled/webpy/ChangeLog.txt bundled/webpy/LICENSE.txt bundled/webpy/experimental/background.py bundled/webpy/experimental/migration.py bundled/webpy/experimental/pwt.py bundled/webpy/experimental/untwisted.py bundled/webpy/setup.py bundled/webpy/test/README bundled/webpy/test/__init__.py bundled/webpy/test/alltests.py bundled/webpy/test/application.py bundled/webpy/test/browser.py bundled/webpy/test/db.py bundled/webpy/test/doctests.py bundled/webpy/test/session.py bundled/webpy/test/webtest.py bundled/webpy/tools/_makedoc.py bundled/webpy/tools/makedoc.py bundled/webpy/tools/markdown.py bundled/webpy/web/__init__.py bundled/webpy/web/application.py bundled/webpy/web/browser.py bundled/webpy/web/contrib/__init__.py bundled/webpy/web/contrib/template.py bundled/webpy/web/db.py bundled/webpy/web/debugerror.py bundled/webpy/web/form.py bundled/webpy/web/http.py bundled/webpy/web/httpserver.py bundled/webpy/web/net.py bundled/webpy/web/session.py bundled/webpy/web/template.py bundled/webpy/web/test.py bundled/webpy/web/utils.py bundled/webpy/web/webapi.py bundled/webpy/web/webopenid.py bundled/webpy/web/wsgi.py bundled/webpy/web/wsgiserver/LICENSE.txt bundled/webpy/web/wsgiserver/__init__.py review/web_ui.py

Changes

--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/.gitignore	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,2 @@
+*.pyc
+.DS_Store
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/ChangeLog.txt	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,293 @@
+# web.py changelog
+
+## 2009-06-04 0.32
+
+* optional from_address to web.emailerrors
+* upgrade wsgiserver to CherryPy/3.1.2
+* support for extensions in Jinja2 templates (tx Zhang Huangbin)
+* support web.datestr for datetime.date objects also 
+* support for lists in db queries
+* new: uniq and iterview
+* fix: set debug=False when application is run with mod_wsgi (tx Patrick Swieskowski) [Bug#370904](https://bugs.launchpad.net/webpy/+bug/370904)
+* fix: make web.commify work  with decimals [Bug#317204](https://bugs.launchpad.net/webpy/+bug/317204)
+* fix: unicode issues with sqlite database [Bug#373219](https://bugs.launchpad.net/webpy/+bug/373219)
+* fix: urlquote url when the server is lighttpd [Bug#339858](https://bugs.launchpad.net/webpy/+bug/339858)
+* fix: issue with using date.format in templates
+* fix: use TOP instead of LIMIT in mssql database [Bug#324049](https://bugs.launchpad.net/webpy/+bug/324049)
+* fix: make sessions work well with expirations
+* fix: accept both list and tuple as arg values in form.Dropdown [Bug#314970](https://bugs.launchpad.net/webpy/+bug/314970)
+* fix: match parenthesis when parsing `for` statement in templates
+* fix: fix python 2.3 compatibility 
+* fix: ignore dot folders when compiling templates (tx Stuart Langridge) 
+* fix: don't consume KeyboardInterrupt and SystemExit errors 
+* fix: make application work well with iterators 
+
+## 2008-12-10: 0.31
+
+* new: browser module
+* new: test utilities
+* new: ShelfStore
+* fix: web.cookies error when default is None
+* fix: paramstyle for OracleDB (tx kromakey)
+* fix: performance issue in SQLQuery.join
+* fix: use wsgi.url_scheme to find ctx.protocol
+
+## 2008-12-06: 0.3
+
+* new: replace print with return (<i>backward-incompatible</i>)
+* new: application framework (<i>backward-incompatible</i>)
+* new: modular database system (<i>backward-incompatible</i>)
+* new: templetor reimplementation
+* new: better unicode support
+* new: debug mode (web.config.debug)
+* new: better db pooling
+* new: sessions
+* new: support for GAE
+* new: etag support
+* new: web.openid module
+* new: web.nthstr
+* fix: various form.py fixes
+* fix: python 2.6 compatibility
+* fix: file uploads are not loaded into memory
+* fix: SQLLiteral issue (Bug#180027)
+* change: web.background is moved to experimental (<i>backward-incompatible</i>) 
+* improved API doc generation (tx Colin Rothwell)
+
+## 2008-01-19: 0.23
+
+* fix: for web.background gotcha ([133079](http://bugs.launchpad.net/webpy/+bug/133079))
+* fix: for postgres unicode bug ([177265](http://bugs.launchpad.net/webpy/+bug/177265))
+* fix: web.profile behavior in python 2.5 ([133080](http://bugs.launchpad.net/webpy/+bug/133080))
+* fix: only uppercase HTTP methods are allowed. ([176415](http://bugs.launchpad.net/webpy/+bug/176415))
+* fix: transaction error in with statement ([125118](http://bugs.launchpad.net/webpy/+bug/125118))
+* fix: fix in web.reparam ([162085](http://bugs.launchpad.net/webpy/+bug/162085))
+* fix: various unicode issues ([137042](http://bugs.launchpad.net/webpy/+bug/137042), [180510](http://bugs.launchpad.net/webpy/+bug/180510), [180549](http://bugs.launchpad.net/webpy/+bug/180549), [180653](http://bugs.launchpad.net/webpy/+bug/180653))
+* new: support for https
+* new: support for secure cookies
+* new: sendmail
+* new: htmlunquote
+
+## 2007-08-23: 0.22
+
+* compatibility with new DBUtils API ([122112](https://bugs.launchpad.net/webpy/+bug/122112))
+* fix reloading ([118683](https://bugs.launchpad.net/webpy/+bug/118683))
+* fix compatibility between `changequery` and `redirect` ([118234](https://bugs.launchpad.net/webpy/+bug/118234))
+* fix relative URI in `web.redirect` ([118236](https://bugs.launchpad.net/webpy/+bug/118236))
+* fix `ctx._write` support in built-in HTTP server ([121908](https://bugs.launchpad.net/webpy/+bug/121908))
+* fix `numify` strips things after '.'s ([118644](https://bugs.launchpad.net/webpy/+bug/118644))
+* fix various unicode isssues ([114703](https://bugs.launchpad.net/webpy/+bug/114703), [120644](https://bugs.launchpad.net/webpy/+bug/120644), [124280](https://bugs.launchpad.net/webpy/+bug/124280))
+
+## 2007-05-28: 0.21
+
+* <strong>security fix:</strong> prevent bad characters in headers
+* support for cheetah template reloading                    
+* support for form validation                               
+* new `form.File`                                           
+* new `web.url`                                             
+* fix rendering issues with hidden and button inputs        
+* fix 2.3 incompatability with `numify`                     
+* fix multiple headers with same name                       
+* fix web.redirect issues when homepath is not /            
+* new CherryPy wsgi server                                  
+* new nested transactions                                   
+* new sqlliteral                                            
+
+## 2006-05-09: 0.138
+
+* New function: `intget`
+* New function: `datestr`
+* New function: `validaddr`
+* New function: `sqlwhere`
+* New function: `background`, `backgrounder`
+* New function: `changequery`
+* New function: `flush`
+* New function: `load`, `unload`
+* New variable: `loadhooks`, `unloadhooks`
+* Better docs; generating [docs](documentation) from web.py now
+* global variable `REAL_SCRIPT_NAME` can now be used to work around lighttpd madness
+* fastcgi/scgi servers now can listen on sockets
+* `output` now encodes Unicode
+* `input` now takes optional `_method` argument
+* <strong>Potentially-incompatible change:</strong> `input` now returns `badrequest` automatically when `requireds` aren't found
+* `storify` now takes lists and dictionaries as requests (see docs)
+* `redirect` now blanks any existing output
+* Quote SQL better when `db_printing` is on
+* Fix delay in `nomethod`
+* Fix `urlquote` to encode better.
+* Fix 2.3 incompatibility with `iters` (tx ??)
+* Fix duplicate headers
+* Improve `storify` docs
+* Fix `IterBetter` to raise IndexError, not KeyError
+
+## 2006-03-27: 0.137
+
+* Add function `dictfindall` (tx Steve Huffman)
+* Add support to `autodelegate` for arguments
+* Add functions `httpdate` and `parsehttpdate`
+* Add function `modified`
+* Add support for FastCGI server mode
+* Clarify `dictadd` documentation (tx Steve Huffman)
+* Changed license to public domain
+* Clean up to use `ctx` and `env` instead of `context` and `environ`
+* Improved support for PUT, DELETE, etc. (tx list)
+* Fix `ctx.fullpath` (tx Jesir Vargas)
+* Fix sqlite support (tx Dubhead)
+* Fix documentation bug in `lstrips` (tx Gregory Petrosyan)
+* Fix support for IPs and ports (1/2 tx Jesir Vargas)
+* Fix `ctx.fullpath` (tx Jesir Vargas)
+* Fix sqlite support (tx Dubhead)
+* Fix documentation bug in `lstrips` (tx Gregory Petrosyan)
+* Fix `iters` bug with sets
+* Fix some breakage introduced by Vargas's patch
+* Fix `sqlors` bug
+* Fix various small style things (tx Jesir Vargas)
+* Fix bug with `input` ignoring GET input
+
+## 2006-02-22: 0.136 (svn)
+
+* Major code cleanup (tx to Jesir Vargas for the patch).
+* 2006-02-15: 0.135
+* Really fix that mysql regression (tx Sean Leach).
+* 2006-02-15: 0.134
+* The `StopIteration` exception is now caught. This can be used by functions that do things like check to see if a user is logged in. If the user isn't, they can output a message with a login box and raise StopIteration, preventing the caller from executing.
+* Fix some documentation bugs.
+* Fix mysql regression (tx mrstone).
+
+## 2006-02-12: 0.133
+
+* Docstrings! (tx numerous, esp. Jonathan Mark (for the patch) and Guido van Rossum (for the prod))
+* Add `set` to web.iters.
+* Make the `len` returned by `query` an int (tx ??).
+* <strong>Backwards-incompatible change:</strong> `base` now called `prefixurl`.
+* <strong>Backwards-incompatible change:</strong> `autoassign` now takes `self` and `locals()` as arguments.
+
+## 2006-02-07: 0.132
+
+* New variable `iters` is now a listing of possible list-like types (currently list, tuple, and, if it exists, Set).
+* New function `dictreverse` turns `{1:2}` into `{2:1}`.
+* `Storage` now a dictionary subclass.
+* `tryall` now takes an optional prefix of functions to run.
+* `sqlors` has various improvements.
+* Fix a bunch of DB API bugs.
+* Fix bug with `storify` when it received multiple inputs (tx Ben Woosley).
+* Fix bug with returning a generator (tx Zbynek Winkler).
+* Fix bug where len returned a long on query results (tx F.S).
+
+
+## 2006-01-31: 0.131 (not officially released)
+
+* New function `_interpolate` used internally for interpolating strings.
+* Redone database API. `select`, `insert`, `update`, and `delete` all made consistent. Database queries can now do more complicated expressions like `$foo.bar` and `${a+b}`. You now have to explicitly pass the dictionary to look up variables in. Pass `vars=locals()` to get the old functionality of looking up variables .
+* New functions `sqllist` and `sqlors` generate certain kinds of SQL.
+
+## 2006-01-30: 0.13
+
+* New functions `found`, `seeother`, and `tempredirect` now let you do other kinds of redirects. `redirect` now also takes an optional status parameter. (tx many)
+* New functions `expires` and `lastmodified` make it easy to send those headers.
+* New function `gone` returns a 410 Gone (tx David Terrell).
+* New function `urlquote` applies url encoding to a string.
+* New function `iterbetter` wraps an iterator and allows you to do __getitem__s on it.
+* Have `query` return an `iterbetter` instead of an iterator.
+* Have `debugerror` show tracebacks with the innermost frame first.
+* Add `__hash__` function to `threadeddict` (and thus, `ctx`).
+* Add `context.host` value for the requested host name.
+* Add option `db_printing` that prints database queries and the time they take.
+* Add support for database pooling (tx Steve Huffman).
+* Add support for passing values to functions called by `handle`. If you do `('foo', 'value')` it will add `'value'` as an argument when it calls `foo`.
+* Add support for scgi (tx David Terrell for the patch).
+* Add support for web.py functions that are iterators (tx Brendan O'Connor for the patch).
+* Use new database cursors on each call instead of reusing one.
+* `setcookie` now takes an optional `domain` argument.
+* Fix bug in autoassign.
+* Fix bug where `debugerror` would break on objects it couldn't display.
+* Fix bug where you couldn't do `#include`s inline.
+* Fix bug with `reloader` and database calls.
+* Fix bug with `reloader` and base templates.
+* Fix bug with CGI mode on certain operating systems.
+* Fix bug where `debug` would crash if called outside a request.
+* Fix bug with `context.ip` giving weird values with proxies.
+
+## 2006-01-29: 0.129
+
+* Add Python 2.2 support.
+
+## 2006-01-28: 0.128
+
+* Fix typo in `web.profile`.
+
+## 2006-01-28: 0.127
+
+* Fix bug in error message if invalid dbn is sent (tx Panos Laganakos).
+
+## 2006-01-27: 0.126
+
+* Fix typos in Content-Type headers (tx Beat Bolli for the prod).
+
+## 2006-01-22: 0.125
+
+* Support Cheetah 2.0.
+
+## 2006-01-22: 0.124
+
+* Fix spacing bug (tx Tommi Raivio for the prod).
+
+## 2006-01-16: 0.123
+
+* Fix bug with CGI usage (tx Eddie Sowden for the prod).
+
+## 2006-01-14: 0.122
+
+* Allow DELETEs from `web.query` (tx Joost Molenaar for the prod).
+
+## 2006-01-08: 0.121
+
+* Allow import of submodules like `pkg.mod.cn` (tx Sridhar Ratna).
+* Fix a bug in `update` (tx Sergey Khenkin).
+
+## 2006-01-05: 0.12
+
+* <strong>Backwards-incompatible change:</strong> `db_parameters` is now a dictionary.
+* <strong>Backwards-incompatible change:</strong> `sumdicts` is now `dictadd`.
+* Add support for PyGreSQL, MySQL (tx Hallgrimur H. Gunnarsson).
+* Use HTML for non-Cheetah error message.
+* New function `htmlquote()`.
+* New function `tryall()`.
+* `ctx.output` can now be set to a generator. (tx Brendan O'Connor)
+
+## 2006-01-04: 0.117
+
+* Add support for psycopg 1.x. (tx Gregory Price)
+
+## 2006-01-04: 0.116
+
+* Add support for Python 2.3. (tx Evan Jones)
+
+## 2006-01-04: 0.115
+
+* Fix some bugs where database queries weren't reparameterized. Oops!
+* Fix a bug where `run()` wasn't getting the right functions.
+* Remove a debug statement accidentally left in.
+* Allow `storify` to be used on dictionaries. (tx Joseph Trent)
+
+## 2006-01-04: 0.114
+
+* Make `reloader` work on Windows. (tx manatlan)
+* Fix some small typos that affected colorization. (tx Gregory Price)
+
+## 2006-01-03: 0.113
+
+* Reorganize `run()` internals so mod_python can be used. (tx Nicholas Matsakis)
+
+## 2006-01-03: 0.112
+
+* Make `reloader` work when `code.py` is called with a full path. (tx David Terrell)
+
+## 2006-01-03: 0.111
+
+* Fixed bug in `strips()`. (tx Michael Josephson)
+
+## 2006-01-03: 0.11
+
+* First public version.
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/LICENSE.txt	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,4 @@
+web.py is in the public domain; it can be used for whatever purpose with absolutely no restrictions.
+
+CherryPy WSGI server that is included in the web.py as web.wsgiserver is licensed under CherryPy License. See web/wsgiserver/LICENSE.txt for more details.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/experimental/background.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,43 @@
+"""Helpers functions to run log-running tasks."""
+from web import utils
+from web import webapi as web
+
+def background(func):
+    """A function decorator to run a long-running function as a background thread."""
+    def internal(*a, **kw):
+        web.data() # cache it
+
+        tmpctx = web._context[threading.currentThread()]
+        web._context[threading.currentThread()] = utils.storage(web.ctx.copy())
+
+        def newfunc():
+            web._context[threading.currentThread()] = tmpctx
+            func(*a, **kw)
+            myctx = web._context[threading.currentThread()]
+            for k in myctx.keys():
+                if k not in ['status', 'headers', 'output']:
+                    try: del myctx[k]
+                    except KeyError: pass
+        
+        t = threading.Thread(target=newfunc)
+        background.threaddb[id(t)] = t
+        t.start()
+        web.ctx.headers = []
+        return seeother(changequery(_t=id(t)))
+    return internal
+background.threaddb = {}
+
+def backgrounder(func):
+    def internal(*a, **kw):
+        i = web.input(_method='get')
+        if '_t' in i:
+            try:
+                t = background.threaddb[int(i._t)]
+            except KeyError:
+                return web.notfound()
+            web._context[threading.currentThread()] = web._context[t]
+            return
+        else:
+            return func(*a, **kw)
+    return internal
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/experimental/migration.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,99 @@
+"""Migration script to run web.py 0.23 programs using 0.3.
+
+Import this module at the beginning of your program.
+"""
+import web
+import sys
+
+def setup_database():
+    if web.config.get('db_parameters'):
+        db = web.database(**web.config.db_parameters)
+        web.insert = db.insert
+        web.select = db.select
+        web.update = db.update
+        web.delete = db.delete
+        web.query = db.query
+
+        def transact():
+            t = db.transaction()
+            web.ctx.setdefault('transaction_stack', []).append(t)
+
+        def rollback():
+            stack = web.ctx.get('transaction_stack')
+            t = stack and stack.pop()
+            t and t.rollback()
+
+        def commit():
+            stack = web.ctx.get('transaction_stack')
+            t = stack and stack.pop()
+            t and t.commit()
+
+        web.transact = transact
+        web.rollback = rollback
+        web.commit = commit
+        
+web.loadhooks = web.webapi.loadhooks = {}
+web._loadhooks = web.webapi._loadhooks = {}
+web.unloadhooks = web.webapi.unloadhooks = {}
+
+def load():
+    setup_database()
+
+web.load = load
+
+def run(urls, fvars, *middleware):
+    setup_database()
+
+    def stdout_processor(handler):
+        handler()
+        return web.ctx.get('output', '')
+
+    def hook_processor(handler):
+        for h in web.loadhooks.values() + web._loadhooks.values(): h()
+        output = handler()
+        for h in web.unloadhooks.values(): h()
+        return output
+
+    app = web.application(urls, fvars)
+    app.add_processor(stdout_processor)
+    app.add_processor(hook_processor)
+    app.run(*middleware)
+
+class _outputter:
+    """Wraps `sys.stdout` so that print statements go into the response."""
+    def __init__(self, file): self.file = file
+    def write(self, string_):
+        if hasattr(web.ctx, 'output'):
+            return output(string_)
+        else:
+            self.file.write(string_)
+    def __getattr__(self, attr): return getattr(self.file, attr)
+    def __getitem__(self, item): return self.file[item]
+
+def output(string_):
+    """Appends `string_` to the response."""
+    string_ = web.utf8(string_)
+    if web.ctx.get('flush'):
+        web.ctx._write(string_)
+    else:
+        web.ctx.output += str(string_)
+
+def _capturedstdout():
+    sysstd = sys.stdout
+    while hasattr(sysstd, 'file'):
+        if isinstance(sys.stdout, _outputter): return True
+        sysstd = sysstd.file
+    if isinstance(sys.stdout, _outputter): return True
+    return False
+
+if not _capturedstdout():
+    sys.stdout = _outputter(sys.stdout)
+
+web.run = run
+
+class Stowage(web.storage):
+    def __str__(self):
+        return self._str
+
+web.template.Stowage = web.template.stowage = Stowage
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/experimental/pwt.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,96 @@
+import web
+import simplejson, sudo
+urls = (
+    '/sudo', 'sudoku',
+    '/length', 'length',
+)
+
+
+class pwt(object):
+    _inFunc = False
+    updated = {}
+    page = """
+<script src="/static/prototype.js"></script>
+<script src="/static/behaviour.js"></script>
+<script>
+Behaviour.register({'input': function (e) { 
+    e.onmouseup = e.onkeyup = e.onchange = function () { send(e) }
+}})
+</script>
+
+<form name="main" onsubmit="return false;">%s</form>
+
+<script>
+function send(e) {
+    ajax =  new Ajax.Request(document.location, {method:'post', parameters: 
+      Form.serialize(document.forms.main)
+    });
+}
+
+function receive(d) {
+    $H(d).keys().each(function (key) {
+        v = d[key];
+        k = document.forms.main[key];
+
+        if (k) k.value = v;
+        else $(key).innerHTML = v;
+    })
+}
+</script>
+"""
+
+    def GET(self):
+        web.header('Content-Type', 'text/html')
+        print self.page % self.form()
+    
+    def POST(self):
+        i = web.input()
+        if '_' in i: del i['_']
+        #for k, v in i.iteritems(): setattr(self, k, v)
+        
+        self._inFunc = True
+        self.work(**i)
+        self._inFunc = False
+        
+        web.header('Content-Type', 'text/javascript')
+        print 'receive('+simplejson.dumps(self.updated)+');'
+    
+    def __setattr__(self, k, v):
+        if self._inFunc and k != '_inFunc':
+            self.updated[k] = v
+        object.__setattr__(self, k, v)
+
+class sudoku(pwt):
+    def form(self):
+        import sudo
+        out = ''
+        n = 0
+        for i in range(9):
+            for j in range(9):
+                out += '<input type="text" size="1" name="%s" />' % (sudo.squares[n])
+                n += 1
+            out += '<br />'
+
+        return out
+    
+    def work(self, **kw):
+        values = dict((s, sudo.digits) for s in sudo.squares)
+        for k, v in kw.iteritems():
+            if v:
+                sudo.assign(values, k, v)
+
+        for k, v in values.iteritems():
+            if len(v) == 1:
+                setattr(self, k, v)
+
+        return values
+
+class length(pwt):
+    def form(self):
+        return '<p id="output">&nbsp;</p><input type="range" name="n" value="0" />'
+    
+    def work(self):
+        self.output = ('a' * web.intget(self.n, 0) or '&nbsp;')
+
+if __name__ == "__main__":
+    web.run(urls, globals(), web.reloader)
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/experimental/untwisted.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,134 @@
+import random
+
+from twisted.internet import reactor, defer
+from twisted.web import http
+
+import simplejson
+
+import web
+
+class Request(http.Request):
+    def process(self):
+        self.content.seek(0, 0)
+        env = {
+          'REMOTE_ADDR': self.client.host,
+          'REQUEST_METHOD': self.method,
+          'PATH_INFO': self.path,
+          'CONTENT_LENGTH': web.intget(self.getHeader('content-length'), 0),
+          'wsgi.input': self.content
+        }
+        if '?' in self.uri:
+            env['QUERY_STRING'] = self.uri.split('?', 1)[1]
+
+        for k, v in self.received_headers.iteritems():
+            env['HTTP_' + k.upper()] = v
+        
+        if self.path.startswith('/static/'):
+            f = web.lstrips(self.path, '/static/')
+            assert '/' not in f
+            #@@@ big security hole
+            self.write(file('static/' + f).read())
+            return self.finish()
+
+        web.webapi._load(env)
+        web.ctx.trequest = self
+        result = self.actualfunc()
+        self.setResponseCode(int(web.ctx.status.split()[0]))
+        for (h, v) in web.ctx.headers:
+            self.setHeader(h, v)
+        self.write(web.ctx.output)
+        if not web.ctx.get('persist'):
+            self.finish()
+
+class Server(http.HTTPFactory):
+    def __init__(self, func):
+        self.func = func
+
+    def buildProtocol(self, addr):
+        """Generate a channel attached to this site.
+        """
+        channel = http.HTTPFactory.buildProtocol(self, addr)
+        class MyRequest(Request):
+            actualfunc = staticmethod(self.func)
+        channel.requestFactory = MyRequest
+        channel.site = self
+        return channel
+
+def runtwisted(func):
+    reactor.listenTCP(8086, Server(func))
+    reactor.run()
+
+def newrun(inp, fvars):
+    print "Running on http://0.0.0.0:8086/"
+    runtwisted(web.webpyfunc(inp, fvars, False))
+
+def iframe(url):
+    return """
+    <iframe height="0" width="0" style="display: none" src="%s"/></iframe>
+    """ % url #("http://%s.ajaxpush.lh.theinfo.org:8086%s" % (random.random(), url))
+
+class Feed:
+    def __init__(self):
+        self.sessions = []
+    
+    def subscribe(self):
+        request = web.ctx.trequest
+        self.sessions.append(request)
+        request.connectionLost = lambda reason: self.sessions.remove(request)
+        web.ctx.persist = True
+    
+    def publish(self, text):
+        for x in self.sessions:
+            x.write(text)
+
+class JSFeed(Feed):
+    def __init__(self, callback="callback"):
+        Feed.__init__(self)
+        self.callback = callback
+        
+    def publish(self, obj):
+        web.debug("publishing")
+        Feed.publish(self, 
+          '<script type="text/javascript">window.parent.%s(%s)</script>' % (self.callback, simplejson.dumps(obj) + 
+          " " * 2048))
+
+if __name__ == "__main__":
+    mfeed = JSFeed()
+
+    urls = (
+      '/', 'view',
+      '/js', 'js',
+      '/send', 'send'
+    )
+
+    class view:
+        def GET(self):
+            print """
+<script type="text/javascript">
+function callback(item) {
+  document.getElementById('content').innerHTML += "<p>" + item + "</p>";
+}
+</script>
+
+<h2>Today's News</h2>
+
+<div id="content"></div>
+
+<h2>Contribute</h2>
+<form method="post" action="/send">
+  <textarea name="text"></textarea>
+  <input type="submit" value="send" />
+</form>
+<iframe id="foo" height="0" width="0" style="display: none" src="/js"/></iframe>
+            """
+        
+    class js:
+        def GET(self):
+            mfeed.subscribe()
+    
+    class send:
+        def POST(self):
+            mfeed.publish('<p>%s</p>' % web.input().text + (" " * 2048))
+            web.seeother('/')
+    
+    newrun(urls, globals())
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/setup.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# ...
+
+from distutils.core import setup
+
+setup(name='web.py',
+      version='0.32',
+      description='web.py: makes web apps',
+      author='Aaron Swartz',
+      author_email='me@aaronsw.com',
+      maintainer='Anand Chitipothu',
+      maintainer_email='anandology@gmail.com',
+      url=' http://webpy.org/',
+      packages=['web', 'web.wsgiserver', 'web.contrib'],
+      long_description="Think about the ideal way to write a web app. Write the code to make it happen.",
+      license="Public domain",
+      platforms=["any"],
+     )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/README	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,27 @@
+# web.py unit tests
+
+## Setup
+
+All databases expect a database with name `webpy` with username `scott` and password `tiger`.
+
+## Running all tests
+
+To run all tests:
+
+    $ python test/alltests.py
+
+## Running individual tests
+
+To run all tests in a file:
+
+    $ python test/db.py
+
+To run all tests in a class:
+
+    $ python test/db.py SqliteTest
+
+To run a single test:
+
+    $ python test/db.py SqliteTest.testUnicode
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/alltests.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,8 @@
+import webtest
+
+def suite():
+    modules = ["doctests", "db", "application", "session"]
+    return webtest.suite(modules)
+    
+if __name__ == "__main__":
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/application.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,297 @@
+import webtest
+import time
+
+import web
+import urllib
+
+data = """
+import web
+
+urls = ("/", "%(classname)s")
+app = web.application(urls, globals(), autoreload=True)
+
+class %(classname)s:
+    def GET(self):
+        return "%(output)s"
+
+"""
+
+urls = (
+    "/iter", "do_iter",
+)
+app = web.application(urls, globals())
+
+class do_iter:
+    def GET(self):
+        yield 'hello, '
+        yield web.input(name='world').name
+
+    POST = GET
+
+def write(filename, data):
+    f = open(filename, 'w')
+    f.write(data)
+    f.close()
+
+class ApplicationTest(webtest.TestCase):
+    def test_reloader(self):
+        write('foo.py', data % dict(classname='a', output='a'))
+        import foo
+        app = foo.app
+        
+        self.assertEquals(app.request('/').data, 'a')
+        
+        # test class change
+        time.sleep(1)
+        write('foo.py', data % dict(classname='a', output='b'))
+        self.assertEquals(app.request('/').data, 'b')
+
+        # test urls change
+        time.sleep(1)
+        write('foo.py', data % dict(classname='c', output='c'))
+        self.assertEquals(app.request('/').data, 'c')
+        
+    def testUppercaseMethods(self):
+        urls = ("/", "hello")
+        app = web.application(urls, locals())
+        class hello:
+            def GET(self): return "hello"
+            def internal(self): return "secret"
+            
+        response = app.request('/', method='internal')
+        self.assertEquals(response.status, '405 Method Not Allowed')
+        
+    def testRedirect(self):
+        urls = (
+            "/a", "redirect /hello/",
+            "/b/(.*)", r"redirect /hello/\1",
+            "/hello/(.*)", "hello"
+        )
+        app = web.application(urls, locals())
+        class hello:
+            def GET(self, name): 
+                name = name or 'world'
+                return "hello " + name
+            
+        response = app.request('/a')
+        self.assertEquals(response.status, '301 Moved Permanently')
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/')
+
+        response = app.request('/a?x=2')
+        self.assertEquals(response.status, '301 Moved Permanently')
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/?x=2')
+
+        response = app.request('/b/foo?x=2')
+        self.assertEquals(response.status, '301 Moved Permanently')
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/hello/foo?x=2')
+        
+    def test_subdirs(self):
+        urls = (
+            "/(.*)", "blog"
+        )
+        class blog:
+            def GET(self, path):
+                return "blog " + path
+        app_blog = web.application(urls, locals())
+        
+        urls = (
+            "/blog", app_blog,
+            "/(.*)", "index"
+        )
+        class index:
+            def GET(self, path):
+                return "hello " + path
+        app = web.application(urls, locals())
+        
+        self.assertEquals(app.request('/blog/foo').data, 'blog foo')
+        self.assertEquals(app.request('/foo').data, 'hello foo')
+        
+        def processor(handler):
+            return web.ctx.path + ":" + handler()
+        app.add_processor(processor)
+        self.assertEquals(app.request('/blog/foo').data, '/blog/foo:blog foo')
+    
+    def test_subdomains(self):
+        def create_app(name):
+            urls = ("/", "index")
+            class index:
+                def GET(self):
+                    return name
+            return web.application(urls, locals())
+        
+        urls = (
+            "a.example.com", create_app('a'),
+            "b.example.com", create_app('b'),
+            ".*.example.com", create_app('*')
+        )
+        app = web.subdomain_application(urls, locals())
+        
+        def test(host, expected_result):
+            result = app.request('/', host=host)
+            self.assertEquals(result.data, expected_result)
+            
+        test('a.example.com', 'a')
+        test('b.example.com', 'b')
+        test('c.example.com', '*')
+        test('d.example.com', '*')
+        
+    def test_redirect(self):
+        urls = (
+            "/(.*)", "blog"
+        )
+        class blog:
+            def GET(self, path):
+                if path == 'foo':
+                    raise web.seeother('/login', absolute=True)
+                else:
+                    raise web.seeother('/bar')
+        app_blog = web.application(urls, locals())
+        
+        urls = (
+            "/blog", app_blog,
+            "/(.*)", "index"
+        )
+        class index:
+            def GET(self, path):
+                return "hello " + path
+        app = web.application(urls, locals())
+        
+        response = app.request('/blog/foo')
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/login')
+        
+        response = app.request('/blog/foo', env={'SCRIPT_NAME': '/x'})
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/x/login')
+
+        response = app.request('/blog/foo2')
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/blog/bar')
+        
+        response = app.request('/blog/foo2', env={'SCRIPT_NAME': '/x'})
+        self.assertEquals(response.headers['Location'], 'http://0.0.0.0:8080/x/blog/bar')
+
+    def test_processors(self):
+        urls = (
+            "/(.*)", "blog"
+        )
+        class blog:
+            def GET(self, path):
+                return 'blog ' + path
+
+        state = web.storage(x=0, y=0)
+        def f():
+            state.x += 1
+
+        app_blog = web.application(urls, locals())
+        app_blog.add_processor(web.loadhook(f))
+        
+        urls = (
+            "/blog", app_blog,
+            "/(.*)", "index"
+        )
+        class index:
+            def GET(self, path):
+                return "hello " + path
+        app = web.application(urls, locals())
+        def g():
+            state.y += 1
+        app.add_processor(web.loadhook(g))
+
+        app.request('/blog/foo')
+        assert state.x == 1 and state.y == 1, repr(state)
+        app.request('/foo')
+        assert state.x == 1 and state.y == 2, repr(state)
+        
+    def testUnicodeInput(self):
+        urls = (
+            "(/.*)", "foo"
+        )
+        class foo:
+            def GET(self, path):
+                i = web.input(name='')
+                return repr(i.name)
+                
+            def POST(self, path):
+                if path == '/multipart':
+                    i = web.input(file={})
+                    return i.file.value
+                else:
+                    i = web.input()
+                    return repr(dict(i))
+                
+        app = web.application(urls, locals())
+        
+        def f(name):
+            path = '/?' + urllib.urlencode({"name": name.encode('utf-8')})
+            self.assertEquals(app.request(path).data, repr(name))
+            
+        f(u'\u1234')
+        f(u'foo')
+
+        response = app.request('/', method='POST', data=dict(name='foo'))
+        self.assertEquals(response.data, "{'name': u'foo'}")
+        
+        data = '--boundary\r\nContent-Disposition: form-data; name="x"\r\nfoo\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\nContent-Type: text/plain\r\n\r\na\r\n--boundary--\r\n'
+        headers = {'Content-Type': 'multipart/form-data; boundary=boundary'}
+        response = app.request('/multipart', method="POST", data=data, headers=headers)
+        self.assertEquals(response.data, 'a')
+        
+    def testCustomNotFound(self):
+        urls_a = ("/", "a")
+        urls_b = ("/", "b")
+        
+        app_a = web.application(urls_a, locals())
+        app_b = web.application(urls_b, locals())
+        
+        app_a.notfound = lambda: web.HTTPError("404 Not Found", {}, "not found 1")
+        
+        urls = (
+            "/a", app_a,
+            "/b", app_b
+        )
+        app = web.application(urls, locals())
+        
+        def assert_notfound(path, message):
+            response = app.request(path)
+            self.assertEquals(response.status.split()[0], "404")
+            self.assertEquals(response.data, message)
+            
+        assert_notfound("/a/foo", "not found 1")
+        assert_notfound("/b/foo", "not found")
+        
+        app.notfound = lambda: web.HTTPError("404 Not Found", {}, "not found 2")
+        assert_notfound("/a/foo", "not found 1")
+        assert_notfound("/b/foo", "not found 2")
+
+    def testIter(self):
+        self.assertEquals(app.request('/iter').data, 'hello, world')
+        self.assertEquals(app.request('/iter?name=web').data, 'hello, web')
+
+        self.assertEquals(app.request('/iter', method='POST').data, 'hello, world')
+        self.assertEquals(app.request('/iter', method='POST', data='name=web').data, 'hello, web')
+
+    def testUnload(self):
+        x = web.storage(a=0)
+
+        urls = (
+            "/foo", "foo",
+            "/bar", "bar"
+        )
+        class foo:
+            def GET(self):
+                return "foo"
+        class bar:
+            def GET(self):
+                raise web.notfound()
+
+        app = web.application(urls, locals())
+        def unload():
+            x.a += 1
+        app.add_processor(web.unloadhook(unload))
+
+        app.request('/foo')
+        self.assertEquals(x.a, 1)
+
+        app.request('/bar')
+        self.assertEquals(x.a, 2)
+
+if __name__ == '__main__':
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/browser.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,64 @@
+import webtest
+import web
+
+urls = (
+    "/", "index",
+    "/hello/(.*)", "hello",
+    "/cookie", "cookie",
+    "/setcookie", "setcookie",
+    "/redirect", "redirect",
+)
+app = web.application(urls, globals())
+
+class index:
+    def GET(self):
+        return "welcome"
+
+class hello:
+    def GET(self, name):
+        name = name or 'world'
+        return "hello, " + name + '!'
+
+class cookie:
+    def GET(self):
+        return ",".join(sorted(web.cookies().keys()))
+
+class setcookie:
+    def GET(self):
+        i = web.input()
+        for k, v in i.items():
+            web.setcookie(k, v)
+        return "done"
+
+class redirect:
+    def GET(self):
+        i = web.input(url='/')
+        raise web.seeother(i.url)
+
+class BrowserTest(webtest.TestCase):
+    def testCookies(self):
+        b = app.browser()
+        b.open('http://0.0.0.0/setcookie?x=1&y=2')
+        b.open('http://0.0.0.0/cookie')
+        self.assertEquals(b.data, 'x,y')
+
+    def testNotfound(self):
+        b = app.browser()
+        b.open('http://0.0.0.0/notfound')
+        self.assertEquals(b.status, 404)
+
+    def testRedirect(self):
+        b = app.browser()
+
+        b.open('http://0.0.0.0:8080/redirect')
+        self.assertEquals(b.url, 'http://0.0.0.0:8080/')
+        b.open('http://0.0.0.0:8080/redirect?url=/hello/foo')
+        self.assertEquals(b.url, 'http://0.0.0.0:8080/hello/foo')
+
+        b.open('https://0.0.0.0:8080/redirect')
+        self.assertEquals(b.url, 'https://0.0.0.0:8080/')
+        b.open('https://0.0.0.0:8080/redirect?url=/hello/foo')
+        self.assertEquals(b.url, 'https://0.0.0.0:8080/hello/foo')
+
+if __name__ == "__main__":
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/db.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,162 @@
+"""DB test"""
+import webtest
+import web
+
+class DBTest(webtest.TestCase):
+    dbname = 'postgres'
+    driver = None
+    
+    def setUp(self):
+        self.db = webtest.setup_database(self.dbname, driver=self.driver)
+        self.db.query("CREATE TABLE person (name text, email text, active boolean)")
+
+    def tearDown(self):
+        # there might be some error with the current connection, delete from a new connection
+        self.db = webtest.setup_database(self.dbname, driver=self.driver)
+        self.db.query('DROP TABLE person')
+        
+    def _testable(self):
+        try:
+            webtest.setup_database(self.dbname, driver=self.driver)
+            return True
+        except ImportError, e:
+            print >> web.debug, str(e), "(ignoring %s)" % self.__class__.__name__
+            return False
+    
+    def testUnicode(self):
+        # Bug#177265: unicode queries throw errors
+        self.db.select('person', where='name=$name', vars={'name': u'\xf4'})
+    
+    def assertRows(self, n):
+        result = self.db.select('person')
+        self.assertEquals(len(list(result)), n)
+        
+    def testCommit(self):
+        t = self.db.transaction()
+        self.db.insert('person', False, name='user1')
+        t.commit()
+
+        t = self.db.transaction()
+        self.db.insert('person', False, name='user2')
+        self.db.insert('person', False, name='user3')
+        t.commit()
+    
+        self.assertRows(3)
+        
+    def testRollback(self):
+        t = self.db.transaction()
+        self.db.insert('person', False, name='user1')
+        self.db.insert('person', False, name='user2')
+        self.db.insert('person', False, name='user3')
+        t.rollback()        
+        self.assertRows(0)
+        
+    def testWrongQuery(self):
+        # It should be possible to run a correct query after getting an error from a wrong query.
+        try:
+            self.db.select('notthere')
+        except:
+            pass
+        self.db.select('person')
+        
+    def testNestedTransactions(self):
+        t1 = self.db.transaction()
+        self.db.insert('person', False, name='user1')
+        self.assertRows(1)        
+        
+        t2 = self.db.transaction()
+        self.db.insert('person', False, name='user2')
+        self.assertRows(2)  
+        t2.rollback()
+        self.assertRows(1)  
+        t3 = self.db.transaction()
+        self.db.insert('person', False, name='user3')
+        self.assertRows(2)  
+        t3.commit()
+        t1.commit()
+        self.assertRows(2)
+        
+    def testPooling(self):
+        # can't test pooling if DBUtils is not installed
+        try:
+            import DBUtils
+        except ImportError:
+            return
+        db = webtest.setup_database(self.dbname, pooling=True)
+        self.assertEquals(db.ctx.db.__class__.__module__, 'DBUtils.PooledDB')
+        db.select('person', limit=1)
+
+    def test_multiple_insert(self):
+        db = webtest.setup_database(self.dbname)
+        db.multiple_insert('person', [dict(name='a'), dict(name='b')], seqname=False)
+
+        assert db.select("person", where="name='a'")
+        assert db.select("person", where="name='b'")
+
+    def test_result_is_unicode(self):
+        db = webtest.setup_database(self.dbname)
+        self.db.insert('person', False, name='user')
+        name = db.select('person')[0].name
+        self.assertEquals(type(name), unicode)
+
+    def testBoolean(self):
+        def t(active):
+            name ='name-%s' % active
+            self.db.insert('person', False, name=name, active=active)
+            a = self.db.select('person', where='name=$name', vars=locals())[0].active
+            self.assertEquals(a, active)
+        t(False)
+        t(True)
+
+class PostgresTest(DBTest):
+    dbname = "postgres"
+    driver = "psycopg2"
+
+class PostgresTest_psycopg(PostgresTest):
+    driver = "psycopg"
+
+class PostgresTest_pgdb(PostgresTest):
+    driver = "pgdb"
+
+class SqliteTest(DBTest):
+    dbname = "sqlite"
+    driver = "sqlite3"
+    
+    def testNestedTransactions(self):
+        #nested transactions does not work with sqlite
+        pass
+
+class SqliteTest_pysqlite2(SqliteTest):
+    driver = "pysqlite2.dbapi2"
+
+class MySQLTest(DBTest):
+    dbname = "mysql"
+    
+    def setUp(self):
+        self.db = webtest.setup_database(self.dbname)
+        # In mysql, transactions are supported only with INNODB engine.
+        self.db.query("CREATE TABLE person (name text, email text) ENGINE=INNODB")
+
+    def testBoolean(self):
+        # boolean datatype is not suppoted in MySQL (at least until v5.0)
+        pass
+
+del DBTest
+
+def is_test(cls):
+    import inspect
+    return inspect.isclass(cls) and webtest.TestCase in inspect.getmro(cls)
+
+# ignore db tests when the required db adapter is not found.
+for t in globals().values():
+    if is_test(t) and not t('_testable')._testable():
+        del globals()[t.__name__]
+del t
+
+try:
+    import DBUtils
+except ImportError, e:
+    print >> web.debug, str(e) + "(ignoring testPooling)"
+
+if __name__ == '__main__':
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/doctests.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,20 @@
+"""Run all doctests in web.py.
+"""
+import webtest
+
+def suite():
+    modules = [
+        "web.application",
+        "web.db", 
+        "web.http", 
+        "web.net", 
+        "web.session",
+        "web.template",
+        "web.utils", 
+#        "web.webapi", 
+#        "web.wsgi", 
+    ]
+    return webtest.doctest_suite(modules)
+    
+if __name__ == "__main__":
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/session.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,74 @@
+import webtest
+import web
+import tempfile
+
+class SessionTest(webtest.TestCase):
+    def setUp(self):
+        app = web.auto_application()
+        session = self.make_session(app)
+        class count(app.page):
+            def GET(self):
+                session.count += 1
+                return str(session.count)
+        
+        class reset(app.page):
+            def GET(self):
+                session.kill()
+                return ""
+                
+        self.app = app
+        self.session = session
+        
+    def make_session(self, app):
+        dir = tempfile.mkdtemp()
+        store = web.session.DiskStore(tempfile.mkdtemp())
+        return web.session.Session(app, store, {'count': 0})
+        
+    def testSession(self):
+        b = self.app.browser() 
+        self.assertEquals(b.open('/count').read(), '1')
+        self.assertEquals(b.open('/count').read(), '2')
+        self.assertEquals(b.open('/count').read(), '3')
+        b.open('/reset')
+        self.assertEquals(b.open('/count').read(), '1')
+
+    def testParallelSessions(self):
+        b1 = self.app.browser()
+        b2 = self.app.browser()
+        
+        b1.open('/count')
+        
+        for i in range(1, 10):
+            self.assertEquals(b1.open('/count').read(), str(i+1))
+            self.assertEquals(b2.open('/count').read(), str(i))
+
+    def testBadSessionId(self):
+        b = self.app.browser()
+        self.assertEquals(b.open('/count').read(), '1')
+        self.assertEquals(b.open('/count').read(), '2')
+        
+        cookie = b.cookiejar._cookies['0.0.0.0']['/']['webpy_session_id']
+        cookie.value = '/etc/password'
+        self.assertEquals(b.open('/count').read(), '1')
+
+class DBSessionTest(SessionTest):
+    """Session test with db store."""
+    def make_session(self, app):
+        db = webtest.setup_database("postgres")
+        #db.printing = True
+        db.query("" 
+            + "CREATE TABLE session ("
+            + "    session_id char(128) unique not null,"
+            + "    atime timestamp default (current_timestamp at time zone 'utc'),"
+            + "    data text)"
+        )
+        store = web.session.DBStore(db, 'session')
+        return web.session.Session(app, store, {'count': 0})
+         
+    def tearDown(self):
+        # there might be some error with the current connection, delete from a new connection
+        self.db = webtest.setup_database("postgres")
+        self.db.query('DROP TABLE session')
+
+if __name__ == "__main__":
+    webtest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/test/webtest.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,20 @@
+"""webtest: test utilities.
+"""
+import sys, os
+
+# adding current directory to path to make sure local modules can be imported
+sys.path.insert(0, '.')
+
+from web.test import *
+    
+def setup_database(dbname, driver=None, pooling=False):
+    if dbname == 'sqlite':
+        db = web.database(dbn=dbname, db='webpy.db', pooling=pooling, driver=driver)
+    elif dbname == 'postgres':
+        user = os.getenv('USER')
+        db = web.database(dbn=dbname, db='webpy', user=user, pw='', pooling=pooling, driver=driver)
+    else:
+        db = web.database(dbn=dbname, db='webpy', user='scott', pw='tiger', pooling=pooling, driver=driver)
+
+    db.printing = '-v' in sys.argv
+    return db
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/tools/_makedoc.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,81 @@
+import os
+import web
+
+class Parser:
+    def __init__(self):
+        self.mode = 'normal'
+        self.text = ''
+        
+    def go(self, pyfile):
+        for line in file(pyfile):
+            if self.mode == 'in def':
+                self.text += ' ' + line.strip()
+                if line.strip().endswith(':'):
+                    if self.definition(self.text):
+                        self.text = ''
+                        self.mode = 'in func'
+                    else:
+                        self.text = ''
+                        self.mode = 'normal'
+
+            elif self.mode == 'in func':
+                if '"""' in line:
+                    self.text += line.strip().strip('"')
+                    self.mode = 'in doc'
+                    if line.count('"""') == 2:
+                        self.mode = 'normal'
+                        self.docstring(self.text)
+                        self.text = ''
+                else:
+                    self.mode = 'normal'
+
+            elif self.mode == 'in doc':
+                self.text += ' ' + line
+                if '"""' in line:
+                    self.mode = 'normal'
+                    self.docstring(self.text.strip().strip('"'))
+                    self.text = ''
+            
+            elif line.startswith('## '):
+                self.header(line.strip().strip('#'))
+            
+            elif line.startswith('def ') or line.startswith('class '):
+                self.text += line.strip().strip(':')
+                if line.strip().endswith(':'):
+                    if self.definition(self.text):
+                        self.text = ''
+                        self.mode = 'in func'
+                    else:
+                        self.text = ''
+                        self.mode = 'normal'
+                else:
+                    self.mode = 'in def'
+    
+    def clean(self, text):
+        text = text.strip()
+        text = text.replace('*', r'\*')
+        return text
+    
+    def definition(self, text):
+        text = web.lstrips(text, 'def ')
+        if text.startswith('_') or text.startswith('class _'):
+            return False
+        print '`'+text.strip()+'`'
+        return True
+    
+    def docstring(self, text):
+        print '   :', text.strip()
+        print
+    
+    def header(self, text):
+        print '##', text.strip()
+        print
+        
+for pyfile in os.listdir('trunk/web'):
+    if pyfile[-2:] == 'py':
+        print
+        print '## ' + pyfile
+        print
+        Parser().go('trunk/web/' + pyfile)
+print '`ctx`\n   :',
+print '\n'.join('    '+x for x in web.ctx.__doc__.strip().split('\n'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/tools/makedoc.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,170 @@
+"""
+Outputs web.py docs as html
+version 2.0: documents all code, and indents nicely.
+By Colin Rothwell (TheBoff)
+"""
+import sys
+import inspect
+import markdown
+sys.path.insert(0, '..')
+
+modules = [
+    'web.application',
+    'web.contrib.template',
+    'web.db',
+    'web.debugerror',
+    'web.form',
+    'web.http',
+    'web.httpserver',
+    'web.net',
+    'web.session',
+    'web.template',
+    'web.utils',
+    'web.webapi',
+    'web.webopenid',
+    'web.wsgi'
+]
+
+item_start = '<code class="%s">'
+item_end = '</code>'
+
+indent_amount = 30
+
+doc_these = ( #These are the types of object that should be docced
+    'module',
+    'classobj',
+    'instancemethod',
+    'function',
+    'type',
+    'property',    
+)
+
+not_these_names = ( #Any particular object names that shouldn't be doced
+    'fget',
+    'fset',
+    'fdel',
+    'storage', #These stop the lower case versions getting docced
+    'memoize',
+    'iterbetter',
+    'capturesstdout',
+    'profile',
+    'threadeddict',
+    'd', #Don't know what this is, but only only conclude it shouldn't be doc'd
+)
+
+css = '''
+<style type="text/css">
+.module {
+    font-size: 130%;
+    font-weight: bold;
+}
+
+.function, .class, .type {
+    font-size: 120%;
+    font-weight: bold;
+}
+
+.method, .property {
+    font-size: 115%;
+    font-weight: bold;
+}
+
+.ts {
+    font-size: small;
+    font-weight: lighter;
+    color: grey;
+}
+
+#contents_link {
+    position: fixed;
+    top: 0;
+    right: 0;
+    padding: 5px;
+    background: rgba(255, 255, 255, 0.5);
+}
+
+#contents_link a:hover {
+    font-weight: bold;
+}
+</style>
+'''
+
+
+indent_start = '<div style="margin-left:%dpx">'
+indent_end = '</div>'
+
+header = '''
+<div id="contents_link">
+<a href="#top">Back to contents</a>
+</div>
+'''
+
+def type_string(ob):
+    return str(type(ob)).split("'")[1]
+    
+def ts_css(text):
+    """applies nice css to the type string"""
+    return '<span class="ts">%s</span>' % text
+    
+def arg_string(func):
+    """Returns a nice argstring for a function or method"""
+    return inspect.formatargspec(*inspect.getargspec(func))
+
+def recurse_over(ob, name, indent_level=0):
+    ts = type_string(ob)    
+    if not ts in doc_these: return #stos what shouldn't be docced getting docced
+    if indent_level > 0 and ts == 'module': return #Stops it getting into the stdlib    
+    if name in not_these_names: return #Stops things we don't want getting docced
+    
+    indent = indent_level * indent_amount #Indents nicely
+    ds_indent = indent + (indent_amount / 2)
+    if indent_level > 0: print indent_start % indent
+    
+    argstr = ''
+    if ts.endswith(('function', 'method')):
+        argstr = arg_string(ob)
+    elif ts == 'classobj' or ts == 'type':
+        if ts == 'classobj': ts = 'class'
+        if hasattr(ob, '__init__'):
+            if type_string(ob.__init__) == 'instancemethod':
+                argstr = arg_string(ob.__init__)
+        else:
+            argstr = '(self)'
+    if ts == 'instancemethod': ts = 'method' #looks much nicer
+    
+    ds = inspect.getdoc(ob)
+    if ds is None: ds = ''
+    ds = markdown.Markdown(ds)
+    
+    mlink = '<a name="%s">' % name if ts == 'module' else '' 
+    mend = '</a>' if ts == 'module' else ''
+                
+    print ''.join(('<p>', ts_css(ts), item_start % ts, ' ', mlink, name, argstr,
+            mend, item_end, '<br />'))
+    print ''.join((indent_start % ds_indent, ds, indent_end, '</p>'))
+    #Although ''.join looks wierd, it's alot faster is string addition    
+    members = ''
+    
+    if hasattr(ob, '__all__'): members = ob.__all__
+    else: members = [item for item in dir(ob) if not item.startswith('_')] 
+    
+    if not 'im_class' in members:    
+        for name in members:
+            recurse_over(getattr(ob, name), name, indent_level + 1)
+    if indent_level > 0: print indent_end
+
+def main():
+    print '<div>' #Stops markdown vandalising my html.
+    print css
+    print header
+    print '<ul>'
+    for name in modules:
+        print '<li><a href="#%(name)s">%(name)s</a></li>' % dict(name=name)
+    print '</ul>' 
+    for name in modules:
+        mod = __import__(name, {}, {}, 'x')
+        recurse_over(mod, name)
+    print '</div>'
+        
+if __name__ == '__main__':
+    main()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/tools/markdown.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,677 @@
+#!/usr/bin/python
+import re, md5, sys, string
+
+"""markdown.py: A Markdown-styled-text to HTML converter in Python.
+
+Usage:
+  ./markdown.py textfile.markdown
+ 
+Calling:
+  import markdown
+  somehtml = markdown.markdown(sometext)
+
+For other versions of markdown, see: 
+  http://www.freewisdom.org/projects/python-markdown/
+  http://en.wikipedia.org/wiki/Markdown
+"""
+
+__version__ = '1.0.1-2' # port of 1.0.1
+__license__ = "GNU GPL 2"
+__author__ = [
+  'John Gruber <http://daringfireball.net/>',
+  'Tollef Fog Heen <tfheen@err.no>', 
+  'Aaron Swartz <me@aaronsw.com>'
+]
+
+def htmlquote(text):
+    """Encodes `text` for raw use in HTML."""
+    text = text.replace("&", "&amp;") # Must be done first!
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    text = text.replace("'", "&#39;")
+    text = text.replace('"', "&quot;")
+    return text
+
+def semirandom(seed):
+    x = 0
+    for c in md5.new(seed).digest(): x += ord(c)
+    return x / (255*16.)
+
+class _Markdown:
+    emptyelt = " />"
+    tabwidth = 4
+
+    escapechars = '\\`*_{}[]()>#+-.!'
+    escapetable = {}
+    for char in escapechars:
+        escapetable[char] = md5.new(char).hexdigest()
+    
+    r_multiline = re.compile("\n{2,}")
+    r_stripspace = re.compile(r"^[ \t]+$", re.MULTILINE)
+    def parse(self, text):
+        self.urls = {}
+        self.titles = {}
+        self.html_blocks = {}
+        self.list_level = 0
+        
+        text = text.replace("\r\n", "\n")
+        text = text.replace("\r", "\n")
+        text += "\n\n"
+        text = self._Detab(text)
+        text = self.r_stripspace.sub("", text)
+        text = self._HashHTMLBlocks(text)
+        text = self._StripLinkDefinitions(text)
+        text = self._RunBlockGamut(text)
+        text = self._UnescapeSpecialChars(text)
+        return text
+    
+    r_StripLinkDefinitions = re.compile(r"""
+    ^[ ]{0,%d}\[(.+)\]:  # id = $1
+      [ \t]*\n?[ \t]*
+    <?(\S+?)>?           # url = $2
+      [ \t]*\n?[ \t]*
+    (?:
+      (?<=\s)            # lookbehind for whitespace
+      [\"\(]             # " is backlashed so it colorizes our code right
+      (.+?)              # title = $3
+      [\"\)]
+      [ \t]*
+    )?                   # title is optional
+    (?:\n+|\Z)
+    """ % (tabwidth-1), re.MULTILINE|re.VERBOSE)
+    def _StripLinkDefinitions(self, text):
+        def replacefunc(matchobj):
+            (t1, t2, t3) = matchobj.groups()
+            #@@ case sensitivity?
+            self.urls[t1.lower()] = self._EncodeAmpsAndAngles(t2)
+            if t3 is not None:
+                self.titles[t1.lower()] = t3.replace('"', '&quot;')
+            return ""
+
+        text = self.r_StripLinkDefinitions.sub(replacefunc, text)
+        return text
+
+    blocktagsb = r"p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|math"
+    blocktagsa = blocktagsb + "|ins|del"
+    
+    r_HashHTMLBlocks1 = re.compile(r"""
+    (            # save in $1
+    ^            # start of line  (with /m)
+    <(%s)        # start tag = $2
+    \b           # word break
+    (.*\n)*?     # any number of lines, minimally matching
+    </\2>        # the matching end tag
+    [ \t]*       # trailing spaces/tabs
+    (?=\n+|$)    # followed by a newline or end of document
+    )
+    """ % blocktagsa, re.MULTILINE | re.VERBOSE)
+
+    r_HashHTMLBlocks2 = re.compile(r"""
+    (            # save in $1
+    ^            # start of line  (with /m)
+    <(%s)        # start tag = $2
+    \b           # word break
+    (.*\n)*?     # any number of lines, minimally matching
+    .*</\2>      # the matching end tag
+    [ \t]*       # trailing spaces/tabs
+    (?=\n+|\Z)   # followed by a newline or end of document
+    )
+    """ % blocktagsb, re.MULTILINE | re.VERBOSE)
+
+    r_HashHR = re.compile(r"""
+    (?:
+    (?<=\n\n)    # Starting after a blank line
+    |            # or
+    \A\n?        # the beginning of the doc
+    )
+    (            # save in $1
+    [ ]{0,%d}
+    <(hr)        # start tag = $2
+    \b           # word break
+    ([^<>])*?    # 
+    /?>          # the matching end tag
+    [ \t]*
+    (?=\n{2,}|\Z)# followed by a blank line or end of document
+    )
+    """ % (tabwidth-1), re.VERBOSE)
+    r_HashComment = re.compile(r"""
+    (?:
+    (?<=\n\n)    # Starting after a blank line
+    |            # or
+    \A\n?        # the beginning of the doc
+    )
+    (            # save in $1
+    [ ]{0,%d}
+    (?: 
+      <!
+      (--.*?--\s*)+
+      >
+    )
+    [ \t]*
+    (?=\n{2,}|\Z)# followed by a blank line or end of document
+    )
+    """ % (tabwidth-1), re.VERBOSE)
+
+    def _HashHTMLBlocks(self, text):
+        def handler(m):
+            key = md5.new(m.group(1)).hexdigest()
+            self.html_blocks[key] = m.group(1)
+            return "\n\n%s\n\n" % key
+
+        text = self.r_HashHTMLBlocks1.sub(handler, text)
+        text = self.r_HashHTMLBlocks2.sub(handler, text)
+        oldtext = text
+        text = self.r_HashHR.sub(handler, text)
+        text = self.r_HashComment.sub(handler, text)
+        return text
+
+    #@@@ wrong!
+    r_hr1 = re.compile(r'^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$', re.M)
+    r_hr2 = re.compile(r'^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$', re.M)
+    r_hr3 = re.compile(r'^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$', re.M)
+	
+    def _RunBlockGamut(self, text):
+        text = self._DoHeaders(text)
+        for x in [self.r_hr1, self.r_hr2, self.r_hr3]:
+            text = x.sub("\n<hr%s\n" % self.emptyelt, text);
+        text = self._DoLists(text)
+        text = self._DoCodeBlocks(text)
+        text = self._DoBlockQuotes(text)
+
+    	# We did this in parse()
+    	# to escape the source
+    	# now it's stuff _we_ made
+    	# so we don't wrap it in <p>s.
+        text = self._HashHTMLBlocks(text)
+        text = self._FormParagraphs(text)
+        return text
+
+    r_NewLine = re.compile(" {2,}\n")
+    def _RunSpanGamut(self, text):
+        text = self._DoCodeSpans(text)
+        text = self._EscapeSpecialChars(text)
+        text = self._DoImages(text)
+        text = self._DoAnchors(text)
+        text = self._DoAutoLinks(text)
+        text = self._EncodeAmpsAndAngles(text)
+        text = self._DoItalicsAndBold(text)
+        text = self.r_NewLine.sub(" <br%s\n" % self.emptyelt, text)
+        return text
+
+    def _EscapeSpecialChars(self, text):
+        tokens = self._TokenizeHTML(text)
+        text = ""
+        for cur_token in tokens:
+            if cur_token[0] == "tag":
+                cur_token[1] = cur_token[1].replace('*', self.escapetable["*"])
+                cur_token[1] = cur_token[1].replace('_', self.escapetable["_"])
+                text += cur_token[1]
+            else:
+                text += self._EncodeBackslashEscapes(cur_token[1])
+        return text
+
+    r_DoAnchors1 = re.compile(
+          r""" (                 # wrap whole match in $1
+                  \[
+                    (.*?)        # link text = $2 
+                    # [for bracket nesting, see below]
+                  \]
+
+                  [ ]?           # one optional space
+                  (?:\n[ ]*)?    # one optional newline followed by spaces
+
+                  \[
+                    (.*?)        # id = $3
+                  \]
+                )
+    """, re.S|re.VERBOSE)
+    r_DoAnchors2 = re.compile(
+          r""" (                   # wrap whole match in $1
+                  \[
+                    (.*?)          # link text = $2
+                  \]
+                  \(               # literal paren
+                        [ \t]*
+                        <?(.+?)>?  # href = $3
+                        [ \t]*
+                        (          # $4
+                          ([\'\"]) # quote char = $5
+                          (.*?)    # Title = $6
+                          \5       # matching quote
+                        )?         # title is optional
+                  \)
+                )
+    """, re.S|re.VERBOSE)
+    def _DoAnchors(self, text): 
+        # We here don't do the same as the perl version, as python's regex
+        # engine gives us no way to match brackets.
+
+        def handler1(m):
+            whole_match = m.group(1)
+            link_text = m.group(2)
+            link_id = m.group(3).lower()
+            if not link_id: link_id = link_text.lower()
+            title = self.titles.get(link_id, None)
+                
+
+            if self.urls.has_key(link_id):
+                url = self.urls[link_id]
+                url = url.replace("*", self.escapetable["*"])
+                url = url.replace("_", self.escapetable["_"])
+                res = '<a href="%s"' % htmlquote(url)
+
+                if title:
+                    title = title.replace("*", self.escapetable["*"])
+                    title = title.replace("_", self.escapetable["_"])
+                    res += ' title="%s"' % htmlquote(title)
+                res += ">%s</a>" % htmlquote(link_text)
+            else:
+                res = whole_match
+            return res
+
+        def handler2(m):
+            whole_match = m.group(1)
+            link_text = m.group(2)
+            url = m.group(3)
+            title = m.group(6)
+
+            url = url.replace("*", self.escapetable["*"])
+            url = url.replace("_", self.escapetable["_"])
+            res = '''<a href="%s"''' % htmlquote(url)
+            
+            if title:
+                title = title.replace('"', '&quot;')
+                title = title.replace("*", self.escapetable["*"])
+                title = title.replace("_", self.escapetable["_"])
+                res += ' title="%s"' % htmlquote(title)
+            res += ">%s</a>" % htmlquote(link_text)
+            return res
+
+        text = self.r_DoAnchors1.sub(handler1, text)
+        text = self.r_DoAnchors2.sub(handler2, text)
+        return text
+
+    r_DoImages1 = re.compile(
+           r""" (                       # wrap whole match in $1
+                  !\[
+                    (.*?)               # alt text = $2
+                  \]
+
+                  [ ]?                  # one optional space
+                  (?:\n[ ]*)?           # one optional newline followed by spaces
+
+                  \[
+                    (.*?)               # id = $3
+                  \]
+
+                )
+    """, re.VERBOSE|re.S)
+
+    r_DoImages2 = re.compile(
+          r""" (                        # wrap whole match in $1
+                  !\[
+                    (.*?)               # alt text = $2
+                  \]
+                  \(                    # literal paren
+                        [ \t]*
+                        <?(\S+?)>?      # src url = $3
+                        [ \t]*
+                        (               # $4
+                        ([\'\"])        # quote char = $5
+                          (.*?)         # title = $6
+                          \5            # matching quote
+                          [ \t]*
+                        )?              # title is optional
+                  \)
+                )
+    """, re.VERBOSE|re.S)
+
+    def _DoImages(self, text):
+        def handler1(m):
+            whole_match = m.group(1)
+            alt_text = m.group(2)
+            link_id = m.group(3).lower()
+
+            if not link_id:
+                link_id = alt_text.lower()
+
+            alt_text = alt_text.replace('"', "&quot;")
+            if self.urls.has_key(link_id):
+                url = self.urls[link_id]
+                url = url.replace("*", self.escapetable["*"])
+                url = url.replace("_", self.escapetable["_"])
+                res = '''<img src="%s" alt="%s"''' % (htmlquote(url), htmlquote(alt_text))
+                if self.titles.has_key(link_id):
+                    title = self.titles[link_id]
+                    title = title.replace("*", self.escapetable["*"])
+                    title = title.replace("_", self.escapetable["_"])
+                    res += ' title="%s"' % htmlquote(title)
+                res += self.emptyelt
+            else:
+                res = whole_match
+            return res
+
+        def handler2(m):
+            whole_match = m.group(1)
+            alt_text = m.group(2)
+            url = m.group(3)
+            title = m.group(6) or ''
+            
+            alt_text = alt_text.replace('"', "&quot;")
+            title = title.replace('"', "&quot;")
+            url = url.replace("*", self.escapetable["*"])
+            url = url.replace("_", self.escapetable["_"])
+            res = '<img src="%s" alt="%s"' % (htmlquote(url), htmlquote(alt_text))
+            if title is not None:
+                title = title.replace("*", self.escapetable["*"])
+                title = title.replace("_", self.escapetable["_"])
+                res += ' title="%s"' % htmlquote(title)
+            res += self.emptyelt
+            return res
+
+        text = self.r_DoImages1.sub(handler1, text)
+        text = self.r_DoImages2.sub(handler2, text)
+        return text
+    
+    r_DoHeaders = re.compile(r"^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+", re.VERBOSE|re.M)
+    def _DoHeaders(self, text):
+        def findheader(text, c, n):
+            textl = text.split('\n')
+            for i in xrange(len(textl)):
+                if i >= len(textl): continue
+                count = textl[i].strip().count(c)
+                if count > 0 and count == len(textl[i].strip()) and textl[i+1].strip() == '' and textl[i-1].strip() != '':
+                    textl = textl[:i] + textl[i+1:]
+                    textl[i-1] = '<h'+n+'>'+self._RunSpanGamut(textl[i-1])+'</h'+n+'>'
+                    textl = textl[:i] + textl[i+1:]
+            text = '\n'.join(textl)
+            return text
+        
+        def handler(m):
+            level = len(m.group(1))
+            header = self._RunSpanGamut(m.group(2))
+            return "<h%s>%s</h%s>\n\n" % (level, header, level)
+
+        text = findheader(text, '=', '1')
+        text = findheader(text, '-', '2')
+        text = self.r_DoHeaders.sub(handler, text)
+        return text
+    
+    rt_l = r"""
+    (
+      (
+        [ ]{0,%d}
+        ([*+-]|\d+[.])
+        [ \t]+
+      )
+      (?:.+?)
+      (
+        \Z
+      |
+        \n{2,}
+        (?=\S)
+        (?![ \t]* ([*+-]|\d+[.])[ \t]+)
+      )
+    )
+    """ % (tabwidth - 1)
+    r_DoLists = re.compile('^'+rt_l, re.M | re.VERBOSE | re.S)
+    r_DoListsTop = re.compile(
+      r'(?:\A\n?|(?<=\n\n))'+rt_l, re.M | re.VERBOSE | re.S)
+    
+    def _DoLists(self, text):
+        def handler(m):
+            list_type = "ol"
+            if m.group(3) in [ "*", "-", "+" ]:
+                list_type = "ul"
+            listn = m.group(1)
+            listn = self.r_multiline.sub("\n\n\n", listn)
+            res = self._ProcessListItems(listn)
+            res = "<%s>\n%s</%s>\n" % (list_type, res, list_type)
+            return res
+            
+        if self.list_level:
+            text = self.r_DoLists.sub(handler, text)
+        else:
+            text = self.r_DoListsTop.sub(handler, text)
+        return text
+
+    r_multiend = re.compile(r"\n{2,}\Z")
+    r_ProcessListItems = re.compile(r"""
+    (\n)?                            # leading line = $1
+    (^[ \t]*)                        # leading whitespace = $2
+    ([*+-]|\d+[.]) [ \t]+            # list marker = $3
+    ((?:.+?)                         # list item text = $4
+    (\n{1,2}))
+    (?= \n* (\Z | \2 ([*+-]|\d+[.]) [ \t]+))
+    """, re.VERBOSE | re.M | re.S)
+
+    def _ProcessListItems(self, text):
+        self.list_level += 1
+        text = self.r_multiend.sub("\n", text)
+        
+        def handler(m):
+            item = m.group(4)
+            leading_line = m.group(1)
+            leading_space = m.group(2)
+
+            if leading_line or self.r_multiline.search(item):
+                item = self._RunBlockGamut(self._Outdent(item))
+            else:
+                item = self._DoLists(self._Outdent(item))
+                if item[-1] == "\n": item = item[:-1] # chomp
+                item = self._RunSpanGamut(item)
+            return "<li>%s</li>\n" % item
+
+        text = self.r_ProcessListItems.sub(handler, text)
+        self.list_level -= 1
+        return text
+    
+    r_DoCodeBlocks = re.compile(r"""
+    (?:\n\n|\A)
+    (                 # $1 = the code block
+    (?:
+    (?:[ ]{%d} | \t)  # Lines must start with a tab or equiv
+    .*\n+
+    )+
+    )
+    ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space/end of doc
+    """ % (tabwidth, tabwidth), re.M | re.VERBOSE)
+    def _DoCodeBlocks(self, text):
+        def handler(m):
+            codeblock = m.group(1)
+            codeblock = self._EncodeCode(self._Outdent(codeblock))
+            codeblock = self._Detab(codeblock)
+            codeblock = codeblock.lstrip("\n")
+            codeblock = codeblock.rstrip()
+            res = "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock
+            return res
+
+        text = self.r_DoCodeBlocks.sub(handler, text)
+        return text
+    r_DoCodeSpans = re.compile(r"""
+    (`+)            # $1 = Opening run of `
+    (.+?)           # $2 = The code block
+    (?<!`)
+    \1              # Matching closer
+    (?!`)
+    """, re.I|re.VERBOSE)
+    def _DoCodeSpans(self, text):
+        def handler(m):
+            c = m.group(2)
+            c = c.strip()
+            c = self._EncodeCode(c)
+            return "<code>%s</code>" % c
+
+        text = self.r_DoCodeSpans.sub(handler, text)
+        return text
+    
+    def _EncodeCode(self, text):
+        text = text.replace("&","&amp;")
+        text = text.replace("<","&lt;")
+        text = text.replace(">","&gt;")
+        for c in "*_{}[]\\":
+            text = text.replace(c, self.escapetable[c])
+        return text
+
+    
+    r_DoBold = re.compile(r"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1", re.VERBOSE | re.S)
+    r_DoItalics = re.compile(r"(\*|_) (?=\S) (.+?) (?<=\S) \1", re.VERBOSE | re.S)
+    def _DoItalicsAndBold(self, text):
+        text = self.r_DoBold.sub(r"<strong>\2</strong>", text)
+        text = self.r_DoItalics.sub(r"<em>\2</em>", text)
+        return text
+    
+    r_start = re.compile(r"^", re.M)
+    r_DoBlockQuotes1 = re.compile(r"^[ \t]*>[ \t]?", re.M)
+    r_DoBlockQuotes2 = re.compile(r"^[ \t]+$", re.M)
+    r_DoBlockQuotes3 = re.compile(r"""
+    (                       # Wrap whole match in $1
+     (
+       ^[ \t]*>[ \t]?       # '>' at the start of a line
+       .+\n                 # rest of the first line
+       (.+\n)*              # subsequent consecutive lines
+       \n*                  # blanks
+      )+
+    )""", re.M | re.VERBOSE)
+    r_protectpre = re.compile(r'(\s*<pre>.+?</pre>)', re.S)
+    r_propre = re.compile(r'^  ', re.M)
+
+    def _DoBlockQuotes(self, text):
+        def prehandler(m):
+            return self.r_propre.sub('', m.group(1))
+                
+        def handler(m):
+            bq = m.group(1)
+            bq = self.r_DoBlockQuotes1.sub("", bq)
+            bq = self.r_DoBlockQuotes2.sub("", bq)
+            bq = self._RunBlockGamut(bq)
+            bq = self.r_start.sub("  ", bq)
+            bq = self.r_protectpre.sub(prehandler, bq)
+            return "<blockquote>\n%s\n</blockquote>\n\n" % bq
+            
+        text = self.r_DoBlockQuotes3.sub(handler, text)
+        return text
+
+    r_tabbed = re.compile(r"^([ \t]*)")
+    def _FormParagraphs(self, text):
+        text = text.strip("\n")
+        grafs = self.r_multiline.split(text)
+
+        for g in xrange(len(grafs)):
+            t = grafs[g].strip() #@@?
+            if not self.html_blocks.has_key(t):
+                t = self._RunSpanGamut(t)
+                t = self.r_tabbed.sub(r"<p>", t)
+                t += "</p>"
+                grafs[g] = t
+
+        for g in xrange(len(grafs)):
+            t = grafs[g].strip()
+            if self.html_blocks.has_key(t):
+                grafs[g] = self.html_blocks[t]
+        
+        return "\n\n".join(grafs)
+
+    r_EncodeAmps = re.compile(r"&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)")
+    r_EncodeAngles = re.compile(r"<(?![a-z/?\$!])")
+    def _EncodeAmpsAndAngles(self, text):
+        text = self.r_EncodeAmps.sub("&amp;", text)
+        text = self.r_EncodeAngles.sub("&lt;", text)
+        return text
+
+    def _EncodeBackslashEscapes(self, text):
+        for char in self.escapechars:
+            text = text.replace("\\" + char, self.escapetable[char])
+        return text
+    
+    r_link = re.compile(r"<((https?|ftp):[^\'\">\s]+)>", re.I)
+    r_email = re.compile(r"""
+      <
+      (?:mailto:)?
+      (
+         [-.\w]+
+         \@
+         [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
+      )
+      >""", re.VERBOSE|re.I)
+    def _DoAutoLinks(self, text):
+        text = self.r_link.sub(r'<a href="\1">\1</a>', text)
+
+        def handler(m):
+            l = m.group(1)
+            return self._EncodeEmailAddress(self._UnescapeSpecialChars(l))
+    
+        text = self.r_email.sub(handler, text)
+        return text
+    
+    r_EncodeEmailAddress = re.compile(r">.+?:")
+    def _EncodeEmailAddress(self, text):
+        encode = [
+            lambda x: "&#%s;" % ord(x),
+            lambda x: "&#x%X;" % ord(x),
+            lambda x: x
+        ]
+
+        text = "mailto:" + text
+        addr = ""
+        for c in text:
+            if c == ':': addr += c; continue
+            
+            r = semirandom(addr)
+            if r < 0.45:
+                addr += encode[1](c)
+            elif r > 0.9 and c != '@':
+                addr += encode[2](c)
+            else:
+                addr += encode[0](c)
+
+        text = '<a href="%s">%s</a>' % (addr, addr)
+        text = self.r_EncodeEmailAddress.sub('>', text)
+        return text
+
+    def _UnescapeSpecialChars(self, text):
+        for key in self.escapetable.keys():
+            text = text.replace(self.escapetable[key], key)
+        return text
+    
+    tokenize_depth = 6
+    tokenize_nested_tags = '|'.join([r'(?:<[a-z/!$](?:[^<>]'] * tokenize_depth) + (')*>)' * tokenize_depth)
+    r_TokenizeHTML = re.compile(
+      r"""(?: <! ( -- .*? -- \s* )+ > ) |  # comment
+          (?: <\? .*? \?> ) |              # processing instruction
+          %s                               # nested tags
+    """ % tokenize_nested_tags, re.I|re.VERBOSE)
+    def _TokenizeHTML(self, text):
+        pos = 0
+        tokens = []
+        matchobj = self.r_TokenizeHTML.search(text, pos)
+        while matchobj:
+            whole_tag = matchobj.string[matchobj.start():matchobj.end()]
+            sec_start = matchobj.end()
+            tag_start = sec_start - len(whole_tag)
+            if pos < tag_start:
+                tokens.append(["text", matchobj.string[pos:tag_start]])
+
+            tokens.append(["tag", whole_tag])
+            pos = sec_start
+            matchobj = self.r_TokenizeHTML.search(text, pos)
+
+        if pos < len(text):
+            tokens.append(["text", text[pos:]])
+        return tokens
+
+    r_Outdent = re.compile(r"""^(\t|[ ]{1,%d})""" % tabwidth, re.M)
+    def _Outdent(self, text):
+        text = self.r_Outdent.sub("", text)
+        return text    
+
+    def _Detab(self, text): return text.expandtabs(self.tabwidth)
+
+def Markdown(*args, **kw): return _Markdown().parse(*args, **kw)
+markdown = Markdown
+
+if __name__ == '__main__':
+    if len(sys.argv) > 1:
+        print Markdown(open(sys.argv[1]).read())
+    else:
+        print Markdown(sys.stdin.read())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/__init__.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+"""web.py: makes web apps (http://webpy.org)"""
+
+from __future__ import generators
+
+__version__ = "0.32"
+__author__ = [
+    "Aaron Swartz <me@aaronsw.com>",
+    "Anand Chitipothu <anandology@gmail.com>"
+]
+__license__ = "public domain"
+__contributors__ = "see http://webpy.org/changes"
+
+import utils, db, net, wsgi, http, webapi, httpserver, debugerror
+import template, form
+
+import session
+
+from utils import *
+from db import *
+from net import *
+from wsgi import *
+from http import *
+from webapi import *
+from httpserver import *
+from debugerror import *
+from application import *
+from browser import *
+import test
+try:
+    import webopenid as openid
+except ImportError:
+    pass # requires openid module
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/application.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,667 @@
+#!/usr/bin/python
+"""
+Web application
+(from web.py)
+"""
+import webapi as web
+import webapi, wsgi, utils
+import debugerror
+from utils import lstrips, safeunicode
+import sys
+
+import urllib
+import traceback
+import itertools
+import os
+import re
+import types
+from exceptions import SystemExit
+
+try:
+    import wsgiref.handlers
+except ImportError:
+    pass # don't break people with old Pythons
+
+__all__ = [
+    "application", "auto_application",
+    "subdir_application", "subdomain_application", 
+    "loadhook", "unloadhook",
+    "autodelegate"
+]
+
+class application:
+    """
+    Application to delegate requests based on path.
+    
+        >>> urls = ("/hello", "hello")
+        >>> app = application(urls, globals())
+        >>> class hello:
+        ...     def GET(self): return "hello"
+        >>>
+        >>> app.request("/hello").data
+        'hello'
+    """
+    def __init__(self, mapping=(), fvars={}, autoreload=None):
+        if autoreload is None:
+            autoreload = web.config.get('debug', False)
+        self.mapping = mapping
+        self.fvars = fvars
+        self.processors = []
+        
+        self.add_processor(loadhook(self._load))
+        self.add_processor(unloadhook(self._unload))
+        
+        if autoreload:
+            def main_module_name():
+                mod = sys.modules['__main__']
+                file = getattr(mod, '__file__', None) # make sure this works even from python interpreter
+                return file and os.path.splitext(os.path.basename(file))[0]
+
+            def modname(fvars):
+                """find name of the module name from fvars."""
+                file, name = fvars.get('__file__'), fvars.get('__name__')
+                if file is None or name is None:
+                    return None
+
+                if name == '__main__':
+                    # Since the __main__ module can't be reloaded, the module has 
+                    # to be imported using its file name.                    
+                    name = main_module_name()
+                return name
+                
+            mapping_name = utils.dictfind(fvars, mapping)
+            module_name = modname(fvars)
+            
+            def reload_mapping():
+                """loadhook to reload mapping and fvars."""
+                mod = __import__(module_name)
+                mapping = getattr(mod, mapping_name, None)
+                if mapping:
+                    self.fvars = mod.__dict__
+                    self.mapping = mapping
+
+            self.add_processor(loadhook(Reloader()))
+            if mapping_name and module_name:
+                self.add_processor(loadhook(reload_mapping))
+
+            # load __main__ module usings its filename, so that it can be reloaded.
+            if main_module_name() and '__main__' in sys.argv:
+                try:
+                    __import__(main_module_name())
+                except ImportError:
+                    pass
+                    
+    def _load(self):
+        web.ctx.app_stack.append(self)
+        
+    def _unload(self):
+        web.ctx.app_stack = web.ctx.app_stack[:-1]
+        
+        if web.ctx.app_stack:
+            # this is a sub-application, revert ctx to earlier state.
+            oldctx = web.ctx.get('_oldctx')
+            if oldctx:
+                web.ctx.home = oldctx.home
+                web.ctx.homepath = oldctx.homepath
+                web.ctx.path = oldctx.path
+                web.ctx.fullpath = oldctx.fullpath
+                
+    def _cleanup(self):
+        #@@@
+        # Since the CherryPy Webserver uses thread pool, the thread-local state is never cleared.
+        # This interferes with the other requests. 
+        # clearing the thread-local storage to avoid that.
+        # see utils.ThreadedDict for details
+        import threading
+        t = threading.currentThread()
+        if hasattr(t, '_d'):
+            del t._d
+    
+    def add_mapping(self, pattern, classname):
+        self.mapping += (pattern, classname)
+        
+    def add_processor(self, processor):
+        """
+        Adds a processor to the application. 
+        
+            >>> urls = ("/(.*)", "echo")
+            >>> app = application(urls, globals())
+            >>> class echo:
+            ...     def GET(self, name): return name
+            ...
+            >>>
+            >>> def hello(handler): return "hello, " +  handler()
+            ...
+            >>> app.add_processor(hello)
+            >>> app.request("/web.py").data
+            'hello, web.py'
+        """
+        self.processors.append(processor)
+
+    def request(self, localpart='/', method='GET', data=None,
+                host="0.0.0.0:8080", headers=None, https=False, **kw):
+        """Makes request to this application for the specified path and method.
+        Response will be a storage object with data, status and headers.
+
+            >>> urls = ("/hello", "hello")
+            >>> app = application(urls, globals())
+            >>> class hello:
+            ...     def GET(self): 
+            ...         web.header('Content-Type', 'text/plain')
+            ...         return "hello"
+            ...
+            >>> response = app.request("/hello")
+            >>> response.data
+            'hello'
+            >>> response.status
+            '200 OK'
+            >>> response.headers['Content-Type']
+            'text/plain'
+
+        To use https, use https=True.
+
+            >>> urls = ("/redirect", "redirect")
+            >>> app = application(urls, globals())
+            >>> class redirect:
+            ...     def GET(self): raise web.seeother("/foo")
+            ...
+            >>> response = app.request("/redirect")
+            >>> response.headers['Location']
+            'http://0.0.0.0:8080/foo'
+            >>> response = app.request("/redirect", https=True)
+            >>> response.headers['Location']
+            'https://0.0.0.0:8080/foo'
+
+        The headers argument specifies HTTP headers as a mapping object
+        such as a dict.
+
+            >>> urls = ('/ua', 'uaprinter')
+            >>> class uaprinter:
+            ...     def GET(self):
+            ...         return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
+            ... 
+            >>> app = application(urls, globals())
+            >>> app.request('/ua', headers = {
+            ...      'User-Agent': 'a small jumping bean/1.0 (compatible)'
+            ... }).data
+            'your user-agent is a small jumping bean/1.0 (compatible)'
+
+        """
+        path, maybe_query = urllib.splitquery(localpart)
+        query = maybe_query or ""
+        
+        if 'env' in kw:
+            env = kw['env']
+        else:
+            env = {}
+        env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
+        headers = headers or {}
+
+        for k, v in headers.items():
+            env['HTTP_' + k.upper().replace('-', '_')] = v
+
+        if 'HTTP_CONTENT_LENGTH' in env:
+            env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
+
+        if 'HTTP_CONTENT_TYPE' in env:
+            env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
+
+        if method in ["POST", "PUT"]:
+            data = data or ''
+            import StringIO
+            if isinstance(data, dict):
+                q = urllib.urlencode(data)
+            else:
+                q = data
+            env['wsgi.input'] = StringIO.StringIO(q)
+            if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
+                env['CONTENT_LENGTH'] = len(q)
+        response = web.storage()
+        def start_response(status, headers):
+            response.status = status
+            response.headers = dict(headers)
+            response.header_items = headers
+        response.data = "".join(self.wsgifunc()(env, start_response))
+        return response
+
+    def browser(self):
+        import browser
+        return browser.AppBrowser(self)
+
+    def handle(self):
+        fn, args = self._match(self.mapping, web.ctx.path)
+        return self._delegate(fn, self.fvars, args)
+        
+    def handle_with_processors(self):
+        def process(processors):
+            try:
+                if processors:
+                    p, processors = processors[0], processors[1:]
+                    return p(lambda: process(processors))
+                else:
+                    return self.handle()
+            except web.HTTPError:
+                raise
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except:
+                print >> web.debug, traceback.format_exc()
+                raise self.internalerror()
+        
+        # processors must be applied in the resvere order. (??)
+        return process(self.processors)
+                        
+    def wsgifunc(self, *middleware):
+        """Returns a WSGI-compatible function for this application."""
+        def peep(iterator):
+            """Peeps into an iterator by doing an iteration
+            and returns an equivalent iterator.
+            """
+            # wsgi requires the headers first
+            # so we need to do an iteration
+            # and save the result for later
+            try:
+                firstchunk = iterator.next()
+            except StopIteration:
+                firstchunk = ''
+
+            return itertools.chain([firstchunk], iterator)    
+                                
+        def is_generator(x): return x and hasattr(x, 'next')
+        
+        def wsgi(env, start_resp):
+            self.load(env)
+            try:
+                # allow uppercase methods only
+                if web.ctx.method.upper() != web.ctx.method:
+                    raise web.nomethod()
+
+                result = self.handle_with_processors()
+                if is_generator(result):
+                    result = peep(result)
+                else:
+                    result = [result]
+            except web.HTTPError, e:
+                result = [e.data]
+
+            result = web.utf8(iter(result))
+
+            status, headers = web.ctx.status, web.ctx.headers
+            start_resp(status, headers)
+            
+            def cleanup():
+                self._cleanup()
+                yield '' # force this function to be a generator
+                            
+            return itertools.chain(result, cleanup())
+
+        for m in middleware: 
+            wsgi = m(wsgi)
+
+        return wsgi
+
+    def run(self, *middleware):
+        """
+        Starts handling requests. If called in a CGI or FastCGI context, it will follow
+        that protocol. If called from the command line, it will start an HTTP
+        server on the port named in the first command line argument, or, if there
+        is no argument, on port 8080.
+        
+        `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
+        function.
+        """
+        return wsgi.runwsgi(self.wsgifunc(*middleware))
+    
+    def cgirun(self, *middleware):
+        """
+        Return a CGI handler. This is mostly useful with Google App Engine.
+        There you can just do:
+        
+            main = app.cgirun()
+        """
+        wsgiapp = self.wsgifunc(*middleware)
+
+        try:
+            from google.appengine.ext.webapp.util import run_wsgi_app
+            return run_wsgi_app(wsgiapp)
+        except ImportError:
+            # we're not running from within Google App Engine
+            return wsgiref.handlers.CGIHandler().run(wsgiapp)
+    
+    def load(self, env):
+        """Initializes ctx using env."""
+        ctx = web.ctx
+        ctx.clear()
+        ctx.status = '200 OK'
+        ctx.headers = []
+        ctx.output = ''
+        ctx.environ = ctx.env = env
+        ctx.host = env.get('HTTP_HOST')
+
+        if env.get('wsgi.url_scheme') in ['http', 'https']:
+            ctx.protocol = env['wsgi.url_scheme']
+        elif env.get('HTTPS', '').lower() in ['on', 'true', '1']:
+            ctx.protocol = 'https'
+        else:
+            ctx.protocol = 'http'
+        ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]')
+        ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
+        ctx.home = ctx.homedomain + ctx.homepath
+        #@@ home is changed when the request is handled to a sub-application.
+        #@@ but the real home is required for doing absolute redirects.
+        ctx.realhome = ctx.home
+        ctx.ip = env.get('REMOTE_ADDR')
+        ctx.method = env.get('REQUEST_METHOD')
+        ctx.path = env.get('PATH_INFO')
+        # http://trac.lighttpd.net/trac/ticket/406 requires:
+        if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
+            ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath)
+            # Apache and CherryPy webservers unquote the url but lighttpd doesn't. 
+            # unquote explicitly for lighttpd to make ctx.path uniform across all servers.
+            ctx.path = urllib.unquote(ctx.path)
+
+        if env.get('QUERY_STRING'):
+            ctx.query = '?' + env.get('QUERY_STRING', '')
+        else:
+            ctx.query = ''
+
+        ctx.fullpath = ctx.path + ctx.query
+        
+        for k, v in ctx.iteritems():
+            if isinstance(v, str):
+                ctx[k] = safeunicode(v)
+
+        # status must always be str
+        ctx.status = '200 OK'
+        
+        ctx.app_stack = []
+
+    def _delegate(self, f, fvars, args=[]):
+        def handle_class(cls):
+            meth = web.ctx.method
+            if meth == 'HEAD' and not hasattr(cls, meth):
+                meth = 'GET'
+            if not hasattr(cls, meth):
+                raise web.nomethod(cls)
+            tocall = getattr(cls(), meth)
+            return tocall(*args)
+            
+        def is_class(o): return isinstance(o, (types.ClassType, type))
+            
+        if f is None:
+            raise web.notfound()
+        elif isinstance(f, application):
+            return f.handle_with_processors()
+        elif is_class(f):
+            return handle_class(f)
+        elif isinstance(f, basestring):
+            if f.startswith('redirect '):
+                url = f.split(' ', 1)[1]
+                if web.ctx.method == "GET":
+                    x = web.ctx.env.get('QUERY_STRING', '')
+                    if x:
+                        url += '?' + x
+                raise web.redirect(url)
+            elif '.' in f:
+                x = f.split('.')
+                mod, cls = '.'.join(x[:-1]), x[-1]
+                mod = __import__(mod, globals(), locals(), [""])
+                cls = getattr(mod, cls)
+            else:
+                cls = fvars[f]
+            return handle_class(cls)
+        elif hasattr(f, '__call__'):
+            return f()
+        else:
+            return web.notfound()
+
+    def _match(self, mapping, value):
+        for pat, what in utils.group(mapping, 2):
+            if isinstance(what, application):
+                if value.startswith(pat):
+                    f = lambda: self._delegate_sub_application(pat, what)
+                    return f, None
+                else:
+                    continue
+            elif isinstance(what, basestring):
+                what, result = utils.re_subm('^' + pat + '$', what, value)
+            else:
+                result = utils.re_compile('^' + pat + '$').match(value)
+                
+            if result: # it's a match
+                return what, [x for x in result.groups()]
+        return None, None
+        
+    def _delegate_sub_application(self, dir, app):
+        """Deletes request to sub application `app` rooted at the directory `dir`.
+        The home, homepath, path and fullpath values in web.ctx are updated to mimic request
+        to the subapp and are restored after it is handled. 
+        
+        @@Any issues with when used with yield?
+        """
+        web.ctx._oldctx = web.storage(web.ctx)
+        web.ctx.home += dir
+        web.ctx.homepath += dir
+        web.ctx.path = web.ctx.path[len(dir):]
+        web.ctx.fullpath = web.ctx.fullpath[len(dir):]
+        return app.handle_with_processors()
+            
+    def get_parent_app(self):
+        if self in web.ctx.app_stack:
+            index = web.ctx.app_stack.index(self)
+            if index > 0:
+                return web.ctx.app_stack[index-1]
+        
+    def notfound(self):
+        """Returns HTTPError with '404 not found' message"""
+        parent = self.get_parent_app()
+        if parent:
+            return parent.notfound()
+        else:
+            return web._NotFound()
+            
+    def internalerror(self):
+        """Returns HTTPError with '500 internal error' message"""
+        parent = self.get_parent_app()
+        if parent:
+            return parent.internalerror()
+        elif web.config.get('debug'):
+            import debugerror
+            return debugerror.debugerror()
+        else:
+            return web._InternalError()
+
+class auto_application(application):
+    """Application similar to `application` but urls are constructed 
+    automatiacally using metaclass.
+
+        >>> app = auto_application()
+        >>> class hello(app.page):
+        ...     def GET(self): return "hello, world"
+        ...
+        >>> class foo(app.page):
+        ...     path = '/foo/.*'
+        ...     def GET(self): return "foo"
+        >>> app.request("/hello").data
+        'hello, world'
+        >>> app.request('/foo/bar').data
+        'foo'
+    """
+    def __init__(self):
+        application.__init__(self)
+
+        class metapage(type):
+            def __init__(klass, name, bases, attrs):
+                type.__init__(klass, name, bases, attrs)
+                path = attrs.get('path', '/' + name)
+
+                # path can be specified as None to ignore that class
+                # typically required to create a abstract base class.
+                if path is not None:
+                    self.add_mapping(path, klass)
+
+        class page:
+            path = None
+            __metaclass__ = metapage
+
+        self.page = page
+
+# The application class already has the required functionality of subdir_application
+subdir_application = application
+                
+class subdomain_application(application):
+    """
+    Application to delegate requests based on the host.
+
+        >>> urls = ("/hello", "hello")
+        >>> app = application(urls, globals())
+        >>> class hello:
+        ...     def GET(self): return "hello"
+        >>>
+        >>> mapping = (r"hello\.example\.com", app)
+        >>> app2 = subdomain_application(mapping)
+        >>> app2.request("/hello", host="hello.example.com").data
+        'hello'
+        >>> response = app2.request("/hello", host="something.example.com")
+        >>> response.status
+        '404 Not Found'
+        >>> response.data
+        'not found'
+    """
+    def handle(self):
+        host = web.ctx.host.split(':')[0] #strip port
+        fn, args = self._match(self.mapping, host)
+        return self._delegate(fn, self.fvars, args)
+        
+    def _match(self, mapping, value):
+        for pat, what in utils.group(mapping, 2):
+            if isinstance(what, basestring):
+                what, result = utils.re_subm('^' + pat + '$', what, value)
+            else:
+                result = utils.re_compile('^' + pat + '$').match(value)
+
+            if result: # it's a match
+                return what, [x for x in result.groups()]
+        return None, None
+        
+def loadhook(h):
+    """
+    Converts a load hook into an application processor.
+    
+        >>> app = auto_application()
+        >>> def f(): "something done before handling request"
+        ...
+        >>> app.add_processor(loadhook(f))
+    """
+    def processor(handler):
+        h()
+        return handler()
+        
+    return processor
+    
+def unloadhook(h):
+    """
+    Converts an unload hook into an application processor.
+    
+        >>> app = auto_application()
+        >>> def f(): "something done after handling request"
+        ...
+        >>> app.add_processor(unloadhook(f))    
+    """
+    def processor(handler):
+        try:
+            result = handler()
+            is_generator = result and hasattr(result, 'next')
+        except:
+            # run the hook even when handler raises some exception
+            h()
+            raise
+
+        if is_generator:
+            return wrap(result)
+        else:
+            h()
+            return result
+            
+    def wrap(result):
+        def next():
+            try:
+                return result.next()
+            except:
+                # call the hook at the and of iterator
+                h()
+                raise
+
+        result = iter(result)
+        while True:
+            yield next()
+            
+    return processor
+
+def autodelegate(prefix=''):
+    """
+    Returns a method that takes one argument and calls the method named prefix+arg,
+    calling `notfound()` if there isn't one. Example:
+
+        urls = ('/prefs/(.*)', 'prefs')
+
+        class prefs:
+            GET = autodelegate('GET_')
+            def GET_password(self): pass
+            def GET_privacy(self): pass
+
+    `GET_password` would get called for `/prefs/password` while `GET_privacy` for 
+    `GET_privacy` gets called for `/prefs/privacy`.
+    
+    If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
+    is called.
+    """
+    def internal(self, arg):
+        if '/' in arg:
+            first, rest = arg.split('/', 1)
+            func = prefix + first
+            args = ['/' + rest]
+        else:
+            func = prefix + arg
+            args = []
+        
+        if hasattr(self, func):
+            try:
+                return getattr(self, func)(*args)
+            except TypeError:
+                return web.notfound()
+        else:
+            return web.notfound()
+    return internal
+
+class Reloader:
+    """Checks to see if any loaded modules have changed on disk and, 
+    if so, reloads them.
+    """
+    def __init__(self):
+        self.mtimes = {}
+
+    def __call__(self):
+        for mod in sys.modules.values():
+            self.check(mod)
+            
+    def check(self, mod):
+        try: 
+            mtime = os.stat(mod.__file__).st_mtime
+        except (AttributeError, OSError, IOError):
+            return
+        if mod.__file__.endswith('.pyc') and os.path.exists(mod.__file__[:-1]):
+            mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
+            
+        if mod not in self.mtimes:
+            self.mtimes[mod] = mtime
+        elif self.mtimes[mod] < mtime:
+            try: 
+                reload(mod)
+                self.mtimes[mod] = mtime
+            except ImportError: 
+                pass
+                
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/browser.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,236 @@
+"""Browser to test web applications.
+(from web.py)
+"""
+from utils import re_compile
+from net import htmlunquote
+
+import httplib, urllib, urllib2
+import copy
+from StringIO import StringIO
+
+DEBUG = False
+
+__all__ = [
+    "BrowserError",
+    "Browser", "AppBrowser",
+    "AppHandler"
+]
+
+class BrowserError(Exception):
+    pass
+
+class Browser:
+    def __init__(self):
+        import cookielib
+        self.cookiejar = cookielib.CookieJar()
+        self._cookie_processor = urllib2.HTTPCookieProcessor(self.cookiejar)
+        self.form = None
+
+        self.url = "http://0.0.0.0:8080/"
+        self.path = "/"
+        
+        self.status = None
+        self.data = None
+        self._response = None
+        self._forms = None
+
+    def reset(self):
+        """Clears all cookies and history."""
+        self.cookiejar.clear()
+
+    def build_opener(self):
+        """Builds the opener using urllib2.build_opener. 
+        Subclasses can override this function to prodive custom openers.
+        """
+        return urllib2.build_opener()
+
+    def do_request(self, req):
+        if DEBUG:
+            print 'requesting', req.get_method(), req.get_full_url()
+        opener = self.build_opener()
+        opener.add_handler(self._cookie_processor)
+        try:
+            self._response = opener.open(req)
+        except urllib2.HTTPError, e:
+            self._response = e
+
+        self.url = self._response.geturl()
+        self.path = urllib2.Request(self.url).get_selector()
+        self.data = self._response.read()
+        self.status = self._response.code
+        self._forms = None
+        self.form = None
+        return self.get_response()
+
+    def open(self, url, data=None, headers={}):
+        """Opens the specified url."""
+        url = urllib.basejoin(self.url, url)
+        req = urllib2.Request(url, data, headers)
+        return self.do_request(req)
+
+    def show(self):
+        """Opens the current page in real web browser."""
+        f = open('page.html', 'w')
+        f.write(self.data)
+        f.close()
+
+        import webbrowser, os
+        url = 'file://' + os.path.abspath('page.html')
+        webbrowser.open(url)
+
+    def get_response(self):
+        """Returns a copy of the current response."""
+        return urllib.addinfourl(StringIO(self.data), self._response.info(), self._response.geturl())
+
+    def get_soup(self):
+        """Returns beautiful soup of the current document."""
+        import BeautifulSoup
+        return BeautifulSoup.BeautifulSoup(self.data)
+
+    def get_text(self, e=None):
+        """Returns content of e or the current document as plain text."""
+        e = e or self.get_soup()
+        return ''.join([htmlunquote(c) for c in e.recursiveChildGenerator() if isinstance(c, unicode)])
+
+    def _get_links(self):
+        soup = self.get_soup()
+        return [a for a in soup.findAll(name='a')]
+        
+    def get_links(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
+        """Returns all links in the document."""
+        return self._filter_links(self._get_links(),
+            text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
+
+    def follow_link(self, link=None, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
+        if link is None:
+            links = self._filter_links(self.get_links(),
+                text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
+            link = links and links[0]
+            
+        if link:
+            return self.open(link['href'])
+        else:
+            raise BrowserError("No link found")
+            
+    def find_link(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None):
+        links = self._filter_links(self.get_links(), 
+            text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate)
+        return links and links[0] or None
+            
+    def _filter_links(self, links, 
+            text=None, text_regex=None,
+            url=None, url_regex=None,
+            predicate=None):
+        predicates = []
+        if text is not None:
+            predicates.append(lambda link: link.string == text)
+        if text_regex is not None:
+            predicates.append(lambda link: re_compile(text_regex).search(link.string or ''))
+        if url is not None:
+            predicates.append(lambda link: link.get('href') == url)
+        if url_regex is not None:
+            predicates.append(lambda link: re_compile(url_regex).search(link.get('href', '')))
+        if predicate:
+            predicate.append(predicate)
+
+        def f(link):
+            for p in predicates:
+                if not p(link):
+                    return False
+            return True
+
+        return [link for link in links if f(link)]
+
+    def get_forms(self):
+        """Returns all forms in the current document.
+        The returned form objects implement the ClientForm.HTMLForm interface.
+        """
+        if self._forms is None:
+            import ClientForm
+            self._forms = ClientForm.ParseResponse(self.get_response(), backwards_compat=False)
+        return self._forms
+
+    def select_form(self, name=None, predicate=None, index=0):
+        """Selects the specified form."""
+        forms = self.get_forms()
+
+        if name is not None:
+            forms = [f for f in forms if f.name == name]
+        if predicate:
+            forms = [f for f in forms if predicate(f)]
+            
+        if forms:
+            self.form = forms[index]
+            return self.form
+        else:
+            raise BrowserError("No form selected.")
+        
+    def submit(self, **kw):
+        """submits the currently selected form."""
+        if self.form is None:
+            raise BrowserError("No form selected.")
+        req = self.form.click(**kw)
+        return self.do_request(req)
+
+    def __getitem__(self, key):
+        return self.form[key]
+
+    def __setitem__(self, key, value):
+        self.form[key] = value
+
+class AppBrowser(Browser):
+    """Browser interface to test web.py apps.
+    
+        b = AppBrowser(app)
+        b.open('/')
+        b.follow_link(text='Login')
+        
+        b.select_form(name='login')
+        b['username'] = 'joe'
+        b['password'] = 'secret'
+        b.submit()
+
+        assert b.path == '/'
+        assert 'Welcome joe' in b.get_text()
+    """
+    def __init__(self, app):
+        Browser.__init__(self)
+        self.app = app
+
+    def build_opener(self):
+        return urllib2.build_opener(AppHandler(self.app))
+
+class AppHandler(urllib2.HTTPHandler):
+    """urllib2 handler to handle requests using web.py application."""
+    handler_order = 100
+
+    def __init__(self, app):
+        self.app = app
+
+    def http_open(self, req):
+        result = self.app.request(
+            localpart=req.get_selector(),
+            method=req.get_method(),
+            host=req.get_host(),
+            data=req.get_data(),
+            headers=dict(req.header_items()),
+            https=req.get_type() == "https"
+        )
+        return self._make_response(result, req.get_full_url())
+
+    def https_open(self, req):
+        return self.http_open(req)
+    
+    try:
+        https_request = urllib2.HTTPHandler.do_request_
+    except AttributeError:
+        # for python 2.3
+        pass
+
+    def _make_response(self, result, url):
+        data = "\r\n".join(["%s: %s" % (k, v) for k, v in result.header_items])
+        headers = httplib.HTTPMessage(StringIO(data))
+        response = urllib.addinfourl(StringIO(result.data), headers, url)
+        code, msg = result.status.split(None, 1)
+        response.code, response.msg = int(code), msg
+        return response
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/contrib/template.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,131 @@
+"""
+Interface to various templating engines.
+"""
+import os.path
+
+__all__ = [
+    "render_cheetah", "render_genshi", "render_mako",
+    "cache", 
+]
+
+class render_cheetah:
+    """Rendering interface to Cheetah Templates.
+
+    Example:
+
+        render = render_cheetah('templates')
+        render.hello(name="cheetah")
+    """
+    def __init__(self, path):
+        # give error if Chetah is not installed
+        from Cheetah.Template import Template
+        self.path = path
+
+    def __getattr__(self, name):
+        from Cheetah.Template import Template
+        path = os.path.join(self.path, name + ".html")
+        
+        def template(**kw):
+            t = Template(file=path, searchList=[kw])
+            return t.respond()
+
+        return template
+    
+class render_genshi:
+    """Rendering interface genshi templates.
+    Example:
+
+    for xml/html templates.
+
+        render = render_genshi(['templates/'])
+        render.hello(name='genshi')
+
+    For text templates:
+
+        render = render_genshi(['templates/'], type='text')
+        render.hello(name='genshi')
+    """
+
+    def __init__(self, *a, **kwargs):
+        from genshi.template import TemplateLoader
+
+        self._type = kwargs.pop('type', None)
+        self._loader = TemplateLoader(*a, **kwargs)
+
+    def __getattr__(self, name):
+        # Assuming all templates are html
+        path = name + ".html"
+
+        if self._type == "text":
+            from genshi.template import TextTemplate
+            cls = TextTemplate
+            type = "text"
+        else:
+            cls = None
+            type = None
+
+        t = self._loader.load(path, cls=cls)
+        def template(**kw):
+            stream = t.generate(**kw)
+            if type:
+                return stream.render(type)
+            else:
+                return stream.render()
+        return template
+
+class render_jinja:
+    """Rendering interface to Jinja2 Templates
+    
+    Example:
+
+        render= render_jinja('templates')
+        render.hello(name='jinja2')
+    """
+    def __init__(self, *a, **kwargs):
+        extensions = kwargs.pop('extensions', [])
+        globals = kwargs.pop('globals', {})
+
+        from jinja2 import Environment,FileSystemLoader
+        self._lookup = Environment(loader=FileSystemLoader(*a, **kwargs), extensions=extensions)
+        self._lookup.globals.update(globals)
+        
+    def __getattr__(self, name):
+        # Assuming all templates end with .html
+        path = name + '.html'
+        t = self._lookup.get_template(path)
+        return t.render
+        
+class render_mako:
+    """Rendering interface to Mako Templates.
+
+    Example:
+
+        render = render_mako(directories=['templates'])
+        render.hello(name="mako")
+    """
+    def __init__(self, *a, **kwargs):
+        from mako.lookup import TemplateLookup
+        self._lookup = TemplateLookup(*a, **kwargs)
+
+    def __getattr__(self, name):
+        # Assuming all templates are html
+        path = name + ".html"
+        t = self._lookup.get_template(path)
+        return t.render
+
+class cache:
+    """Cache for any rendering interface.
+    
+    Example:
+
+        render = cache(render_cheetah("templates/"))
+        render.hello(name='cache')
+    """
+    def __init__(self, render):
+        self._render = render
+        self._cache = {}
+
+    def __getattr__(self, name):
+        if name not in self._cache:
+            self._cache[name] = getattr(self._render, name)
+        return self._cache[name]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/db.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,1137 @@
+"""
+Database API
+(part of web.py)
+"""
+
+__all__ = [
+  "UnknownParamstyle", "UnknownDB", "TransactionError", 
+  "sqllist", "sqlors", "reparam", "sqlquote",
+  "SQLQuery", "SQLParam", "sqlparam",
+  "SQLLiteral", "sqlliteral",
+  "database", 'DB',
+]
+
+import time
+try:
+    import datetime
+except ImportError:
+    datetime = None
+
+from utils import threadeddict, storage, iters, iterbetter
+
+try:
+    # db module can work independent of web.py
+    from webapi import debug, config
+except:
+    import sys
+    debug = sys.stderr
+    config = storage()
+
+class UnknownDB(Exception):
+    """raised for unsupported dbms"""
+    pass
+
+class _ItplError(ValueError): 
+    def __init__(self, text, pos):
+        ValueError.__init__(self)
+        self.text = text
+        self.pos = pos
+    def __str__(self):
+        return "unfinished expression in %s at char %d" % (
+            repr(self.text), self.pos)
+
+class TransactionError(Exception): pass
+
+class UnknownParamstyle(Exception): 
+    """
+    raised for unsupported db paramstyles
+
+    (currently supported: qmark, numeric, format, pyformat)
+    """
+    pass
+    
+class SQLParam:
+    """
+    Parameter in SQLQuery.
+    
+        >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam("joe")])
+        >>> q
+        <sql: "SELECT * FROM test WHERE name='joe'">
+        >>> q.query()
+        'SELECT * FROM test WHERE name=%s'
+        >>> q.values()
+        ['joe']
+    """
+    def __init__(self, value):
+        self.value = value
+        
+    def get_marker(self, paramstyle='pyformat'):
+        if paramstyle == 'qmark':
+            return '?'
+        elif paramstyle == 'numeric':
+            return ':1'
+        elif paramstyle is None or paramstyle in ['format', 'pyformat']:
+            return '%s'
+        raise UnknownParamstyle, paramstyle
+        
+    def sqlquery(self): 
+        return SQLQuery([self])
+        
+    def __add__(self, other):
+        return self.sqlquery() + other
+        
+    def __radd__(self, other):
+        return other + self.sqlquery() 
+            
+    def __str__(self): 
+        return str(self.value)
+    
+    def __repr__(self):
+        return '<param: %s>' % repr(self.value)
+
+sqlparam =  SQLParam
+
+class SQLQuery:
+    """
+    You can pass this sort of thing as a clause in any db function.
+    Otherwise, you can pass a dictionary to the keyword argument `vars`
+    and the function will call reparam for you.
+
+    Internally, consists of `items`, which is a list of strings and
+    SQLParams, which get concatenated to produce the actual query.
+    """
+    # tested in sqlquote's docstring
+    def __init__(self, items=[]):
+        """Creates a new SQLQuery.
+        
+            >>> SQLQuery("x")
+            <sql: 'x'>
+            >>> q = SQLQuery(['SELECT * FROM ', 'test', ' WHERE x=', SQLParam(1)])
+            >>> q
+            <sql: 'SELECT * FROM test WHERE x=1'>
+            >>> q.query(), q.values()
+            ('SELECT * FROM test WHERE x=%s', [1])
+            >>> SQLQuery(SQLParam(1))
+            <sql: '1'>
+        """
+        if isinstance(items, list):
+            self.items = items
+        elif isinstance(items, SQLParam):
+            self.items = [items]
+        elif isinstance(items, SQLQuery):
+            self.items = list(items.items)
+        else:
+            self.items = [str(items)]
+            
+        # Take care of SQLLiterals
+        for i, item in enumerate(self.items):
+            if isinstance(item, SQLParam) and isinstance(item.value, SQLLiteral):
+                self.items[i] = item.value.v
+
+    def __add__(self, other):
+        if isinstance(other, basestring):
+            items = [other]
+        elif isinstance(other, SQLQuery):
+            items = other.items
+        else:
+            return NotImplemented
+        return SQLQuery(self.items + items)
+
+    def __radd__(self, other):
+        if isinstance(other, basestring):
+            items = [other]
+        else:
+            return NotImplemented
+            
+        return SQLQuery(items + self.items)
+
+    def __iadd__(self, other):
+        if isinstance(other, basestring):
+            items = [other]
+        elif isinstance(other, SQLQuery):
+            items = other.items
+        else:
+            return NotImplemented
+        self.items.extend(items)
+        return self
+
+    def __len__(self):
+        return len(self.query())
+        
+    def query(self, paramstyle=None):
+        """
+        Returns the query part of the sql query.
+            >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')])
+            >>> q.query()
+            'SELECT * FROM test WHERE name=%s'
+            >>> q.query(paramstyle='qmark')
+            'SELECT * FROM test WHERE name=?'
+        """
+        s = ''
+        for x in self.items:
+            if isinstance(x, SQLParam):
+                x = x.get_marker(paramstyle)
+            s += x
+        return s
+    
+    def values(self):
+        """
+        Returns the values of the parameters used in the sql query.
+            >>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')])
+            >>> q.values()
+            ['joe']
+        """
+        return [i.value for i in self.items if isinstance(i, SQLParam)]
+        
+    def join(items, sep=' '):
+        """
+        Joins multiple queries.
+        
+        >>> SQLQuery.join(['a', 'b'], ', ')
+        <sql: 'a, b'>
+        """
+        if len(items) == 0:
+            return SQLQuery("")
+
+        q = SQLQuery(items[0])
+        for item in items[1:]:
+            q += sep
+            q += item
+        return q
+    
+    join = staticmethod(join)
+
+    def __str__(self):
+        try:
+            return self.query() % tuple([sqlify(x) for x in self.values()])
+        except (ValueError, TypeError):
+            return self.query()
+
+    def __repr__(self):
+        return '<sql: %s>' % repr(str(self))
+
+class SQLLiteral: 
+    """
+    Protects a string from `sqlquote`.
+
+        >>> sqlquote('NOW()')
+        <sql: "'NOW()'">
+        >>> sqlquote(SQLLiteral('NOW()'))
+        <sql: 'NOW()'>
+    """
+    def __init__(self, v): 
+        self.v = v
+
+    def __repr__(self): 
+        return self.v
+
+sqlliteral = SQLLiteral
+
+def _sqllist(values):
+    """
+        >>> _sqllist([1, 2, 3])
+        <sql: '(1, 2, 3)'>
+    """
+    items = []
+    items.append('(')
+    for i, v in enumerate(values):
+        if i != 0:
+            items.append(', ')
+        items.append(sqlparam(v))
+    items.append(')')
+    return SQLQuery(items)
+
+def reparam(string_, dictionary): 
+    """
+    Takes a string and a dictionary and interpolates the string
+    using values from the dictionary. Returns an `SQLQuery` for the result.
+
+        >>> reparam("s = $s", dict(s=True))
+        <sql: "s = 't'">
+        >>> reparam("s IN $s", dict(s=[1, 2]))
+        <sql: 's IN (1, 2)'>
+    """
+    dictionary = dictionary.copy() # eval mucks with it
+    vals = []
+    result = []
+    for live, chunk in _interpolate(string_):
+        if live:
+            v = eval(chunk, dictionary)
+            result.append(sqlquote(v))
+        else: 
+            result.append(chunk)
+    return SQLQuery.join(result, '')
+
+def sqlify(obj): 
+    """
+    converts `obj` to its proper SQL version
+
+        >>> sqlify(None)
+        'NULL'
+        >>> sqlify(True)
+        "'t'"
+        >>> sqlify(3)
+        '3'
+    """
+    # because `1 == True and hash(1) == hash(True)`
+    # we have to do this the hard way...
+
+    if obj is None:
+        return 'NULL'
+    elif obj is True:
+        return "'t'"
+    elif obj is False:
+        return "'f'"
+    elif datetime and isinstance(obj, datetime.datetime):
+        return repr(obj.isoformat())
+    else:
+        return repr(obj)
+
+def sqllist(lst): 
+    """
+    Converts the arguments for use in something like a WHERE clause.
+    
+        >>> sqllist(['a', 'b'])
+        'a, b'
+        >>> sqllist('a')
+        'a'
+        >>> sqllist(u'abc')
+        u'abc'
+    """
+    if isinstance(lst, basestring): 
+        return lst
+    else:
+        return ', '.join(lst)
+
+def sqlors(left, lst):
+    """
+    `left is a SQL clause like `tablename.arg = ` 
+    and `lst` is a list of values. Returns a reparam-style
+    pair featuring the SQL that ORs together the clause
+    for each item in the lst.
+
+        >>> sqlors('foo = ', [])
+        <sql: '1=2'>
+        >>> sqlors('foo = ', [1])
+        <sql: 'foo = 1'>
+        >>> sqlors('foo = ', 1)
+        <sql: 'foo = 1'>
+        >>> sqlors('foo = ', [1,2,3])
+        <sql: '(foo = 1 OR foo = 2 OR foo = 3 OR 1=2)'>
+    """
+    if isinstance(lst, iters):
+        lst = list(lst)
+        ln = len(lst)
+        if ln == 0:
+            return SQLQuery("1=2")
+        if ln == 1:
+            lst = lst[0]
+
+    if isinstance(lst, iters):
+        return SQLQuery(['('] + 
+          sum([[left, sqlparam(x), ' OR '] for x in lst], []) +
+          ['1=2)']
+        )
+    else:
+        return left + sqlparam(lst)
+        
+def sqlwhere(dictionary, grouping=' AND '): 
+    """
+    Converts a `dictionary` to an SQL WHERE clause `SQLQuery`.
+    
+        >>> sqlwhere({'cust_id': 2, 'order_id':3})
+        <sql: 'order_id = 3 AND cust_id = 2'>
+        >>> sqlwhere({'cust_id': 2, 'order_id':3}, grouping=', ')
+        <sql: 'order_id = 3, cust_id = 2'>
+        >>> sqlwhere({'a': 'a', 'b': 'b'}).query()
+        'a = %s AND b = %s'
+    """
+    return SQLQuery.join([k + ' = ' + sqlparam(v) for k, v in dictionary.items()], grouping)
+
+def sqlquote(a): 
+    """
+    Ensures `a` is quoted properly for use in a SQL query.
+
+        >>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3)
+        <sql: "WHERE x = 't' AND y = 3">
+        >>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote([2, 3])
+        <sql: "WHERE x = 't' AND y IN (2, 3)">
+    """
+    if isinstance(a, list):
+        return _sqllist(a)
+    else:
+        return sqlparam(a).sqlquery()
+
+class Transaction:
+    """Database transaction."""
+    def __init__(self, ctx):
+        self.ctx = ctx
+        self.transaction_count = transaction_count = len(ctx.transactions)
+
+        class transaction_engine:
+            """Transaction Engine used in top level transactions."""
+            def do_transact(self):
+                ctx.commit(unload=False)
+
+            def do_commit(self):
+                ctx.commit()
+
+            def do_rollback(self):
+                ctx.rollback()
+
+        class subtransaction_engine:
+            """Transaction Engine used in sub transactions."""
+            def query(self, q):
+                db_cursor = ctx.db.cursor()
+                ctx.db_execute(db_cursor, SQLQuery(q % transaction_count))
+
+            def do_transact(self):
+                self.query('SAVEPOINT webpy_sp_%s')
+
+            def do_commit(self):
+                self.query('RELEASE SAVEPOINT webpy_sp_%s')
+
+            def do_rollback(self):
+                self.query('ROLLBACK TO SAVEPOINT webpy_sp_%s')
+
+        class dummy_engine:
+            """Transaction Engine used instead of subtransaction_engine 
+            when sub transactions are not supported."""
+            do_transact = do_commit = do_rollback = lambda self: None
+
+        if self.transaction_count:
+            # nested transactions are not supported in some databases
+            if self.ctx.get('ignore_nested_transactions'):
+                self.engine = dummy_engine()
+            else:
+                self.engine = subtransaction_engine()
+        else:
+            self.engine = transaction_engine()
+
+        self.engine.do_transact()
+        self.ctx.transactions.append(self)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exctype, excvalue, traceback):
+        if exctype is not None:
+            self.rollback()
+        else:
+            self.commit()
+
+    def commit(self):
+        if len(self.ctx.transactions) > self.transaction_count:
+            self.engine.do_commit()
+            self.ctx.transactions = self.ctx.transactions[:self.transaction_count]
+
+    def rollback(self):
+        if len(self.ctx.transactions) > self.transaction_count:
+            self.engine.do_rollback()
+            self.ctx.transactions = self.ctx.transactions[:self.transaction_count]
+
+class DB: 
+    """Database"""
+    def __init__(self, db_module, keywords):
+        """Creates a database.
+        """
+        # some DB implementaions take optional paramater `driver` to use a specific driver modue
+        # but it should not be passed to connect
+        keywords.pop('driver', None)
+
+        self.db_module = db_module
+        self.keywords = keywords
+
+        
+        self._ctx = threadeddict()
+        # flag to enable/disable printing queries
+        self.printing = config.get('debug', False)
+        self.supports_multiple_insert = False
+        
+        try:
+            import DBUtils
+            # enable pooling if DBUtils module is available.
+            self.has_pooling = True
+        except ImportError:
+            self.has_pooling = False
+            
+        # Pooling can be disabled by passing pooling=False in the keywords.
+        self.has_pooling = self.keywords.pop('pooling', True) and self.has_pooling
+            
+    def _getctx(self): 
+        if not self._ctx.get('db'):
+            self._load_context(self._ctx)
+        return self._ctx
+    ctx = property(_getctx)
+    
+    def _load_context(self, ctx):
+        ctx.dbq_count = 0
+        ctx.transactions = [] # stack of transactions
+        
+        if self.has_pooling:
+            ctx.db = self._connect_with_pooling(self.keywords)
+        else:
+            ctx.db = self._connect(self.keywords)
+        ctx.db_execute = self._db_execute
+        
+        if not hasattr(ctx.db, 'commit'):
+            ctx.db.commit = lambda: None
+
+        if not hasattr(ctx.db, 'rollback'):
+            ctx.db.rollback = lambda: None
+            
+        def commit(unload=True):
+            # do db commit and release the connection if pooling is enabled.            
+            ctx.db.commit()
+            if unload and self.has_pooling:
+                self._unload_context(self._ctx)
+                
+        def rollback():
+            # do db rollback and release the connection if pooling is enabled.
+            ctx.db.rollback()
+            if self.has_pooling:
+                self._unload_context(self._ctx)
+                
+        ctx.commit = commit
+        ctx.rollback = rollback
+            
+    def _unload_context(self, ctx):
+        del ctx.db
+            
+    def _connect(self, keywords):
+        return self.db_module.connect(**keywords)
+        
+    def _connect_with_pooling(self, keywords):
+        def get_pooled_db():
+            from DBUtils import PooledDB
+
+            # In DBUtils 0.9.3, `dbapi` argument is renamed as `creator`
+            # see Bug#122112
+            
+            if PooledDB.__version__.split('.') < '0.9.3'.split('.'):
+                return PooledDB.PooledDB(dbapi=self.db_module, **keywords)
+            else:
+                return PooledDB.PooledDB(creator=self.db_module, **keywords)
+        
+        if getattr(self, '_pooleddb', None) is None:
+            self._pooleddb = get_pooled_db()
+        
+        return self._pooleddb.connection()
+        
+    def _db_cursor(self):
+        return self.ctx.db.cursor()
+
+    def _param_marker(self):
+        """Returns parameter marker based on paramstyle attribute if this database."""
+        style = getattr(self, 'paramstyle', 'pyformat')
+
+        if style == 'qmark':
+            return '?'
+        elif style == 'numeric':
+            return ':1'
+        elif style in ['format', 'pyformat']:
+            return '%s'
+        raise UnknownParamstyle, style
+
+    def _db_execute(self, cur, sql_query): 
+        """executes an sql query"""
+        self.ctx.dbq_count += 1
+        
+        try:
+            a = time.time()
+            paramstyle = getattr(self, 'paramstyle', 'pyformat')
+            out = cur.execute(sql_query.query(paramstyle), sql_query.values())
+            b = time.time()
+        except:
+            if self.printing:
+                print >> debug, 'ERR:', str(sql_query)
+            if self.ctx.transactions:
+                self.ctx.transactions[-1].rollback()
+            else:
+                self.ctx.rollback()
+            raise
+
+        if self.printing:
+            print >> debug, '%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query))
+        return out
+    
+    def _where(self, where, vars): 
+        if isinstance(where, (int, long)):
+            where = "id = " + sqlparam(where)
+        #@@@ for backward-compatibility
+        elif isinstance(where, (list, tuple)) and len(where) == 2:
+            where = SQLQuery(where[0], where[1])
+        elif isinstance(where, SQLQuery):
+            pass
+        else:
+            where = reparam(where, vars)        
+        return where
+    
+    def query(self, sql_query, vars=None, processed=False, _test=False): 
+        """
+        Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
+        If `processed=True`, `vars` is a `reparam`-style list to use 
+        instead of interpolating.
+        
+            >>> db = DB(None, {})
+            >>> db.query("SELECT * FROM foo", _test=True)
+            <sql: 'SELECT * FROM foo'>
+            >>> db.query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True)
+            <sql: "SELECT * FROM foo WHERE x = 'f'">
+            >>> db.query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True)
+            <sql: "SELECT * FROM foo WHERE x = 'f'">
+        """
+        if vars is None: vars = {}
+        
+        if not processed and not isinstance(sql_query, SQLQuery):
+            sql_query = reparam(sql_query, vars)
+        
+        if _test: return sql_query
+        
+        db_cursor = self._db_cursor()
+        self._db_execute(db_cursor, sql_query)
+        
+        if db_cursor.description:
+            names = [x[0] for x in db_cursor.description]
+            def iterwrapper():
+                row = db_cursor.fetchone()
+                while row:
+                    yield storage(dict(zip(names, row)))
+                    row = db_cursor.fetchone()
+            out = iterbetter(iterwrapper())
+            out.__len__ = lambda: int(db_cursor.rowcount)
+            out.list = lambda: [storage(dict(zip(names, x))) \
+                               for x in db_cursor.fetchall()]
+        else:
+            out = db_cursor.rowcount
+        
+        if not self.ctx.transactions: 
+            self.ctx.commit()
+        return out
+    
+    def select(self, tables, vars=None, what='*', where=None, order=None, group=None, 
+               limit=None, offset=None, _test=False): 
+        """
+        Selects `what` from `tables` with clauses `where`, `order`, 
+        `group`, `limit`, and `offset`. Uses vars to interpolate. 
+        Otherwise, each clause can be a SQLQuery.
+        
+            >>> db = DB(None, {})
+            >>> db.select('foo', _test=True)
+            <sql: 'SELECT * FROM foo'>
+            >>> db.select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True)
+            <sql: 'SELECT * FROM foo, bar WHERE foo.bar_id = bar.id LIMIT 5'>
+        """
+        if vars is None: vars = {}
+        sql_clauses = self.sql_clauses(what, tables, where, group, order, limit, offset)
+        clauses = [self.gen_clause(sql, val, vars) for sql, val in sql_clauses if val is not None]
+        qout = SQLQuery.join(clauses)
+        if _test: return qout
+        return self.query(qout, processed=True)
+    
+    def where(self, table, what='*', order=None, group=None, limit=None, 
+              offset=None, _test=False, **kwargs):
+        """
+        Selects from `table` where keys are equal to values in `kwargs`.
+        
+            >>> db = DB(None, {})
+            >>> db.where('foo', bar_id=3, _test=True)
+            <sql: 'SELECT * FROM foo WHERE bar_id = 3'>
+            >>> db.where('foo', source=2, crust='dewey', _test=True)
+            <sql: "SELECT * FROM foo WHERE source = 2 AND crust = 'dewey'">
+        """
+        where = []
+        for k, v in kwargs.iteritems():
+            where.append(k + ' = ' + sqlquote(v))
+        return self.select(table, what=what, order=order, 
+               group=group, limit=limit, offset=offset, _test=_test, 
+               where=SQLQuery.join(where, ' AND '))
+    
+    def sql_clauses(self, what, tables, where, group, order, limit, offset): 
+        return (
+            ('SELECT', what),
+            ('FROM', sqllist(tables)),
+            ('WHERE', where),
+            ('GROUP BY', group),
+            ('ORDER BY', order),
+            ('LIMIT', limit),
+            ('OFFSET', offset))
+    
+    def gen_clause(self, sql, val, vars): 
+        if isinstance(val, (int, long)):
+            if sql == 'WHERE':
+                nout = 'id = ' + sqlquote(val)
+            else:
+                nout = SQLQuery(val)
+        #@@@
+        elif isinstance(val, (list, tuple)) and len(val) == 2:
+            nout = SQLQuery(val[0], val[1]) # backwards-compatibility
+        elif isinstance(val, SQLQuery):
+            nout = val
+        else:
+            nout = reparam(val, vars)
+
+        def xjoin(a, b):
+            if a and b: return a + ' ' + b
+            else: return a or b
+
+        return xjoin(sql, nout)
+
+    def insert(self, tablename, seqname=None, _test=False, **values): 
+        """
+        Inserts `values` into `tablename`. Returns current sequence ID.
+        Set `seqname` to the ID if it's not the default, or to `False`
+        if there isn't one.
+        
+            >>> db = DB(None, {})
+            >>> q = db.insert('foo', name='bob', age=2, created=SQLLiteral('NOW()'), _test=True)
+            >>> q
+            <sql: "INSERT INTO foo (age, name, created) VALUES (2, 'bob', NOW())">
+            >>> q.query()
+            'INSERT INTO foo (age, name, created) VALUES (%s, %s, NOW())'
+            >>> q.values()
+            [2, 'bob']
+        """
+        def q(x): return "(" + x + ")"
+        
+        if values:
+            _keys = SQLQuery.join(values.keys(), ', ')
+            _values = SQLQuery.join([sqlparam(v) for v in values.values()], ', ')
+            sql_query = "INSERT INTO %s " % tablename + q(_keys) + ' VALUES ' + q(_values)
+        else:
+            sql_query = SQLQuery("INSERT INTO %s DEFAULT VALUES" % tablename)
+
+        if _test: return sql_query
+        
+        db_cursor = self._db_cursor()
+        if seqname is not False: 
+            sql_query = self._process_insert_query(sql_query, tablename, seqname)
+
+        if isinstance(sql_query, tuple):
+            # for some databases, a separate query has to be made to find 
+            # the id of the inserted row.
+            q1, q2 = sql_query
+            self._db_execute(db_cursor, q1)
+            self._db_execute(db_cursor, q2)
+        else:
+            self._db_execute(db_cursor, sql_query)
+
+        try: 
+            out = db_cursor.fetchone()[0]
+        except Exception: 
+            out = None
+        
+        if not self.ctx.transactions: 
+            self.ctx.commit()
+        return out
+        
+    def multiple_insert(self, tablename, values, seqname=None, _test=False):
+        """
+        Inserts multiple rows into `tablename`. The `values` must be a list of dictioanries, 
+        one for each row to be inserted, each with the same set of keys.
+        Returns the list of ids of the inserted rows.        
+        Set `seqname` to the ID if it's not the default, or to `False`
+        if there isn't one.
+        
+            >>> db = DB(None, {})
+            >>> db.supports_multiple_insert = True
+            >>> values = [{"name": "foo", "email": "foo@example.com"}, {"name": "bar", "email": "bar@example.com"}]
+            >>> db.multiple_insert('person', values=values, _test=True)
+            <sql: "INSERT INTO person (name, email) VALUES ('foo', 'foo@example.com'), ('bar', 'bar@example.com')">
+        """        
+        if not values:
+            return []
+            
+        if not self.supports_multiple_insert:
+            out = [self.insert(tablename, seqname=seqname, _test=_test, **v) for v in values]
+            if seqname is False:
+                return None
+            else:
+                return out
+                
+        keys = values[0].keys()
+        #@@ make sure all keys are valid
+
+        # make sure all rows have same keys.
+        for v in values:
+            if v.keys() != keys:
+                raise ValueError, 'Bad data'
+
+        sql_query = SQLQuery('INSERT INTO %s (%s) VALUES ' % (tablename, ', '.join(keys))) 
+
+        data = []
+        for row in values:
+            d = SQLQuery.join([SQLParam(row[k]) for k in keys], ', ')
+            data.append('(' + d + ')')
+        sql_query += SQLQuery.join(data, ', ')
+
+        if _test: return sql_query
+
+        db_cursor = self._db_cursor()
+        if seqname is not False: 
+            sql_query = self._process_insert_query(sql_query, tablename, seqname)
+
+        if isinstance(sql_query, tuple):
+            # for some databases, a separate query has to be made to find 
+            # the id of the inserted row.
+            q1, q2 = sql_query
+            self._db_execute(db_cursor, q1)
+            self._db_execute(db_cursor, q2)
+        else:
+            self._db_execute(db_cursor, sql_query)
+
+        try: 
+            out = db_cursor.fetchone()[0]
+            out = range(out-len(values)+1, out+1)        
+        except Exception: 
+            out = None
+
+        if not self.ctx.transactions: 
+            self.ctx.commit()
+        return out
+
+    
+    def update(self, tables, where, vars=None, _test=False, **values): 
+        """
+        Update `tables` with clause `where` (interpolated using `vars`)
+        and setting `values`.
+
+            >>> db = DB(None, {})
+            >>> name = 'Joseph'
+            >>> q = db.update('foo', where='name = $name', name='bob', age=2,
+            ...     created=SQLLiteral('NOW()'), vars=locals(), _test=True)
+            >>> q
+            <sql: "UPDATE foo SET age = 2, name = 'bob', created = NOW() WHERE name = 'Joseph'">
+            >>> q.query()
+            'UPDATE foo SET age = %s, name = %s, created = NOW() WHERE name = %s'
+            >>> q.values()
+            [2, 'bob', 'Joseph']
+        """
+        if vars is None: vars = {}
+        where = self._where(where, vars)
+
+        query = (
+          "UPDATE " + sqllist(tables) + 
+          " SET " + sqlwhere(values, ', ') + 
+          " WHERE " + where)
+
+        if _test: return query
+        
+        db_cursor = self._db_cursor()
+        self._db_execute(db_cursor, query)
+        if not self.ctx.transactions: 
+            self.ctx.commit()
+        return db_cursor.rowcount
+    
+    def delete(self, table, where, using=None, vars=None, _test=False): 
+        """
+        Deletes from `table` with clauses `where` and `using`.
+
+            >>> db = DB(None, {})
+            >>> name = 'Joe'
+            >>> db.delete('foo', where='name = $name', vars=locals(), _test=True)
+            <sql: "DELETE FROM foo WHERE name = 'Joe'">
+        """
+        if vars is None: vars = {}
+        where = self._where(where, vars)
+
+        q = 'DELETE FROM ' + table
+        if where: q += ' WHERE ' + where
+        if using: q += ' USING ' + sqllist(using)
+
+        if _test: return q
+
+        db_cursor = self._db_cursor()
+        self._db_execute(db_cursor, q)
+        if not self.ctx.transactions: 
+            self.ctx.commit()
+        return db_cursor.rowcount
+
+    def _process_insert_query(self, query, tablename, seqname):
+        return query
+
+    def transaction(self): 
+        """Start a transaction."""
+        return Transaction(self.ctx)
+    
+class PostgresDB(DB): 
+    """Postgres driver."""
+    def __init__(self, **keywords):
+        if 'pw' in keywords:
+            keywords['password'] = keywords['pw']
+            del keywords['pw']
+            
+        db_module = import_driver(["psycopg2", "psycopg", "pgdb"], preferred=keywords.pop('driver', None))
+        if db_module.__name__ == "psycopg2":
+            import psycopg2.extensions
+            psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
+
+        keywords['database'] = keywords.pop('db')
+        self.dbname = "postgres"
+        self.paramstyle = db_module.paramstyle
+        DB.__init__(self, db_module, keywords)
+        self.supports_multiple_insert = True
+        
+    def _process_insert_query(self, query, tablename, seqname):
+        if seqname is None: 
+            seqname = tablename + "_id_seq"
+        return query + "; SELECT currval('%s')" % seqname
+
+    def _connect(self, keywords):
+        conn = DB._connect(self, keywords)
+        conn.set_client_encoding('UTF8')
+        return conn
+        
+    def _connect_with_pooling(self, keywords):
+        conn = DB._connect_with_pooling(self, keywords)
+        conn._con._con.set_client_encoding('UTF8')
+        return conn
+
+class MySQLDB(DB): 
+    def __init__(self, **keywords):
+        import MySQLdb as db
+        if 'pw' in keywords:
+            keywords['passwd'] = keywords['pw']
+            del keywords['pw']
+
+        if 'charset' not in keywords:
+            keywords['charset'] = 'utf8'
+        elif keywords['charset'] is None:
+            del keywords['charset']
+
+        self.paramstyle = db.paramstyle = 'pyformat' # it's both, like psycopg
+        self.dbname = "mysql"
+        DB.__init__(self, db, keywords)
+        self.supports_multiple_insert = True
+        
+    def _process_insert_query(self, query, tablename, seqname):
+        return query, SQLQuery('SELECT last_insert_id();')
+
+def import_driver(drivers, preferred=None):
+    """Import the first available driver or preferred driver.
+    """
+    if preferred:
+        drivers = [preferred]
+
+    for d in drivers:
+        try:
+            return __import__(d, None, None, ['x'])
+        except ImportError:
+            pass
+    raise ImportError("Unable to import " + " or ".join(drivers))
+
+class SqliteDB(DB): 
+    def __init__(self, **keywords):
+        db = import_driver(["sqlite3", "pysqlite2.dbapi2", "sqlite"], preferred=keywords.pop('driver', None))
+
+        if db.__name__ in ["sqlite3", "pysqlite2.dbapi2"]:
+            db.paramstyle = 'qmark'
+
+        self.paramstyle = db.paramstyle
+        keywords['database'] = keywords.pop('db')
+        self.dbname = "sqlite"        
+        DB.__init__(self, db, keywords)
+
+    def _process_insert_query(self, query, tablename, seqname):
+        return query, SQLQuery('SELECT last_insert_rowid();')
+    
+    def query(self, *a, **kw):
+        out = DB.query(self, *a, **kw)
+        if isinstance(out, iterbetter):
+            # rowcount is not provided by sqlite
+            del out.__len__
+        return out
+
+class FirebirdDB(DB):
+    """Firebird Database.
+    """
+    def __init__(self, **keywords):
+        try:
+            import kinterbasdb as db
+        except Exception:
+            db = None
+            pass
+        if 'pw' in keywords:
+            keywords['passwd'] = keywords['pw']
+            del keywords['pw']
+        keywords['database'] = keywords['db']
+        del keywords['db']
+        DB.__init__(self, db, keywords)
+        
+    def delete(self, table, where=None, using=None, vars=None, _test=False):
+        # firebird doesn't support using clause
+        using=None
+        return DB.delete(self, table, where, using, vars, _test)
+
+    def sql_clauses(self, what, tables, where, group, order, limit, offset):
+        return (
+            ('SELECT', ''),
+            ('FIRST', limit),
+            ('SKIP', offset),
+            ('', what),
+            ('FROM', sqllist(tables)),
+            ('WHERE', where),
+            ('GROUP BY', group),
+            ('ORDER BY', order)
+        )
+
+class MSSQLDB(DB):
+    def __init__(self, **keywords):
+        import pymssql as db    
+        if 'pw' in keywords:
+            keywords['password'] = keywords.pop('pw')
+        keywords['database'] = keywords.pop('db')
+        self.dbname = "mssql"
+        DB.__init__(self, db, keywords)
+
+    def sql_clauses(self, what, tables, where, group, order, limit, offset): 
+        return (
+            ('SELECT', what),
+            ('TOP', limit),
+            ('FROM', sqllist(tables)),
+            ('WHERE', where),
+            ('GROUP BY', group),
+            ('ORDER BY', order),
+            ('OFFSET', offset))
+            
+    def _test(self):
+        """Test LIMIT.
+
+            Fake presence of pymssql module for running tests.
+            >>> import sys
+            >>> sys.modules['pymssql'] = sys.modules['sys']
+            
+            MSSQL has TOP clause instead of LIMIT clause.
+            >>> db = MSSQLDB(db='test', user='joe', pw='secret')
+            >>> db.select('foo', limit=4, _test=True)
+            <sql: 'SELECT * TOP 4 FROM foo'>
+        """
+        pass
+
+class OracleDB(DB): 
+    def __init__(self, **keywords): 
+        import cx_Oracle as db 
+        if 'pw' in keywords: 
+            keywords['password'] = keywords.pop('pw') 
+
+        #@@ TODO: use db.makedsn if host, port is specified 
+        keywords['dsn'] = keywords.pop('db') 
+        self.dbname = 'oracle' 
+        db.paramstyle = 'numeric' 
+        self.paramstyle = db.paramstyle
+
+        # oracle doesn't support pooling 
+        keywords.pop('pooling', None) 
+        DB.__init__(self, db, keywords) 
+
+    def _process_insert_query(self, query, tablename, seqname): 
+        if seqname is None: 
+            # It is not possible to get seq name from table name in Oracle
+            return query
+        else:
+            return query + "; SELECT %s.currval FROM dual" % seqname 
+
+_databases = {}
+def database(dburl=None, **params):
+    """Creates appropriate database using params.
+    
+    Pooling will be enabled if DBUtils module is available. 
+    Pooling can be disabled by passing pooling=False in params.
+    """
+    dbn = params.pop('dbn')
+    if dbn in _databases:
+        return _databases[dbn](**params)
+    else:
+        raise UnknownDB, dbn
+
+def register_database(name, clazz):
+    """
+    Register a database.
+
+        >>> class LegacyDB(DB): 
+        ...     def __init__(self, **params): 
+        ...        pass 
+        ...
+        >>> register_database('legacy', LegacyDB)
+        >>> db = database(dbn='legacy', db='test', user='joe', passwd='secret') 
+    """
+    _databases[name] = clazz
+
+register_database('mysql', MySQLDB)
+register_database('postgres', PostgresDB)
+register_database('sqlite', SqliteDB)
+register_database('firebird', FirebirdDB)
+register_database('mssql', MSSQLDB)
+register_database('oracle', OracleDB)
+
+def _interpolate(format): 
+    """
+    Takes a format string and returns a list of 2-tuples of the form
+    (boolean, string) where boolean says whether string should be evaled
+    or not.
+
+    from <http://lfw.org/python/Itpl.py> (public domain, Ka-Ping Yee)
+    """
+    from tokenize import tokenprog
+
+    def matchorfail(text, pos):
+        match = tokenprog.match(text, pos)
+        if match is None:
+            raise _ItplError(text, pos)
+        return match, match.end()
+
+    namechars = "abcdefghijklmnopqrstuvwxyz" \
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
+    chunks = []
+    pos = 0
+
+    while 1:
+        dollar = format.find("$", pos)
+        if dollar < 0: 
+            break
+        nextchar = format[dollar + 1]
+
+        if nextchar == "{":
+            chunks.append((0, format[pos:dollar]))
+            pos, level = dollar + 2, 1
+            while level:
+                match, pos = matchorfail(format, pos)
+                tstart, tend = match.regs[3]
+                token = format[tstart:tend]
+                if token == "{": 
+                    level = level + 1
+                elif token == "}":  
+                    level = level - 1
+            chunks.append((1, format[dollar + 2:pos - 1]))
+
+        elif nextchar in namechars:
+            chunks.append((0, format[pos:dollar]))
+            match, pos = matchorfail(format, dollar + 1)
+            while pos < len(format):
+                if format[pos] == "." and \
+                    pos + 1 < len(format) and format[pos + 1] in namechars:
+                    match, pos = matchorfail(format, pos + 1)
+                elif format[pos] in "([":
+                    pos, level = pos + 1, 1
+                    while level:
+                        match, pos = matchorfail(format, pos)
+                        tstart, tend = match.regs[3]
+                        token = format[tstart:tend]
+                        if token[0] in "([": 
+                            level = level + 1
+                        elif token[0] in ")]":  
+                            level = level - 1
+                else: 
+                    break
+            chunks.append((1, format[dollar + 1:pos]))
+        else:
+            chunks.append((0, format[pos:dollar + 1]))
+            pos = dollar + 1 + (nextchar == "$")
+
+    if pos < len(format): 
+        chunks.append((0, format[pos:]))
+    return chunks
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/debugerror.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,356 @@
+"""
+pretty debug errors
+(part of web.py)
+
+portions adapted from Django <djangoproject.com> 
+Copyright (c) 2005, the Lawrence Journal-World
+Used under the modified BSD license:
+http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
+"""
+
+__all__ = ["debugerror", "djangoerror", "emailerrors"]
+
+import sys, urlparse, pprint, traceback
+from net import websafe
+from template import Template
+from utils import sendmail
+import webapi as web
+
+import os, os.path
+whereami = os.path.join(os.getcwd(), __file__)
+whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
+djangoerror_t = """\
+$def with (exception_type, exception_value, frames)
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <meta name="robots" content="NONE,NOARCHIVE" />
+  <title>$exception_type at $ctx.path</title>
+  <style type="text/css">
+    html * { padding:0; margin:0; }
+    body * { padding:10px 20px; }
+    body * * { padding:0; }
+    body { font:small sans-serif; }
+    body>div { border-bottom:1px solid #ddd; }
+    h1 { font-weight:normal; }
+    h2 { margin-bottom:.8em; }
+    h2 span { font-size:80%; color:#666; font-weight:normal; }
+    h3 { margin:1em 0 .5em 0; }
+    h4 { margin:0 0 .5em 0; font-weight: normal; }
+    table { 
+        border:1px solid #ccc; border-collapse: collapse; background:white; }
+    tbody td, tbody th { vertical-align:top; padding:2px 3px; }
+    thead th { 
+        padding:1px 6px 1px 3px; background:#fefefe; text-align:left; 
+        font-weight:normal; font-size:11px; border:1px solid #ddd; }
+    tbody th { text-align:right; color:#666; padding-right:.5em; }
+    table.vars { margin:5px 0 2px 40px; }
+    table.vars td, table.req td { font-family:monospace; }
+    table td.code { width:100%;}
+    table td.code div { overflow:hidden; }
+    table.source th { color:#666; }
+    table.source td { 
+        font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
+    ul.traceback { list-style-type:none; }
+    ul.traceback li.frame { margin-bottom:1em; }
+    div.context { margin: 10px 0; }
+    div.context ol { 
+        padding-left:30px; margin:0 10px; list-style-position: inside; }
+    div.context ol li { 
+        font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
+    div.context ol.context-line li { color:black; background-color:#ccc; }
+    div.context ol.context-line li span { float: right; }
+    div.commands { margin-left: 40px; }
+    div.commands a { color:black; text-decoration:none; }
+    #summary { background: #ffc; }
+    #summary h2 { font-weight: normal; color: #666; }
+    #explanation { background:#eee; }
+    #template, #template-not-exist { background:#f6f6f6; }
+    #template-not-exist ul { margin: 0 0 0 20px; }
+    #traceback { background:#eee; }
+    #requestinfo { background:#f6f6f6; padding-left:120px; }
+    #summary table { border:none; background:transparent; }
+    #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
+    #requestinfo h3 { margin-bottom:-1em; }
+    .error { background: #ffc; }
+    .specific { color:#cc3300; font-weight:bold; }
+  </style>
+  <script type="text/javascript">
+  //<!--
+    function getElementsByClassName(oElm, strTagName, strClassName){
+        // Written by Jonathan Snook, http://www.snook.ca/jon; 
+        // Add-ons by Robert Nyman, http://www.robertnyman.com
+        var arrElements = (strTagName == "*" && document.all)? document.all :
+        oElm.getElementsByTagName(strTagName);
+        var arrReturnElements = new Array();
+        strClassName = strClassName.replace(/\-/g, "\\-");
+        var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
+        var oElement;
+        for(var i=0; i<arrElements.length; i++){
+            oElement = arrElements[i];
+            if(oRegExp.test(oElement.className)){
+                arrReturnElements.push(oElement);
+            }
+        }
+        return (arrReturnElements)
+    }
+    function hideAll(elems) {
+      for (var e = 0; e < elems.length; e++) {
+        elems[e].style.display = 'none';
+      }
+    }
+    window.onload = function() {
+      hideAll(getElementsByClassName(document, 'table', 'vars'));
+      hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
+      hideAll(getElementsByClassName(document, 'ol', 'post-context'));
+    }
+    function toggle() {
+      for (var i = 0; i < arguments.length; i++) {
+        var e = document.getElementById(arguments[i]);
+        if (e) {
+          e.style.display = e.style.display == 'none' ? 'block' : 'none';
+        }
+      }
+      return false;
+    }
+    function varToggle(link, id) {
+      toggle('v' + id);
+      var s = link.getElementsByTagName('span')[0];
+      var uarr = String.fromCharCode(0x25b6);
+      var darr = String.fromCharCode(0x25bc);
+      s.innerHTML = s.innerHTML == uarr ? darr : uarr;
+      return false;
+    }
+    //-->
+  </script>
+</head>
+<body>
+
+$def dicttable (d, kls='req', id=None):
+    $ items = d and d.items() or []
+    $items.sort()
+    $:dicttable_items(items, kls, id)
+        
+$def dicttable_items(items, kls='req', id=None):
+    $if items:
+        <table class="$kls"
+        $if id: id="$id"
+        ><thead><tr><th>Variable</th><th>Value</th></tr></thead>
+        <tbody>
+        $for k, v in items:
+            <tr><td>$k</td><td class="code"><div>$prettify(v)</div></td></tr>
+        </tbody>
+        </table>
+    $else:
+        <p>No data.</p>
+
+<div id="summary">
+  <h1>$exception_type at $ctx.path</h1>
+  <h2>$exception_value</h2>
+  <table><tr>
+    <th>Python</th>
+    <td>$frames[0].filename in $frames[0].function, line $frames[0].lineno</td>
+  </tr><tr>
+    <th>Web</th>
+    <td>$ctx.method $ctx.home$ctx.path</td>
+  </tr></table>
+</div>
+<div id="traceback">
+<h2>Traceback <span>(innermost first)</span></h2>
+<ul class="traceback">
+$for frame in frames:
+    <li class="frame">
+    <code>$frame.filename</code> in <code>$frame.function</code>
+    $if frame.context_line:
+        <div class="context" id="c$frame.id">
+        $if frame.pre_context:
+            <ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">
+            $for line in frame.pre_context:
+                <li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
+            </ol>
+            <ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
+        $if frame.post_context:
+            <ol start='${frame.lineno + 1}' class="post-context" id="post$frame.id">
+            $for line in frame.post_context:
+                <li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
+            </ol>
+      </div>
+    
+    $if frame.vars:
+        <div class="commands">
+        <a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>
+        $# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
+        </div>
+        $:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id)))
+      </li>
+  </ul>
+</div>
+
+<div id="requestinfo">
+$if ctx.output or ctx.headers:
+    <h2>Response so far</h2>
+    <h3>HEADERS</h3>
+    $:dicttable_items(ctx.headers)
+
+    <h3>BODY</h3>
+    <p class="req" style="padding-bottom: 2em"><code>
+    $ctx.output
+    </code></p>
+  
+<h2>Request information</h2>
+
+<h3>INPUT</h3>
+$:dicttable(web.input())
+
+<h3 id="cookie-info">COOKIES</h3>
+$:dicttable(web.cookies())
+
+<h3 id="meta-info">META</h3>
+$ newctx = [(k, v) for (k, v) in ctx.iteritems() if not k.startswith('_') and not isinstance(v, dict)]
+$:dicttable(dict(newctx))
+
+<h3 id="meta-info">ENVIRONMENT</h3>
+$:dicttable(ctx.env)
+</div>
+
+<div id="explanation">
+  <p>
+    You're seeing this error because you have <code>web.config.debug</code>
+    set to <code>True</code>. Set that to <code>False</code> if you don't to see this.
+  </p>
+</div>
+
+</body>
+</html>
+"""
+
+djangoerror_r = None
+
+def djangoerror():
+    def _get_lines_from_file(filename, lineno, context_lines):
+        """
+        Returns context_lines before and after lineno from file.
+        Returns (pre_context_lineno, pre_context, context_line, post_context).
+        """
+        try:
+            source = open(filename).readlines()
+            lower_bound = max(0, lineno - context_lines)
+            upper_bound = lineno + context_lines
+
+            pre_context = \
+                [line.strip('\n') for line in source[lower_bound:lineno]]
+            context_line = source[lineno].strip('\n')
+            post_context = \
+                [line.strip('\n') for line in source[lineno + 1:upper_bound]]
+
+            return lower_bound, pre_context, context_line, post_context
+        except (OSError, IOError):
+            return None, [], None, []    
+    
+    exception_type, exception_value, tback = sys.exc_info()
+    frames = []
+    while tback is not None:
+        filename = tback.tb_frame.f_code.co_filename
+        function = tback.tb_frame.f_code.co_name
+        lineno = tback.tb_lineno - 1
+        pre_context_lineno, pre_context, context_line, post_context = \
+            _get_lines_from_file(filename, lineno, 7)
+        frames.append(web.storage({
+            'tback': tback,
+            'filename': filename,
+            'function': function,
+            'lineno': lineno,
+            'vars': tback.tb_frame.f_locals,
+            'id': id(tback),
+            'pre_context': pre_context,
+            'context_line': context_line,
+            'post_context': post_context,
+            'pre_context_lineno': pre_context_lineno,
+        }))
+        tback = tback.tb_next
+    frames.reverse()
+    urljoin = urlparse.urljoin
+    def prettify(x):
+        try: 
+            out = pprint.pformat(x)
+        except Exception, e: 
+            out = '[could not display: <' + e.__class__.__name__ + \
+                  ': '+str(e)+'>]'
+        return out
+        
+    global djangoerror_r
+    if djangoerror_r is None:
+        djangoerror_r = Template(djangoerror_t, filename=__file__, filter=websafe)
+        
+    t = djangoerror_r
+    globals = {'ctx': web.ctx, 'web':web, 'dict':dict, 'str':str, 'prettify': prettify}
+    t.t.func_globals.update(globals)
+    return t(exception_type, exception_value, frames)
+
+def debugerror():
+    """
+    A replacement for `internalerror` that presents a nice page with lots
+    of debug information for the programmer.
+
+    (Based on the beautiful 500 page from [Django](http://djangoproject.com/), 
+    designed by [Wilson Miner](http://wilsonminer.com/).)
+    """
+    return web._InternalError(djangoerror())
+
+def emailerrors(to_address, olderror, from_address=None):
+    """
+    Wraps the old `internalerror` handler (pass as `olderror`) to 
+    additionally email all errors to `to_address`, to aid in
+    debugging production websites.
+    
+    Emails contain a normal text traceback as well as an
+    attachment containing the nice `debugerror` page.
+    """
+    from_address = from_address or to_address
+
+    def emailerrors_internal():
+        error = olderror()
+        tb = sys.exc_info()
+        error_name = tb[0]
+        error_value = tb[1]
+        tb_txt = ''.join(traceback.format_exception(*tb))
+        path = web.ctx.path
+        request = web.ctx.method + ' ' + web.ctx.home + web.ctx.fullpath
+        text = ("""\
+------here----
+Content-Type: text/plain
+Content-Disposition: inline
+
+%(request)s
+
+%(tb_txt)s
+
+------here----
+Content-Type: text/html; name="bug.html"
+Content-Disposition: attachment; filename="bug.html"
+
+""" % locals()) + str(djangoerror())
+        sendmail(
+          "your buggy site <%s>" % from_address,
+          "the bugfixer <%s>" % to_address,
+          "bug: %(error_name)s: %(error_value)s (%(path)s)" % locals(),
+          text, 
+          headers={'Content-Type': 'multipart/mixed; boundary="----here----"'})
+        return error
+    
+    return emailerrors_internal
+
+if __name__ == "__main__":
+    urls = (
+        '/', 'index'
+    )
+    from application import application
+    app = application(urls, globals())
+    app.internalerror = debugerror
+    
+    class index:
+        def GET(self):
+            thisdoesnotexist
+
+    app.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/form.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,264 @@
+"""
+HTML forms
+(part of web.py)
+"""
+
+import copy, re
+import webapi as web
+import utils, net
+
+def attrget(obj, attr, value=None):
+    if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr]
+    if hasattr(obj, attr): return getattr(obj, attr)
+    return value
+
+class Form:
+    r"""
+    HTML form.
+    
+        >>> f = Form(Textbox("x"))
+        >>> f.render()
+        '<table>\n    <tr><th><label for="x">x</label></th><td><input type="text" name="x" id="x" /></td></tr>\n</table>'
+    """
+    def __init__(self, *inputs, **kw):
+        self.inputs = inputs
+        self.valid = True
+        self.note = None
+        self.validators = kw.pop('validators', [])
+
+    def __call__(self, x=None):
+        o = copy.deepcopy(self)
+        if x: o.validates(x)
+        return o
+    
+    def render(self):
+        out = ''
+        out += self.rendernote(self.note)
+        out += '<table>\n'
+        for i in self.inputs:
+            out += '    <tr><th><label for="%s">%s</label></th>' % (i.id, net.websafe(i.description))
+            out += "<td>"+i.pre+i.render()+i.post+"</td></tr>\n"
+        out += "</table>"
+        return out
+        
+    def render_css(self): 
+        out = [] 
+        out.append(self.rendernote(self.note)) 
+        for i in self.inputs: 
+            out.append('<label for="%s">%s</label>' % (i.id, net.websafe(i.description))) 
+            out.append(i.pre) 
+            out.append(i.render()) 
+            out.append(i.post) 
+            out.append('\n') 
+        return ''.join(out) 
+        
+    def rendernote(self, note):
+        if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
+        else: return ""
+    
+    def validates(self, source=None, _validate=True, **kw):
+        source = source or kw or web.input()
+        out = True
+        for i in self.inputs:
+            v = attrget(source, i.name)
+            if _validate:
+                out = i.validate(v) and out
+            else:
+                i.value = v
+        if _validate:
+            out = out and self._validate(source)
+            self.valid = out
+        return out
+
+    def _validate(self, value):
+        self.value = value
+        for v in self.validators:
+            if not v.valid(value):
+                self.note = v.msg
+                return False
+        return True
+
+    def fill(self, source=None, **kw):
+        return self.validates(source, _validate=False, **kw)
+    
+    def __getitem__(self, i):
+        for x in self.inputs:
+            if x.name == i: return x
+        raise KeyError, i
+
+    def __getattr__(self, name):
+        # don't interfere with deepcopy
+        inputs = self.__dict__.get('inputs') or []
+        for x in inputs:
+            if x.name == name: return x
+        raise AttributeError, name
+    
+    def get(self, i, default=None):
+        try:
+            return self[i]
+        except KeyError:
+            return default
+            
+    def _get_d(self): #@@ should really be form.attr, no?
+        return utils.storage([(i.name, i.value) for i in self.inputs])
+    d = property(_get_d)
+
+class Input(object):
+    def __init__(self, name, *validators, **attrs):
+        self.description = attrs.pop('description', name)
+        self.value = attrs.pop('value', None)
+        self.pre = attrs.pop('pre', "")
+        self.post = attrs.pop('post', "")
+        self.id = attrs.setdefault('id', name)
+        if 'class_' in attrs:
+            attrs['class'] = attrs['class_']
+            del attrs['class_']
+        self.name, self.validators, self.attrs, self.note = name, validators, attrs, None
+
+    def validate(self, value):
+        self.value = value
+        for v in self.validators:
+            if not v.valid(value):
+                self.note = v.msg
+                return False
+        return True
+
+    def render(self): raise NotImplementedError
+
+    def rendernote(self, note):
+        if note: return '<strong class="wrong">%s</strong>' % net.websafe(note)
+        else: return ""
+        
+    def addatts(self):
+        str = ""
+        for (n, v) in self.attrs.items():
+            str += ' %s="%s"' % (n, net.websafe(v))
+        return str
+    
+#@@ quoting
+
+class Textbox(Input):
+    def render(self, shownote=True):
+        x = '<input type="text" name="%s"' % net.websafe(self.name)
+        if self.value: x += ' value="%s"' % net.websafe(self.value)
+        x += self.addatts()
+        x += ' />'
+        if shownote:
+            x += self.rendernote(self.note)
+        return x
+
+class Password(Input):
+    def render(self):
+        x = '<input type="password" name="%s"' % net.websafe(self.name)
+        if self.value: x += ' value="%s"' % net.websafe(self.value)
+        x += self.addatts()
+        x += ' />'
+        x += self.rendernote(self.note)
+        return x
+
+class Textarea(Input):
+    def render(self):
+        x = '<textarea name="%s"' % net.websafe(self.name)
+        x += self.addatts()
+        x += '>'
+        if self.value is not None: x += net.websafe(self.value)
+        x += '</textarea>'
+        x += self.rendernote(self.note)
+        return x
+
+class Dropdown(Input):
+    def __init__(self, name, args, *validators, **attrs):
+        self.args = args
+        super(Dropdown, self).__init__(name, *validators, **attrs)
+
+    def render(self):
+        x = '<select name="%s"%s>\n' % (net.websafe(self.name), self.addatts())
+        for arg in self.args:
+            if isinstance(arg, (tuple, list)):
+                value, desc= arg
+            else:
+                value, desc = arg, arg 
+
+            if self.value == value: select_p = ' selected="selected"'
+            else: select_p = ''
+            x += '  <option %s value="%s">%s</option>\n' % (select_p, net.websafe(value), net.websafe(desc))
+        x += '</select>\n'
+        x += self.rendernote(self.note)
+        return x
+
+class Radio(Input):
+    def __init__(self, name, args, *validators, **attrs):
+        self.args = args
+        super(Radio, self).__init__(name, *validators, **attrs)
+
+    def render(self):
+        x = '<span>'
+        for arg in self.args:
+            if self.value == arg: select_p = ' checked="checked"'
+            else: select_p = ''
+            x += '<input type="radio" name="%s" value="%s"%s%s /> %s ' % (net.websafe(self.name), net.websafe(arg), select_p, self.addatts(), net.websafe(arg))
+            x += '</span>'
+            x += self.rendernote(self.note)    
+        return x
+
+class Checkbox(Input):
+    def render(self):
+        x = '<input name="%s" type="checkbox"' % net.websafe(self.name)
+        if self.value: x += ' checked="checked"'
+        x += self.addatts()
+        x += ' />'
+        x += self.rendernote(self.note)
+        return x
+
+class Button(Input):
+    def __init__(self, name, *validators, **attrs):
+        super(Button, self).__init__(name, *validators, **attrs)
+        self.description = ""
+
+    def render(self):
+        safename = net.websafe(self.name)
+        x = '<button name="%s"%s>%s</button>' % (safename, self.addatts(), safename)
+        x += self.rendernote(self.note)
+        return x
+
+class Hidden(Input):
+    def __init__(self, name, *validators, **attrs):
+        super(Hidden, self).__init__(name, *validators, **attrs)
+        # it doesnt make sence for a hidden field to have description
+        self.description = ""
+
+    def render(self):
+        x = '<input type="hidden" name="%s"' % net.websafe(self.name)
+        if self.value: x += ' value="%s"' % net.websafe(self.value)
+        x += self.addatts()
+        x += ' />'
+        return x
+
+class File(Input):
+    def render(self):
+        x = '<input type="file" name="%s"' % net.websafe(self.name)
+        x += self.addatts()
+        x += ' />'
+        x += self.rendernote(self.note)
+        return x
+    
+class Validator:
+    def __deepcopy__(self, memo): return copy.copy(self)
+    def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals())
+    def valid(self, value): 
+        try: return self.test(value)
+        except: return False
+
+notnull = Validator("Required", bool)
+
+class regexp(Validator):
+    def __init__(self, rexp, msg):
+        self.rexp = re.compile(rexp)
+        self.msg = msg
+    
+    def valid(self, value):
+        return bool(self.rexp.match(value))
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/http.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,163 @@
+"""
+HTTP Utilities
+(from web.py)
+"""
+
+__all__ = [
+  "expires", "lastmodified", 
+  "prefixurl", "modified", 
+  "write",
+  "changequery", "url",
+  "profiler",
+]
+
+import sys, os, threading, urllib, urlparse
+try: import datetime
+except ImportError: pass
+import net, utils, webapi as web
+
+def prefixurl(base=''):
+    """
+    Sorry, this function is really difficult to explain.
+    Maybe some other time.
+    """
+    url = web.ctx.path.lstrip('/')
+    for i in xrange(url.count('/')): 
+        base += '../'
+    if not base: 
+        base = './'
+    return base
+
+def expires(delta):
+    """
+    Outputs an `Expires` header for `delta` from now. 
+    `delta` is a `timedelta` object or a number of seconds.
+    """
+    if isinstance(delta, (int, long)):
+        delta = datetime.timedelta(seconds=delta)
+    date_obj = datetime.datetime.utcnow() + delta
+    web.header('Expires', net.httpdate(date_obj))
+
+def lastmodified(date_obj):
+    """Outputs a `Last-Modified` header for `datetime`."""
+    web.header('Last-Modified', net.httpdate(date_obj))
+
+def modified(date=None, etag=None):
+    """
+    Checks to see if the page has been modified since the version in the
+    requester's cache.
+    
+    When you publish pages, you can include `Last-Modified` and `ETag`
+    with the date the page was last modified and an opaque token for
+    the particular version, respectively. When readers reload the page, 
+    the browser sends along the modification date and etag value for
+    the version it has in its cache. If the page hasn't changed, 
+    the server can just return `304 Not Modified` and not have to 
+    send the whole page again.
+    
+    This function takes the last-modified date `date` and the ETag `etag`
+    and checks the headers to see if they match. If they do, it returns 
+    `True` and sets the response status to `304 Not Modified`. It also
+    sets `Last-Modified and `ETag` output headers.
+    """
+    try:
+        from __builtin__ import set
+    except ImportError:
+        # for python 2.3
+        from sets import Set as set
+
+    n = set([x.strip('" ') for x in web.ctx.env.get('HTTP_IF_NONE_MATCH', '').split(',')])
+    m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
+    validate = False
+    if etag:
+        if '*' in n or etag in n:
+            validate = True
+    if date and m:
+        # we subtract a second because 
+        # HTTP dates don't have sub-second precision
+        if date-datetime.timedelta(seconds=1) <= m:
+            validate = True
+    
+    if validate: web.ctx.status = '304 Not Modified'
+    if date: lastmodified(date)
+    if etag: web.header('ETag', '"' + etag + '"')
+    return not validate
+
+def write(cgi_response):
+    """
+    Converts a standard CGI-style string response into `header` and 
+    `output` calls.
+    """
+    cgi_response = str(cgi_response)
+    cgi_response.replace('\r\n', '\n')
+    head, body = cgi_response.split('\n\n', 1)
+    lines = head.split('\n')
+
+    for line in lines:
+        if line.isspace(): 
+            continue
+        hdr, value = line.split(":", 1)
+        value = value.strip()
+        if hdr.lower() == "status": 
+            web.ctx.status = value
+        else: 
+            web.header(hdr, value)
+
+    web.output(body)
+
+def urlencode(query):
+    """
+    Same as urllib.urlencode, but supports unicode strings.
+    
+        >>> urlencode({'text':'foo bar'})
+        'text=foo+bar'
+    """
+    query = dict([(k, utils.utf8(v)) for k, v in query.items()])
+    return urllib.urlencode(query)
+
+def changequery(query=None, **kw):
+    """
+    Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
+    `/foo?a=3&b=2` -- the same URL but with the arguments you requested
+    changed.
+    """
+    if query is None:
+        query = web.input(_method='get')
+    for k, v in kw.iteritems():
+        if v is None:
+            query.pop(k, None)
+        else:
+            query[k] = v
+    out = web.ctx.path
+    if query:
+        out += '?' + urlencode(query)
+    return out
+
+def url(path=None, **kw):
+    """
+    Makes url by concatinating web.ctx.homepath and path and the 
+    query string created using the arguments.
+    """
+    if path is None:
+        path = web.ctx.path
+    if path.startswith("/"):
+        out = web.ctx.homepath + path
+    else:
+        out = path
+
+    if kw:
+        out += '?' + urlencode(kw)
+    
+    return out
+
+def profiler(app):
+    """Outputs basic profiling information at the bottom of each response."""
+    from utils import profile
+    def profile_internal(e, o):
+        out, result = profile(app)(e, o)
+        return list(out) + ['<pre>' + net.websafe(result) + '</pre>']
+    return profile_internal
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/httpserver.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,225 @@
+__all__ = ["runsimple"]
+
+import sys, os
+import webapi as web
+import net
+import utils
+
+def runbasic(func, server_address=("0.0.0.0", 8080)):
+    """
+    Runs a simple HTTP server hosting WSGI app `func`. The directory `static/` 
+    is hosted statically.
+
+    Based on [WsgiServer][ws] from [Colin Stewart][cs].
+    
+  [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
+  [cs]: http://www.owlfish.com/
+    """
+    # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
+    # Modified somewhat for simplicity
+    # Used under the modified BSD license:
+    # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
+
+    import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
+    import socket, errno
+    import traceback
+
+    class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+        def run_wsgi_app(self):
+            protocol, host, path, parameters, query, fragment = \
+                urlparse.urlparse('http://dummyhost%s' % self.path)
+
+            # we only use path, query
+            env = {'wsgi.version': (1, 0)
+                   ,'wsgi.url_scheme': 'http'
+                   ,'wsgi.input': self.rfile
+                   ,'wsgi.errors': sys.stderr
+                   ,'wsgi.multithread': 1
+                   ,'wsgi.multiprocess': 0
+                   ,'wsgi.run_once': 0
+                   ,'REQUEST_METHOD': self.command
+                   ,'REQUEST_URI': self.path
+                   ,'PATH_INFO': path
+                   ,'QUERY_STRING': query
+                   ,'CONTENT_TYPE': self.headers.get('Content-Type', '')
+                   ,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
+                   ,'REMOTE_ADDR': self.client_address[0]
+                   ,'SERVER_NAME': self.server.server_address[0]
+                   ,'SERVER_PORT': str(self.server.server_address[1])
+                   ,'SERVER_PROTOCOL': self.request_version
+                   }
+
+            for http_header, http_value in self.headers.items():
+                env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
+                    http_value
+
+            # Setup the state
+            self.wsgi_sent_headers = 0
+            self.wsgi_headers = []
+
+            try:
+                # We have there environment, now invoke the application
+                result = self.server.app(env, self.wsgi_start_response)
+                try:
+                    try:
+                        for data in result:
+                            if data: 
+                                self.wsgi_write_data(data)
+                    finally:
+                        if hasattr(result, 'close'): 
+                            result.close()
+                except socket.error, socket_err:
+                    # Catch common network errors and suppress them
+                    if (socket_err.args[0] in \
+                       (errno.ECONNABORTED, errno.EPIPE)): 
+                        return
+                except socket.timeout, socket_timeout: 
+                    return
+            except:
+                print >> web.debug, traceback.format_exc(),
+
+            if (not self.wsgi_sent_headers):
+                # We must write out something!
+                self.wsgi_write_data(" ")
+            return
+
+        do_POST = run_wsgi_app
+        do_PUT = run_wsgi_app
+        do_DELETE = run_wsgi_app
+
+        def do_GET(self):
+            if self.path.startswith('/static/'):
+                SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
+            else:
+                self.run_wsgi_app()
+
+        def wsgi_start_response(self, response_status, response_headers, 
+                              exc_info=None):
+            if (self.wsgi_sent_headers):
+                raise Exception \
+                      ("Headers already sent and start_response called again!")
+            # Should really take a copy to avoid changes in the application....
+            self.wsgi_headers = (response_status, response_headers)
+            return self.wsgi_write_data
+
+        def wsgi_write_data(self, data):
+            if (not self.wsgi_sent_headers):
+                status, headers = self.wsgi_headers
+                # Need to send header prior to data
+                status_code = status[:status.find(' ')]
+                status_msg = status[status.find(' ') + 1:]
+                self.send_response(int(status_code), status_msg)
+                for header, value in headers:
+                    self.send_header(header, value)
+                self.end_headers()
+                self.wsgi_sent_headers = 1
+            # Send the data
+            self.wfile.write(data)
+
+    class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+        def __init__(self, func, server_address):
+            BaseHTTPServer.HTTPServer.__init__(self, 
+                                               server_address, 
+                                               WSGIHandler)
+            self.app = func
+            self.serverShuttingDown = 0
+
+    print "http://%s:%d/" % server_address
+    WSGIServer(func, server_address).serve_forever()
+
+def runsimple(func, server_address=("0.0.0.0", 8080)):
+    """
+    Runs [CherryPy][cp] WSGI server hosting WSGI app `func`. 
+    The directory `static/` is hosted statically.
+
+    [cp]: http://www.cherrypy.org
+    """
+    from wsgiserver import CherryPyWSGIServer
+    from SimpleHTTPServer import SimpleHTTPRequestHandler
+    from BaseHTTPServer import BaseHTTPRequestHandler
+
+    class StaticApp(SimpleHTTPRequestHandler):
+        """WSGI application for serving static files."""
+        def __init__(self, environ, start_response):
+            self.headers = []
+            self.environ = environ
+            self.start_response = start_response
+
+        def send_response(self, status, msg=""):
+            self.status = str(status) + " " + msg
+
+        def send_header(self, name, value):
+            self.headers.append((name, value))
+
+        def end_headers(self):
+            pass
+
+        def log_message(*a): pass
+
+        def __iter__(self):
+            environ = self.environ
+
+            self.path = environ.get('PATH_INFO', '')
+            self.client_address = environ.get('REMOTE_ADDR','-'), \
+                                  environ.get('REMOTE_PORT','-')
+            self.command = environ.get('REQUEST_METHOD', '-')
+
+            from cStringIO import StringIO
+            self.wfile = StringIO() # for capturing error
+
+            f = self.send_head()
+            self.start_response(self.status, self.headers)
+
+            if f:
+                block_size = 16 * 1024
+                while True:
+                    buf = f.read(block_size)
+                    if not buf:
+                        break
+                    yield buf
+                f.close()
+            else:
+                value = self.wfile.getvalue()
+                yield value
+                    
+    class WSGIWrapper(BaseHTTPRequestHandler):
+        """WSGI wrapper for logging the status and serving static files."""
+        def __init__(self, app):
+            self.app = app
+            self.format = '%s - - [%s] "%s %s %s" - %s'
+
+        def __call__(self, environ, start_response):
+            def xstart_response(status, response_headers, *args):
+                write = start_response(status, response_headers, *args)
+                self.log(status, environ)
+                return write
+
+            path = environ.get('PATH_INFO', '')
+            if path.startswith('/static/'):
+                return StaticApp(environ, xstart_response)
+            else:
+                return self.app(environ, xstart_response)
+
+        def log(self, status, environ):
+            outfile = environ.get('wsgi.errors', web.debug)
+            req = environ.get('PATH_INFO', '_')
+            protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-')
+            method = environ.get('REQUEST_METHOD', '-')
+            host = "%s:%s" % (environ.get('REMOTE_ADDR','-'), 
+                              environ.get('REMOTE_PORT','-'))
+
+            #@@ It is really bad to extend from 
+            #@@ BaseHTTPRequestHandler just for this method
+            time = self.log_date_time_string()
+
+            msg = self.format % (host, time, protocol, method, req, status)
+            print >> outfile, utils.safestr(msg)
+            
+    func = WSGIWrapper(func)
+    server = CherryPyWSGIServer(server_address, func, server_name="localhost")
+
+    print "http://%s:%d/" % server_address
+    try:
+        server.start()
+    except KeyboardInterrupt:
+        server.stop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/net.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,190 @@
+"""
+Network Utilities
+(from web.py)
+"""
+
+__all__ = [
+  "validipaddr", "validipport", "validip", "validaddr", 
+  "urlquote",
+  "httpdate", "parsehttpdate", 
+  "htmlquote", "htmlunquote", "websafe",
+]
+
+import urllib, time
+try: import datetime
+except ImportError: pass
+
+def validipaddr(address):
+    """
+    Returns True if `address` is a valid IPv4 address.
+    
+        >>> validipaddr('192.168.1.1')
+        True
+        >>> validipaddr('192.168.1.800')
+        False
+        >>> validipaddr('192.168.1')
+        False
+    """
+    try:
+        octets = address.split('.')
+        if len(octets) != 4:
+            return False
+        for x in octets:
+            if not (0 <= int(x) <= 255):
+                return False
+    except ValueError:
+        return False
+    return True
+
+def validipport(port):
+    """
+    Returns True if `port` is a valid IPv4 port.
+    
+        >>> validipport('9000')
+        True
+        >>> validipport('foo')
+        False
+        >>> validipport('1000000')
+        False
+    """
+    try:
+        if not (0 <= int(port) <= 65535):
+            return False
+    except ValueError:
+        return False
+    return True
+
+def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
+    """Returns `(ip_address, port)` from string `ip_addr_port`"""
+    addr = defaultaddr
+    port = defaultport
+    
+    ip = ip.split(":", 1)
+    if len(ip) == 1:
+        if not ip[0]:
+            pass
+        elif validipaddr(ip[0]):
+            addr = ip[0]
+        elif validipport(ip[0]):
+            port = int(ip[0])
+        else:
+            raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+    elif len(ip) == 2:
+        addr, port = ip
+        if not validipaddr(addr) and validipport(port):
+            raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+        port = int(port)
+    else:
+        raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
+    return (addr, port)
+
+def validaddr(string_):
+    """
+    Returns either (ip_address, port) or "/path/to/socket" from string_
+    
+        >>> validaddr('/path/to/socket')
+        '/path/to/socket'
+        >>> validaddr('8000')
+        ('0.0.0.0', 8000)
+        >>> validaddr('127.0.0.1')
+        ('127.0.0.1', 8080)
+        >>> validaddr('127.0.0.1:8000')
+        ('127.0.0.1', 8000)
+        >>> validaddr('fff')
+        Traceback (most recent call last):
+            ...
+        ValueError: fff is not a valid IP address/port
+    """
+    if '/' in string_:
+        return string_
+    else:
+        return validip(string_)
+
+def urlquote(val):
+    """
+    Quotes a string for use in a URL.
+    
+        >>> urlquote('://?f=1&j=1')
+        '%3A//%3Ff%3D1%26j%3D1'
+        >>> urlquote(None)
+        ''
+        >>> urlquote(u'\u203d')
+        '%E2%80%BD'
+    """
+    if val is None: return ''
+    if not isinstance(val, unicode): val = str(val)
+    else: val = val.encode('utf-8')
+    return urllib.quote(val)
+
+def httpdate(date_obj):
+    """
+    Formats a datetime object for use in HTTP headers.
+    
+        >>> import datetime
+        >>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1))
+        'Thu, 01 Jan 1970 01:01:01 GMT'
+    """
+    return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+def parsehttpdate(string_):
+    """
+    Parses an HTTP date into a datetime object.
+
+        >>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT')
+        datetime.datetime(1970, 1, 1, 1, 1, 1)
+    """
+    try:
+        t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
+    except ValueError:
+        return None
+    return datetime.datetime(*t[:6])
+
+def htmlquote(text):
+    """
+    Encodes `text` for raw use in HTML.
+    
+        >>> htmlquote("<'&\\">")
+        '&lt;&#39;&amp;&quot;&gt;'
+    """
+    text = text.replace("&", "&amp;") # Must be done first!
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    text = text.replace("'", "&#39;")
+    text = text.replace('"', "&quot;")
+    return text
+
+def htmlunquote(text):
+    """
+    Decodes `text` that's HTML quoted.
+
+        >>> htmlunquote('&lt;&#39;&amp;&quot;&gt;')
+        '<\\'&">'
+    """
+    text = text.replace("&quot;", '"')
+    text = text.replace("&#39;", "'")
+    text = text.replace("&gt;", ">")
+    text = text.replace("&lt;", "<")
+    text = text.replace("&amp;", "&") # Must be done last!
+    return text
+
+def websafe(val):
+    """
+    Converts `val` so that it's safe for use in UTF-8 HTML.
+    
+        >>> websafe("<'&\\">")
+        '&lt;&#39;&amp;&quot;&gt;'
+        >>> websafe(None)
+        ''
+        >>> websafe(u'\u203d')
+        '\\xe2\\x80\\xbd'
+    """
+    if val is None:
+        return ''
+    if isinstance(val, unicode):
+        val = val.encode('utf-8')
+    val = str(val)
+    return htmlquote(val)
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/session.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,319 @@
+"""
+Session Management
+(from web.py)
+"""
+
+import os, time, datetime, random, base64
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+try:
+    import hashlib
+    sha1 = hashlib.sha1
+except ImportError:
+    import sha
+    sha1 = sha.new
+
+import utils
+import webapi as web
+
+__all__ = [
+    'Session', 'SessionExpired',
+    'Store', 'DiskStore', 'DBStore',
+]
+
+web.config.session_parameters = utils.storage({
+    'cookie_name': 'webpy_session_id',
+    'cookie_domain': None,
+    'timeout': 86400, #24 * 60 * 60, # 24 hours in seconds
+    'ignore_expiry': True,
+    'ignore_change_ip': True,
+    'secret_key': 'fLjUfxqXtfNoIldA0A0J',
+    'expired_message': 'Session expired',
+})
+
+class SessionExpired(web.HTTPError): 
+    def __init__(self, message):
+        web.HTTPError.__init__(self, '200 OK', {}, data=message)
+
+class Session(utils.ThreadedDict):
+    """Session management for web.py
+    """
+
+    def __init__(self, app, store, initializer=None):
+        self.__dict__['store'] = store
+        self.__dict__['_initializer'] = initializer
+        self.__dict__['_last_cleanup_time'] = 0
+        self.__dict__['_config'] = utils.storage(web.config.session_parameters)
+
+        if app:
+            app.add_processor(self._processor)
+
+    def _processor(self, handler):
+        """Application processor to setup session for every request"""
+        self._cleanup()
+        self._load()
+
+        try:
+            return handler()
+        finally:
+            self._save()
+
+    def _load(self):
+        """Load the session from the store, by the id from cookie"""
+        cookie_name = self._config.cookie_name
+        cookie_domain = self._config.cookie_domain
+        self.session_id = web.cookies().get(cookie_name)
+
+        # protection against session_id tampering
+        if self.session_id and not self._valid_session_id(self.session_id):
+            self.session_id = None
+
+        self._check_expiry()
+        if self.session_id:
+            d = self.store[self.session_id]
+            self.update(d)
+            self._validate_ip()
+        
+        if not self.session_id:
+            self.session_id = self._generate_session_id()
+
+            if self._initializer:
+                if isinstance(self._initializer, dict):
+                    self.update(self._initializer)
+                elif hasattr(self._initializer, '__call__'):
+                    self._initializer()
+ 
+        self.ip = web.ctx.ip
+
+    def _check_expiry(self):
+        # check for expiry
+        if self.session_id and self.session_id not in self.store:
+            if self._config.ignore_expiry:
+                self.session_id = None
+            else:
+                return self.expired()
+
+    def _validate_ip(self):
+        # check for change of IP
+        if self.session_id and self.get('ip', None) != web.ctx.ip:
+            if not self._config.ignore_change_ip:
+               return self.expired() 
+    
+    def _save(self):
+        cookie_name = self._config.cookie_name
+        cookie_domain = self._config.cookie_domain
+        if not self.get('_killed'):
+            web.setcookie(cookie_name, self.session_id, domain=cookie_domain)
+            self.store[self.session_id] = dict(self)
+        else:
+            web.setcookie(cookie_name, self.session_id, expires=-1, domain=cookie_domain)
+    
+    def _generate_session_id(self):
+        """Generate a random id for session"""
+
+        while True:
+            rand = os.urandom(16)
+            now = time.time()
+            secret_key = self._config.secret_key
+            session_id = sha1("%s%s%s%s" %(rand, now, utils.safestr(web.ctx.ip), secret_key))
+            session_id = session_id.hexdigest()
+            if session_id not in self.store:
+                break
+        return session_id
+
+    def _valid_session_id(self, session_id):
+        rx = utils.re_compile('^[0-9a-fA-F]+$')
+        return rx.match(session_id)
+        
+    def _cleanup(self):
+        """Cleanup the stored sessions"""
+        current_time = time.time()
+        timeout = self._config.timeout
+        if current_time - self._last_cleanup_time > timeout:
+            self.store.cleanup(timeout)
+            self.__dict__['_last_cleanup_time'] = current_time
+
+    def expired(self):
+        """Called when an expired session is atime"""
+        self._killed = True
+        self._save()
+        raise SessionExpired(self._config.expired_message)
+ 
+    def kill(self):
+        """Kill the session, make it no longer available"""
+        del self.store[self.session_id]
+        self._killed = True
+
+class Store:
+    """Base class for session stores"""
+
+    def __contains__(self, key):
+        raise NotImplementedError
+
+    def __getitem__(self, key):
+        raise NotImplementedError
+
+    def __setitem__(self, key, value):
+        raise NotImplementedError
+
+    def cleanup(self, timeout):
+        """removes all the expired sessions"""
+        raise NotImplementedError
+
+    def encode(self, session_dict):
+        """encodes session dict as a string"""
+        pickled = pickle.dumps(session_dict)
+        return base64.encodestring(pickled)
+
+    def decode(self, session_data):
+        """decodes the data to get back the session dict """
+        pickled = base64.decodestring(session_data)
+        return pickle.loads(pickled)
+
+class DiskStore(Store):
+    """
+    Store for saving a session on disk.
+
+        >>> import tempfile
+        >>> root = tempfile.mkdtemp()
+        >>> s = DiskStore(root)
+        >>> s['a'] = 'foo'
+        >>> s['a']
+        'foo'
+        >>> time.sleep(0.01)
+        >>> s.cleanup(0.01)
+        >>> s['a']
+        Traceback (most recent call last):
+            ...
+        KeyError: 'a'
+    """
+    def __init__(self, root):
+        # if the storage root doesn't exists, create it.
+        if not os.path.exists(root):
+            os.mkdir(root)
+        self.root = root
+
+    def _get_path(self, key):
+        if os.path.sep in key: 
+            raise ValueError, "Bad key: %s" % repr(key)
+        return os.path.join(self.root, key)
+    
+    def __contains__(self, key):
+        path = self._get_path(key)
+        return os.path.exists(path)
+
+    def __getitem__(self, key):
+        path = self._get_path(key)
+        if os.path.exists(path): 
+            pickled = open(path).read()
+            return self.decode(pickled)
+        else:
+            raise KeyError, key
+
+    def __setitem__(self, key, value):
+        path = self._get_path(key)
+        pickled = self.encode(value)    
+        try:
+            f = open(path, 'w')
+            try:
+                f.write(pickled)
+            finally: 
+                f.close()
+        except IOError:
+            pass
+
+    def __delitem__(self, key):
+        path = self._get_path(key)
+        if os.path.exists(path):
+            os.remove(path)
+    
+    def cleanup(self, timeout):
+        now = time.time()
+        for f in os.listdir(self.root):
+            path = self._get_path(f)
+            atime = os.stat(path).st_atime
+            if now - atime > timeout :
+                os.remove(path)
+
+class DBStore(Store):
+    """Store for saving a session in database
+    Needs a table with the following columns:
+
+        session_id CHAR(128) UNIQUE NOT NULL,
+        atime DATETIME NOT NULL default current_timestamp,
+        data TEXT
+    """
+    def __init__(self, db, table_name):
+        self.db = db
+        self.table = table_name
+    
+    def __contains__(self, key):
+        data = self.db.select(self.table, where="session_id=$key", vars=locals())
+        return bool(list(data)) 
+
+    def __getitem__(self, key):
+        now = datetime.datetime.now()
+        try:
+            s = self.db.select(self.table, where="session_id=$key", vars=locals())[0]
+            self.db.update(self.table, where="session_id=$key", atime=now, vars=locals())
+        except IndexError:
+            raise KeyError
+        else:
+            return self.decode(s.data)
+
+    def __setitem__(self, key, value):
+        pickled = self.encode(value)
+        now = datetime.datetime.now()
+        if key in self:
+            self.db.update(self.table, where="session_id=$key", data=pickled, vars=locals())
+        else:
+            self.db.insert(self.table, False, session_id=key, data=pickled )
+                
+    def __delitem__(self, key):
+        self.db.delete(self.table, where="session_id=$key", vars=locals())
+
+    def cleanup(self, timeout):
+        timeout = datetime.timedelta(timeout/(24.0*60*60)) #timedelta takes numdays as arg
+        last_allowed_time = datetime.datetime.now() - timeout
+        self.db.delete(self.table, where="$last_allowed_time > atime", vars=locals())
+
+class ShelfStore:
+    """Store for saving session using `shelve` module.
+
+        import shelve
+        store = ShelfStore(shelve.open('session.shelf'))
+
+    XXX: is shelve thread-safe?
+    """
+    def __init__(self, shelf):
+        self.shelf = shelf
+
+    def __contains__(self, key):
+        return key in self.shelf
+
+    def __getitem__(self, key):
+        atime, v = self.shelf[key]
+        self[key] = v # update atime
+        return v
+
+    def __setitem__(self, key, value):
+        self.shelf[key] = time.time(), value
+        
+    def __delitem__(self, key):
+        try:
+            del self.shelf[key]
+        except KeyError:
+            pass
+
+    def cleanup(self, timeout):
+        now = time.time()
+        for k in self.shelf.keys():
+            atime, v = self.shelf[k]
+            if now - atime > timeout :
+                del self[k]
+
+if __name__ == '__main__' :
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/template.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,1412 @@
+"""
+simple, elegant templating
+(part of web.py)
+
+Template design:
+
+Template string is split into tokens and the tokens are combined into nodes. 
+Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and 
+for-loop, if-loop etc are block nodes, which contain multiple child nodes. 
+
+Each node can emit some python string. python string emitted by the 
+root node is validated for safeeval and executed using python in the given environment.
+
+Enough care is taken to make sure the generated code and the template has line to line match, 
+so that the error messages can point to exact line number in template. (It doesn't work in some cases still.)
+
+Grammar:
+
+    template -> defwith sections 
+    defwith -> '$def with (' arguments ')' | ''
+    sections -> section*
+    section -> block | assignment | line
+
+    assignment -> '$ ' <assignment expression>
+    line -> (text|expr)*
+    text -> <any characters other than $>
+    expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}'
+    pyexpr -> <python expression>
+
+"""
+
+__all__ = [
+    "Template",
+    "Render", "render", "frender",
+    "ParseError", "SecurityError",
+    "test"
+]
+
+import tokenize
+import os
+import glob
+import re
+
+from utils import storage, safeunicode, safestr, re_compile
+from webapi import config
+from net import websafe
+
+def splitline(text):
+    r"""
+    Splits the given text at newline.
+    
+        >>> splitline('foo\nbar')
+        ('foo\n', 'bar')
+        >>> splitline('foo')
+        ('foo', '')
+        >>> splitline('')
+        ('', '')
+    """
+    index = text.find('\n') + 1
+    if index:
+        return text[:index], text[index:]
+    else:
+        return text, ''
+
+class Parser:
+    """Parser Base.
+    """
+    def __init__(self, text, name="<template>"):
+        self.text = text
+        self.name = name
+
+    def parse(self):
+        text = self.text
+        defwith, text = self.read_defwith(text)
+        suite = self.read_suite(text)
+        return DefwithNode(defwith, suite)
+
+    def read_defwith(self, text):
+        if text.startswith('$def with'):
+            defwith, text = splitline(text)
+            defwith = defwith[1:].strip() # strip $ and spaces
+            return defwith, text
+        else:
+            return '', text
+    
+    def read_section(self, text):
+        r"""Reads one section from the given text.
+        
+        section -> block | assignment | line
+        
+            >>> read_section = Parser('').read_section
+            >>> read_section('foo\nbar\n')
+            (<line: [t'foo\n']>, 'bar\n')
+            >>> read_section('$ a = b + 1\nfoo\n')
+            (<assignment: 'a = b + 1'>, 'foo\n')
+            
+        read_section('$for in range(10):\n    hello $i\nfoo)
+        """
+        if text.lstrip(' ').startswith('$'):
+            index = text.index('$')
+            begin_indent, text2 = text[:index], text[index+1:]
+            ahead = self.python_lookahead(text2)
+            
+            if ahead == 'var':
+                return self.read_var(text2)
+            elif ahead in STATEMENT_NODES:
+                return self.read_block_section(text2, begin_indent)
+            elif ahead in KEYWORDS:
+                return self.read_keyword(text2)
+            elif ahead.strip() == '':
+                # assignments starts with a space after $
+                # ex: $ a = b + 2
+                return self.read_assignment(text2)
+        return self.readline(text)
+        
+    def read_var(self, text):
+        r"""Reads a var statement.
+        
+            >>> read_var = Parser('').read_var
+            >>> read_var('var x=10\nfoo')
+            (<var: x = 10>, 'foo')
+            >>> read_var('var x: hello $name\nfoo')
+            (<var: x = join_('hello ', escape_(name, True))>, 'foo')
+        """
+        line, text = splitline(text)
+        tokens = self.python_tokens(line)
+        if len(tokens) < 4:
+            raise SyntaxError('Invalid var statement')
+            
+        name = tokens[1]
+        sep = tokens[2]
+        value = line.split(sep, 1)[1].strip()
+        
+        if sep == '=':
+            pass # no need to process value
+        elif sep == ':': 
+            #@@ Hack for backward-compatability
+            if tokens[3] == '\n': # multi-line var statement
+                block, text = self.read_indented_block(text, '    ')
+                lines = [self.readline(x)[0] for x in block.splitlines()]
+                nodes = []
+                for x in lines:
+                    nodes.extend(x.nodes)
+                    nodes.append(TextNode('\n'))         
+            else: # single-line var statement
+                linenode, _ = self.readline(value)
+                nodes = linenode.nodes                
+            parts = [node.emit('') for node in nodes]
+            value = "join_(%s)" % ", ".join(parts)
+        else:
+            raise SyntaxError('Invalid var statement')
+        return VarNode(name, value), text
+                    
+    def read_suite(self, text):
+        r"""Reads section by section till end of text.
+        
+            >>> read_suite = Parser('').read_suite
+            >>> read_suite('hello $name\nfoo\n')
+            [<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>]
+        """
+        sections = []
+        while text:
+            section, text = self.read_section(text)
+            sections.append(section)
+        return SuiteNode(sections)
+    
+    def readline(self, text):
+        r"""Reads one line from the text. Newline is supressed if the line ends with \.
+        
+            >>> readline = Parser('').readline
+            >>> readline('hello $name!\nbye!')
+            (<line: [t'hello ', $name, t'!\n']>, 'bye!')
+            >>> readline('hello $name!\\\nbye!')
+            (<line: [t'hello ', $name, t'!']>, 'bye!')
+            >>> readline('$f()\n\n')
+            (<line: [$f(), t'\n']>, '\n')
+        """
+        line, text = splitline(text)
+
+        # supress new line if line ends with \
+        if line.endswith('\\\n'):
+            line = line[:-2]
+                
+        nodes = []
+        while line:
+            node, line = self.read_node(line)
+            nodes.append(node)
+            
+        return LineNode(nodes), text
+
+    def read_node(self, text):
+        r"""Reads a node from the given text and returns the node and remaining text.
+
+            >>> read_node = Parser('').read_node
+            >>> read_node('hello $name')
+            (t'hello ', '$name')
+            >>> read_node('$name')
+            ($name, '')
+        """
+        if text.startswith('$$'):
+            return TextNode('$'), text[2:]
+        elif text.startswith('$#'): # comment
+            line, text = splitline(text)
+            return TextNode('\n'), text
+        elif text.startswith('$'):
+            text = text[1:] # strip $
+            if text.startswith(':'):
+                escape = False
+                text = text[1:] # strip :
+            else:
+                escape = True
+            return self.read_expr(text, escape=escape)
+        else:
+            return self.read_text(text)
+    
+    def read_text(self, text):
+        r"""Reads a text node from the given text.
+        
+            >>> read_text = Parser('').read_text
+            >>> read_text('hello $name')
+            (t'hello ', '$name')
+        """
+        index = text.find('$')
+        if index < 0:
+            return TextNode(text), ''
+        else:
+            return TextNode(text[:index]), text[index:]
+            
+    def read_keyword(self, text):
+        line, text = splitline(text)
+        return CodeNode(None, line.strip() + "\n"), text
+
+    def read_expr(self, text, escape=True):
+        """Reads a python expression from the text and returns the expression and remaining text.
+
+        expr -> simple_expr | paren_expr
+        simple_expr -> id extended_expr
+        extended_expr -> attr_access | paren_expr extended_expr | ''
+        attr_access -> dot id extended_expr
+        paren_expr -> [ tokens ] | ( tokens ) | { tokens }
+     
+            >>> read_expr = Parser('').read_expr
+            >>> read_expr("name")
+            ($name, '')
+            >>> read_expr("a.b and c")
+            ($a.b, ' and c')
+            >>> read_expr("a. b")
+            ($a, '. b')
+            >>> read_expr("name</h1>")
+            ($name, '</h1>')
+            >>> read_expr("(limit)ing")
+            ($(limit), 'ing')
+            >>> read_expr('a[1, 2][:3].f(1+2, "weird string[).", 3 + 4) done.')
+            ($a[1, 2][:3].f(1+2, "weird string[).", 3 + 4), ' done.')
+        """
+        def simple_expr():
+            identifier()
+            extended_expr()
+        
+        def identifier():
+            tokens.next()
+        
+        def extended_expr():
+            lookahead = tokens.lookahead()
+            if lookahead is None:
+                return
+            elif lookahead.value == '.':
+                attr_access()
+            elif lookahead.value in parens:
+                paren_expr()
+                extended_expr()
+            else:
+                return
+        
+        def attr_access():
+            from token import NAME # python token constants
+            dot = tokens.lookahead()
+            if tokens.lookahead2().type == NAME:
+                tokens.next() # consume dot
+                identifier()
+                extended_expr()
+        
+        def paren_expr():
+            begin = tokens.next().value
+            end = parens[begin]
+            while True:
+                if tokens.lookahead().value in parens:
+                    paren_expr()
+                else:
+                    t = tokens.next()
+                    if t.value == end:
+                        break
+            return
+
+        parens = {
+            "(": ")",
+            "[": "]",
+            "{": "}"
+        }
+        
+        def get_tokens(text):
+            """tokenize text using python tokenizer.
+            Python tokenizer ignores spaces, but they might be important in some cases. 
+            This function introduces dummy space tokens when it identifies any ignored space.
+            Each token is a storage object containing type, value, begin and end.
+            """
+            readline = iter([text]).next
+            end = None
+            for t in tokenize.generate_tokens(readline):
+                t = storage(type=t[0], value=t[1], begin=t[2], end=t[3])
+                if end is not None and end != t.begin:
+                    _, x1 = end
+                    _, x2 = t.begin
+                    yield storage(type=-1, value=text[x1:x2], begin=end, end=t.begin)
+                end = t.end
+                yield t
+                
+        class BetterIter:
+            """Iterator like object with 2 support for 2 look aheads."""
+            def __init__(self, items):
+                self.iteritems = iter(items)
+                self.items = []
+                self.position = 0
+                self.current_item = None
+            
+            def lookahead(self):
+                if len(self.items) <= self.position:
+                    self.items.append(self._next())
+                return self.items[self.position]
+
+            def _next(self):
+                try:
+                    return self.iteritems.next()
+                except StopIteration:
+                    return None
+                
+            def lookahead2(self):
+                if len(self.items) <= self.position+1:
+                    self.items.append(self._next())
+                return self.items[self.position+1]
+                    
+            def next(self):
+                self.current_item = self.lookahead()
+                self.position += 1
+                return self.current_item
+
+        tokens = BetterIter(get_tokens(text))
+                
+        if tokens.lookahead().value in parens:
+            paren_expr()
+        else:
+            simple_expr()
+        row, col = tokens.current_item.end
+        return ExpressionNode(text[:col], escape=escape), text[col:]    
+
+    def read_assignment(self, text):
+        r"""Reads assignment statement from text.
+    
+            >>> read_assignment = Parser('').read_assignment
+            >>> read_assignment('a = b + 1\nfoo')
+            (<assignment: 'a = b + 1'>, 'foo')
+        """
+        line, text = splitline(text)
+        return AssignmentNode(line.strip()), text
+    
+    def python_lookahead(self, text):
+        """Returns the first python token from the given text.
+        
+            >>> python_lookahead = Parser('').python_lookahead
+            >>> python_lookahead('for i in range(10):')
+            'for'
+            >>> python_lookahead('else:')
+            'else'
+            >>> python_lookahead(' x = 1')
+            ' '
+        """
+        readline = iter([text]).next
+        tokens = tokenize.generate_tokens(readline)
+        return tokens.next()[1]
+        
+    def python_tokens(self, text):
+        readline = iter([text]).next
+        tokens = tokenize.generate_tokens(readline)
+        return [t[1] for t in tokens]
+        
+    def read_indented_block(self, text, indent):
+        r"""Read a block of text. A block is what typically follows a for or it statement.
+        It can be in the same line as that of the statement or an indented block.
+
+            >>> read_indented_block = Parser('').read_indented_block
+            >>> read_indented_block('  a\n  b\nc', '  ')
+            ('a\nb\n', 'c')
+            >>> read_indented_block('  a\n    b\n  c\nd', '  ')
+            ('a\n  b\nc\n', 'd')
+        """
+        if indent == '':
+            return '', text
+            
+        block = ""
+        while True:
+            if text.startswith(indent):
+                line, text = splitline(text)
+                block += line[len(indent):]
+            else:
+                break
+        return block, text
+
+    def read_statement(self, text):
+        r"""Reads a python statement.
+        
+            >>> read_statement = Parser('').read_statement
+            >>> read_statement('for i in range(10): hello $name')
+            ('for i in range(10):', ' hello $name')
+        """
+        tok = PythonTokenizer(text)
+        tok.consume_till(':')
+        return text[:tok.index], text[tok.index:]
+        
+    def read_block_section(self, text, begin_indent=''):
+        r"""
+            >>> read_block_section = Parser('').read_block_section
+            >>> read_block_section('for i in range(10): hello $i\nfoo')
+            (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
+            >>> read_block_section('for i in range(10):\n        hello $i\n    foo', begin_indent='    ')
+            (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, '    foo')
+            >>> read_block_section('for i in range(10):\n  hello $i\nfoo')
+            (<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
+        """
+        line, text = splitline(text)
+        stmt, line = self.read_statement(line)
+        keyword = self.python_lookahead(stmt)
+        
+        # if there is some thing left in the line
+        if line.strip():
+            block = line.lstrip()
+        else:
+            def find_indent(text):
+                rx = re_compile('  +')
+                match = rx.match(text)    
+                first_indent = match and match.group(0)
+                return first_indent or ""
+
+            # find the indentation of the block by looking at the first line
+            first_indent = find_indent(text)[len(begin_indent):]
+            indent = begin_indent + min(first_indent, INDENT)
+            
+            block, text = self.read_indented_block(text, indent)
+            
+        return self.create_block_node(keyword, stmt, block, begin_indent), text
+        
+    def create_block_node(self, keyword, stmt, block, begin_indent):
+        if keyword in STATEMENT_NODES:
+            return STATEMENT_NODES[keyword](stmt, block, begin_indent)
+        else:
+            raise ParseError, 'Unknown statement: %s' % repr(keyword)
+        
+class PythonTokenizer:
+    """Utility wrapper over python tokenizer."""
+    def __init__(self, text):
+        self.text = text
+        readline = iter([text]).next
+        self.tokens = tokenize.generate_tokens(readline)
+        self.index = 0
+        
+    def consume_till(self, delim):        
+        """Consumes tokens till colon.
+        
+            >>> tok = PythonTokenizer('for i in range(10): hello $i')
+            >>> tok.consume_till(':')
+            >>> tok.text[:tok.index]
+            'for i in range(10):'
+            >>> tok.text[tok.index:]
+            ' hello $i'
+        """
+        try:
+            while True:
+                t = self.next()
+                if t.value == delim:
+                    break
+                elif t.value == '(':
+                    self.consume_till(')')
+                elif t.value == '[':
+                    self.consume_till(']')
+                elif t.value == '{':
+                    self.consume_till('}')
+
+                # if end of line is found, it is an exception.
+                # Since there is no easy way to report the line number,
+                # leave the error reporting to the python parser later  
+                #@@ This should be fixed.
+                if t.value == '\n':
+                    break
+        except:
+            #raise ParseError, "Expected %s, found end of line." % repr(delim)
+
+            # raising ParseError doesn't show the line number. 
+            # if this error is ignored, then it will be caught when compiling the python code.
+            return
+    
+    def next(self):
+        type, t, begin, end, line = self.tokens.next()
+        row, col = end
+        self.index = col
+        return storage(type=type, value=t, begin=begin, end=end)
+        
+class DefwithNode:
+    def __init__(self, defwith, suite):
+        if defwith:
+            self.defwith = defwith.replace('with', '__template__') + ':'
+        else:
+            self.defwith = 'def __template__():'
+        self.suite = suite
+
+    def emit(self, indent):
+        return self.defwith + self.suite.emit(indent + INDENT)
+
+    def __repr__(self):
+        return "<defwith: %s, %s>" % (self.defwith, self.nodes)
+
+class TextNode:
+    def __init__(self, value):
+        self.value = value
+
+    def emit(self, indent):
+        return repr(self.value)
+        
+    def __repr__(self):
+        return 't' + repr(self.value)
+        
+class ExpressionNode:
+    def __init__(self, value, escape=True):
+        self.value = value.strip()
+        
+        # convert ${...} to $(...)
+        if value.startswith('{') and value.endswith('}'):
+            self.value = '(' + self.value[1:-1] + ')'
+            
+        self.escape = escape
+
+    def emit(self, indent):
+        return 'escape_(%s, %s)' % (self.value, bool(self.escape))
+        
+    def __repr__(self):
+        if self.escape:
+            escape = ''
+        else:
+            escape = ':'
+        return "$%s%s" % (escape, self.value)
+        
+class AssignmentNode:
+    def __init__(self, code):
+        self.code = code
+        
+    def emit(self, indent, begin_indent=''):
+        return indent + self.code + "\n"
+        
+    def __repr__(self):
+        return "<assignment: %s>" % repr(self.code)
+        
+class LineNode:
+    def __init__(self, nodes):
+        self.nodes = nodes
+        
+    def emit(self, indent, text_indent='', name=''):
+        text = [node.emit('') for node in self.nodes]
+        if text_indent:
+            text = [repr(text_indent)] + text
+        return indent + 'yield %s, join_(%s)\n' % (repr(name), ', '.join(text))
+    
+    def __repr__(self):
+        return "<line: %s>" % repr(self.nodes)
+
+INDENT = '    ' # 4 spaces
+        
+class BlockNode:
+    def __init__(self, stmt, block, begin_indent=''):
+        self.stmt = stmt
+        self.suite = Parser('').read_suite(block)
+        self.begin_indent = begin_indent
+
+    def emit(self, indent, text_indent=''):
+        text_indent = self.begin_indent + text_indent
+        out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
+        return out
+        
+    def text(self):
+        return '${' + self.stmt + '}' + "".join([node.text(indent) for node in self.nodes])
+        
+    def __repr__(self):
+        return "<block: %s, %s>" % (repr(self.stmt), repr(self.nodelist))
+
+class ForNode(BlockNode):
+    def __init__(self, stmt, block, begin_indent=''):
+        self.original_stmt = stmt
+        tok = PythonTokenizer(stmt)
+        tok.consume_till('in')
+        a = stmt[:tok.index] # for i in
+        b = stmt[tok.index:-1] # rest of for stmt excluding :
+        stmt = a + ' loop.setup(' + b.strip() + '):'
+        BlockNode.__init__(self, stmt, block, begin_indent)
+        
+    def __repr__(self):
+        return "<block: %s, %s>" % (repr(self.original_stmt), repr(self.suite))
+
+class CodeNode:
+    def __init__(self, stmt, block, begin_indent=''):
+        self.code = block
+        
+    def emit(self, indent, text_indent=''):
+        import re
+        rx = re.compile('^', re.M)
+        return rx.sub(indent, self.code).rstrip(' ')
+        
+    def __repr__(self):
+        return "<code: %s>" % repr(self.code)
+        
+class IfNode(BlockNode):
+    pass
+
+class ElseNode(BlockNode):
+    pass
+
+class ElifNode(BlockNode):
+    pass
+
+class DefNode(BlockNode):
+    pass
+
+class VarNode:
+    def __init__(self, name, value):
+        self.name = name
+        self.value = value
+        
+    def emit(self, indent, text_indent):
+        return indent + 'yield %s, %s\n' % (repr(self.name), self.value)
+        
+    def __repr__(self):
+        return "<var: %s = %s>" % (self.name, self.value)
+
+class SuiteNode:
+    """Suite is a list of sections."""
+    def __init__(self, sections):
+        self.sections = sections
+        
+    def emit(self, indent, text_indent=''):
+        return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])
+        
+    def __repr__(self):
+        return repr(self.sections)
+
+STATEMENT_NODES = {
+    'for': ForNode,
+    'while': BlockNode,
+    'if': IfNode,
+    'elif': ElifNode,
+    'else': ElseNode,
+    'def': DefNode,
+    'code': CodeNode
+}
+
+KEYWORDS = [
+    "pass",
+    "break",
+    "continue",
+    "return"
+]
+
+TEMPLATE_BUILTIN_NAMES = [
+    "dict", "enumerate", "float", "int", "bool", "list", "long", "reversed", 
+    "set", "slice", "tuple", "xrange",
+    "abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex", 
+    "id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range",
+    "True", "False",
+    "None",
+    "__import__", # some c-libraries like datetime requires __import__ to present in the namespace
+]
+
+import __builtin__
+TEMPLATE_BUILTINS = dict([(name, getattr(__builtin__, name)) for name in TEMPLATE_BUILTIN_NAMES if name in __builtin__.__dict__])
+
+class ForLoop:
+    """
+    Wrapper for expression in for stament to support loop.xxx helpers.
+    
+        >>> loop = ForLoop()
+        >>> for x in loop.setup(['a', 'b', 'c']):
+        ...     print loop.index, loop.revindex, loop.parity, x
+        ...
+        1 3 odd a
+        2 2 even b
+        3 1 odd c
+        >>> loop.index
+        Traceback (most recent call last):
+            ...
+        AttributeError: index
+    """
+    def __init__(self):
+        self._ctx = None
+        
+    def __getattr__(self, name):
+        if self._ctx is None:
+            raise AttributeError, name
+        else:
+            return getattr(self._ctx, name)
+        
+    def setup(self, seq):        
+        self._push()
+        return self._ctx.setup(seq)
+        
+    def _push(self):
+        self._ctx = ForLoopContext(self, self._ctx)
+        
+    def _pop(self):
+        self._ctx = self._ctx.parent
+                
+class ForLoopContext:
+    """Stackable context for ForLoop to support nested for loops.
+    """
+    def __init__(self, forloop, parent):
+        self._forloop = forloop
+        self.parent = parent
+        
+    def setup(self, seq):
+        if hasattr(seq, '__len__'):
+            n = len(seq)
+        else:
+            n = 0
+            
+        self.index = 0
+        seq = iter(seq)
+        
+        # Pre python-2.5 does not support yield in try-except.
+        # This is a work-around to overcome that limitation.
+        def next(seq):
+            try:
+                return seq.next()
+            except:
+                self._forloop._pop()
+                raise
+        
+        while True:
+            self._next(self.index + 1, n)
+            yield next(seq)
+            
+    def _next(self, i, n):
+        self.index = i
+        self.index0 = i - 1
+        self.first = (i == 1)
+        self.last = (i == n)
+        self.odd = (i % 2 == 1)
+        self.even = (i % 2 == 0)
+        self.parity = ['odd', 'even'][self.even]
+        if n:
+            self.length = n
+            self.revindex0 = n - i
+            self.revindex = self.revindex0 + 1
+        
+class BaseTemplate:
+    def __init__(self, code, filename, filter, globals, builtins):
+        self.filename = filename
+        self.filter = filter
+        self._globals = globals
+        self._builtins = builtins
+        if code:
+            self.t = self._compile(code)
+        else:
+            self.t = lambda: ''
+        
+    def _compile(self, code):
+        env = self.make_env(self._globals or {}, self._builtins)
+        exec(code, env)
+        return env['__template__']
+
+    def __call__(self, *a, **kw):
+        out = self.t(*a, **kw)
+        return self._join_output(out)
+        
+    def _join_output(self, out):
+        d = TemplateResult()
+        data = []
+        
+        for name, value in out:
+            if name:
+                d[name] = value
+            else:
+                data.append(value)
+                            
+        d.__body__ = u"".join(data)
+        return d       
+
+    def make_env(self, globals, builtins):
+        return dict(globals,
+            __builtins__=builtins, 
+            loop=ForLoop(),
+            escape_=self._escape,
+            join_=self._join
+        )
+    
+    def _join(self, *items):
+        return u"".join([safeunicode(item) for item in items])
+        
+    def _escape(self, value, escape=False):
+        import types
+        if value is None: 
+            value = ''
+        elif isinstance(value, types.GeneratorType):
+            value = self._join_output(value)
+            
+        value = safeunicode(value)
+        if escape and self.filter:
+            value = self.filter(value)
+        return value
+
+class Template(BaseTemplate):
+    CONTENT_TYPES = {
+        '.html' : 'text/html; charset=utf-8',
+        '.xhtml' : 'application/xhtml+xml; charset=utf-8',
+        '.txt' : 'text/plain',
+    }
+    FILTERS = {
+        '.html': websafe,
+        '.xhtml': websafe,
+        '.xml': websafe
+    }
+    globals = {}
+    
+    def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None):
+        text = Template.normalize_text(text)
+        code = self.compile_template(text, filename)
+                
+        _, ext = os.path.splitext(filename)
+        filter = filter or self.FILTERS.get(ext, None)
+        self.content_type = self.CONTENT_TYPES.get(ext, None)
+
+        if globals is None:
+            globals = self.globals
+        if builtins is None:
+            builtins = TEMPLATE_BUILTINS
+                
+        BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)
+        
+    def normalize_text(text):
+        """Normalizes template text by correcting \r\n, tabs and BOM chars."""
+        text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
+        if not text.endswith('\n'):
+            text += '\n'
+
+        # ignore BOM chars at the begining of template
+        BOM = '\xef\xbb\xbf'
+        if isinstance(text, str) and text.startswith(BOM):
+            text = text[len(BOM):]
+        
+        # support fort \$ for backward-compatibility 
+        text = text.replace(r'\$', '$$')
+        return text
+    normalize_text = staticmethod(normalize_text)
+                
+    def __call__(self, *a, **kw):
+        import webapi as web
+        if 'headers' in web.ctx and self.content_type:
+            web.header('Content-Type', self.content_type, unique=True)
+            
+        return BaseTemplate.__call__(self, *a, **kw)
+        
+    def generate_code(text, filename):
+        # parse the text
+        rootnode = Parser(text, filename).parse()
+                
+        # generate python code from the parse tree
+        code = rootnode.emit(indent="").strip()
+        return safestr(code)
+        
+    generate_code = staticmethod(generate_code)
+        
+    def compile_template(self, template_string, filename):
+        code = Template.generate_code(template_string, filename)
+    
+        def get_source_line(filename, lineno):
+            try:
+                lines = open(filename).read().splitlines()
+                return lines[lineno]
+            except:
+                return None
+        
+        try:
+            # compile the code first to report the errors, if any, with the filename
+            compiled_code = compile(code, filename, 'exec')
+        except SyntaxError, e:
+            # display template line that caused the error along with the traceback.
+            try:
+                e.msg += '\n\nTemplate traceback:\n    File %s, line %s\n        %s' % \
+                    (repr(e.filename), e.lineno, get_source_line(e.filename, e.lineno-1))
+            except: 
+                pass
+            raise
+        
+        # make sure code is safe
+        import compiler
+        ast = compiler.parse(code)
+        SafeVisitor().walk(ast, filename)
+
+        return compiled_code
+        
+class CompiledTemplate(Template):
+    def __init__(self, f, filename):
+        Template.__init__(self, '', filename)
+        self.t = f
+        
+    def compile_template(self, *a):
+        return None
+    
+    def _compile(self, *a):
+        return None
+                
+class Render:
+    """The most preferred way of using templates.
+    
+        render = web.template.render('templates')
+        print render.foo()
+        
+    Optional parameter can be `base` can be used to pass output of 
+    every template through the base template.
+    
+        render = web.template.render('templates', base='layout')
+    """
+    def __init__(self, loc='templates', cache=None, base=None, **keywords):
+        self._loc = loc
+        self._keywords = keywords
+
+        if cache is None:
+            cache = not config.get('debug', False)
+        
+        if cache:
+            self._cache = {}
+        else:
+            self._cache = None
+        
+        if base and not hasattr(base, '__call__'):
+            # make base a function, so that it can be passed to sub-renders
+            self._base = lambda page: self._template(base)(page)
+        else:
+            self._base = base
+            
+    def _lookup(self, name):
+        path = os.path.join(self._loc, name)
+        if os.path.isdir(path):
+            return 'dir', path
+        else:
+            path = self._findfile(path)
+            if path:
+                return 'file', path
+            else:
+                return 'none', None
+        
+    def _load_template(self, name):
+        kind, path = self._lookup(name)
+        
+        if kind == 'dir':
+            return Render(path, cache=self._cache is not None, base=self._base, **self._keywords)
+        elif kind == 'file':
+            return Template(open(path).read(), filename=path, **self._keywords)
+        else:
+            raise AttributeError, "No template named " + name            
+
+    def _findfile(self, path_prefix): 
+        p = [f for f in glob.glob(path_prefix + '.*') if not f.endswith('~')] # skip backup files
+        return p and p[0]
+            
+    def _template(self, name):
+        if self._cache is not None:
+            if name not in self._cache:
+                self._cache[name] = self._load_template(name)
+            return self._cache[name]
+        else:
+            return self._load_template(name)
+        
+    def __getattr__(self, name):
+        t = self._template(name)
+        if self._base and isinstance(t, Template):
+            def template(*a, **kw):
+                return self._base(t(*a, **kw))
+            return template
+        else:
+            return self._template(name)
+
+class GAE_Render(Render):
+    # Render gets over-written. make a copy here.
+    super = Render
+    def __init__(self, loc, *a, **kw):
+        GAE_Render.super.__init__(self, loc, *a, **kw)
+        
+        import types
+        if isinstance(loc, types.ModuleType):
+            self.mod = loc
+        else:
+            name = loc.rstrip('/').replace('/', '.')
+            self.mod = __import__(name, None, None, ['x'])
+
+        self.mod.__dict__.update(kw.get('builtins', TEMPLATE_BUILTINS))
+        self.mod.__dict__.update(Template.globals)
+        self.mod.__dict__.update(kw.get('globals', {}))
+
+    def _load_template(self, name):
+        t = getattr(self.mod, name)
+        import types
+        if isinstance(t, types.ModuleType):
+            return GAE_Render(t, cache=self._cache is not None, base=self._base, **self._keywords)
+        else:
+            return t
+
+render = Render
+# setup render for Google App Engine.
+try:
+    from google import appengine
+    render = Render = GAE_Render
+except ImportError:
+    pass
+        
+def frender(path, **keywords):
+    """Creates a template from the given file path.
+    """
+    return Template(open(path).read(), filename=path, **keywords)
+    
+def compile_templates(root):
+    """Compiles templates to python code."""
+    re_start = re_compile('^', re.M)
+    
+    for dirpath, dirnames, filenames in os.walk(root):
+        filenames = [f for f in filenames if not f.startswith('.') and not f.endswith('~') and not f.startswith('__init__.py')]
+
+        for d in dirnames[:]:
+            if d.startswith('.'):
+                dirnames.remove(d) # don't visit this dir
+
+        out = open(os.path.join(dirpath, '__init__.py'), 'w')
+        out.write('from web.template import CompiledTemplate, ForLoop\n\n')
+        if dirnames:
+            out.write("import " + ", ".join(dirnames))
+
+        for f in filenames:
+            path = os.path.join(dirpath, f)
+
+            if '.' in f:
+                name, _ = f.split('.', 1)
+            else:
+                name = f
+                
+            text = open(path).read()
+            text = Template.normalize_text(text)
+            code = Template.generate_code(text, path)
+            code = re_start.sub('    ', code)
+                        
+            _gen = '' + \
+            '\ndef %s():' + \
+            '\n    loop = ForLoop()' + \
+            '\n    _dummy  = CompiledTemplate(lambda: None, "dummy")' + \
+            '\n    join_ = _dummy._join' + \
+            '\n    escape_ = _dummy._escape' + \
+            '\n' + \
+            '\n%s' + \
+            '\n    return __template__'
+            
+            gen_code = _gen % (name, code)
+            out.write(gen_code)
+            out.write('\n\n')
+            out.write('%s = CompiledTemplate(%s(), %s)\n\n' % (name, name, repr(path)))
+
+            # create template to make sure it compiles
+            t = Template(open(path).read(), path)
+        out.close()
+                
+class ParseError(Exception):
+    pass
+    
+class SecurityError(Exception):
+    """The template seems to be trying to do something naughty."""
+    pass
+
+# Enumerate all the allowed AST nodes
+ALLOWED_AST_NODES = [
+    "Add", "And",
+#   "AssAttr",
+    "AssList", "AssName", "AssTuple",
+#   "Assert",
+    "Assign", "AugAssign",
+#   "Backquote",
+    "Bitand", "Bitor", "Bitxor", "Break",
+    "CallFunc","Class", "Compare", "Const", "Continue",
+    "Decorators", "Dict", "Discard", "Div",
+    "Ellipsis", "EmptyNode",
+#   "Exec",
+    "Expression", "FloorDiv", "For",
+#   "From",
+    "Function", 
+    "GenExpr", "GenExprFor", "GenExprIf", "GenExprInner",
+    "Getattr", 
+#   "Global", 
+    "If", "IfExp",
+#   "Import",
+    "Invert", "Keyword", "Lambda", "LeftShift",
+    "List", "ListComp", "ListCompFor", "ListCompIf", "Mod",
+    "Module",
+    "Mul", "Name", "Not", "Or", "Pass", "Power",
+#   "Print", "Printnl", "Raise",
+    "Return", "RightShift", "Slice", "Sliceobj",
+    "Stmt", "Sub", "Subscript",
+#   "TryExcept", "TryFinally",
+    "Tuple", "UnaryAdd", "UnarySub",
+    "While", "With", "Yield",
+]
+
+class SafeVisitor(object):
+    """
+    Make sure code is safe by walking through the AST.
+    
+    Code considered unsafe if:
+        * it has restricted AST nodes
+        * it is trying to access resricted attributes   
+        
+    Adopted from http://www.zafar.se/bkz/uploads/safe.txt (public domain, Babar K. Zafar)
+    """
+    def __init__(self):
+        "Initialize visitor by generating callbacks for all AST node types."
+        self.errors = []
+
+    def walk(self, ast, filename):
+        "Validate each node in AST and raise SecurityError if the code is not safe."
+        self.filename = filename
+        self.visit(ast)
+        
+        if self.errors:        
+            raise SecurityError, '\n'.join([str(err) for err in self.errors])
+        
+    def visit(self, node, *args):
+        "Recursively validate node and all of its children."
+        def classname(obj):
+            return obj.__class__.__name__
+        nodename = classname(node)
+        fn = getattr(self, 'visit' + nodename, None)
+        
+        if fn:
+            fn(node, *args)
+        else:
+            if nodename not in ALLOWED_AST_NODES:
+                self.fail(node, *args)
+            
+        for child in node.getChildNodes():
+            self.visit(child, *args)
+
+    def visitName(self, node, *args):
+        "Disallow any attempts to access a restricted attr."
+        #self.assert_attr(node.getChildren()[0], node)
+        pass
+        
+    def visitGetattr(self, node, *args):
+        "Disallow any attempts to access a restricted attribute."
+        self.assert_attr(node.attrname, node)
+            
+    def assert_attr(self, attrname, node):
+        if self.is_unallowed_attr(attrname):
+            lineno = self.get_node_lineno(node)
+            e = SecurityError("%s:%d - access to attribute '%s' is denied" % (self.filename, lineno, attrname))
+            self.errors.append(e)
+
+    def is_unallowed_attr(self, name):
+        return name.startswith('_') \
+            or name.startswith('func_') \
+            or name.startswith('im_')
+            
+    def get_node_lineno(self, node):
+        return (node.lineno) and node.lineno or 0
+        
+    def fail(self, node, *args):
+        "Default callback for unallowed AST nodes."
+        lineno = self.get_node_lineno(node)
+        nodename = node.__class__.__name__
+        e = SecurityError("%s:%d - execution of '%s' statements is denied" % (self.filename, lineno, nodename))
+        self.errors.append(e)
+
+class TemplateResult(storage):
+    """Dictionary like object for storing template output.
+    
+    A template can specify key-value pairs in the output using 
+    `var` statements. Each `var` statement adds a new key to the 
+    template output and the main output is stored with key 
+    __body__.
+    
+        >>> d = TemplateResult(__body__='hello, world', x='foo')
+        >>> d
+        <TemplateResult: {'__body__': 'hello, world', 'x': 'foo'}>
+        >>> print d
+        hello, world
+    """
+    def __unicode__(self): 
+        return safeunicode(self.get('__body__', ''))
+    
+    def __str__(self):
+        return safestr(self.get('__body__', ''))
+        
+    def __repr__(self):
+        return "<TemplateResult: %s>" % dict.__repr__(self)
+    
+def test():
+    r"""Doctest for testing template module.
+
+    Define a utility function to run template test.
+    
+        >>> class TestResult(TemplateResult):
+        ...     def __repr__(self): return repr(unicode(self))
+        ...
+        >>> def t(code, **keywords):
+        ...     tmpl = Template(code, **keywords)
+        ...     return lambda *a, **kw: TestResult(tmpl(*a, **kw))
+        ...
+    
+    Simple tests.
+    
+        >>> t('1')()
+        u'1\n'
+        >>> t('$def with ()\n1')()
+        u'1\n'
+        >>> t('$def with (a)\n$a')(1)
+        u'1\n'
+        >>> t('$def with (a=0)\n$a')(1)
+        u'1\n'
+        >>> t('$def with (a=0)\n$a')(a=1)
+        u'1\n'
+    
+    Test complicated expressions.
+        
+        >>> t('$def with (x)\n$x.upper()')('hello')
+        u'HELLO\n'
+        >>> t('$(2 * 3 + 4 * 5)')()
+        u'26\n'
+        >>> t('${2 * 3 + 4 * 5}')()
+        u'26\n'
+        >>> t('$def with (limit)\nkeep $(limit)ing.')('go')
+        u'keep going.\n'
+        >>> t('$def with (a)\n$a.b[0]')(storage(b=[1]))
+        u'1\n'
+        
+    Test html escaping.
+    
+        >>> t('$def with (x)\n$x', filename='a.html')('<html>')
+        u'&lt;html&gt;\n'
+        >>> t('$def with (x)\n$x', filename='a.txt')('<html>')
+        u'<html>\n'
+                
+    Test if, for and while.
+    
+        >>> t('$if 1: 1')()
+        u'1\n'
+        >>> t('$if 1:\n    1')()
+        u'1\n'
+        >>> t('$if 1:\n    1\\')()
+        u'1'
+        >>> t('$if 0: 0\n$elif 1: 1')()
+        u'1\n'
+        >>> t('$if 0: 0\n$elif None: 0\n$else: 1')()
+        u'1\n'
+        >>> t('$if 0 < 1 and 1 < 2: 1')()
+        u'1\n'
+        >>> t('$for x in [1, 2, 3]: $x')()
+        u'1\n2\n3\n'
+        >>> t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1})
+        u'1\n'
+        >>> t('$for x in [1, 2, 3]:\n\t$x')()
+        u'    1\n    2\n    3\n'
+        >>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3])
+        u'1\n1\n1\n'
+
+    The space after : must be ignored.
+    
+        >>> t('$if True: foo')()
+        u'foo\n'
+    
+    Test loop.xxx.
+
+        >>> t("$for i in range(5):$loop.index, $loop.parity")()
+        u'1, odd\n2, even\n3, odd\n4, even\n5, odd\n'
+        >>> t("$for i in range(2):\n    $for j in range(2):$loop.parent.parity $loop.parity")()
+        u'odd odd\nodd even\neven odd\neven even\n'
+        
+    Test assignment.
+    
+        >>> t('$ a = 1\n$a')()
+        u'1\n'
+        >>> t('$ a = [1]\n$a[0]')()
+        u'1\n'
+        >>> t('$ a = {1: 1}\n$a.keys()[0]')()
+        u'1\n'
+        >>> t('$ a = []\n$if not a: 1')()
+        u'1\n'
+        >>> t('$ a = {}\n$if not a: 1')()
+        u'1\n'
+        >>> t('$ a = -1\n$a')()
+        u'-1\n'
+        >>> t('$ a = "1"\n$a')()
+        u'1\n'
+
+    Test comments.
+    
+        >>> t('$# 0')()
+        u'\n'
+        >>> t('hello$#comment1\nhello$#comment2')()
+        u'hello\nhello\n'
+        >>> t('$#comment0\nhello$#comment1\nhello$#comment2')()
+        u'\nhello\nhello\n'
+        
+    Test unicode.
+    
+        >>> t('$def with (a)\n$a')(u'\u203d')
+        u'\u203d\n'
+        >>> t('$def with (a)\n$a')(u'\u203d'.encode('utf-8'))
+        u'\u203d\n'
+        >>> t(u'$def with (a)\n$a $:a')(u'\u203d')
+        u'\u203d \u203d\n'
+        >>> t(u'$def with ()\nfoo')()
+        u'foo\n'
+        >>> def f(x): return x
+        ...
+        >>> t(u'$def with (f)\n$:f("x")')(f)
+        u'x\n'
+        >>> t('$def with (f)\n$:f("x")')(f)
+        u'x\n'
+    
+    Test dollar escaping.
+    
+        >>> t("Stop, $$money isn't evaluated.")()
+        u"Stop, $money isn't evaluated.\n"
+        >>> t("Stop, \$money isn't evaluated.")()
+        u"Stop, $money isn't evaluated.\n"
+        
+    Test space sensitivity.
+    
+        >>> t('$def with (x)\n$x')(1)
+        u'1\n'
+        >>> t('$def with(x ,y)\n$x')(1, 1)
+        u'1\n'
+        >>> t('$(1 + 2*3 + 4)')()
+        u'11\n'
+        
+    Make sure globals are working.
+            
+        >>> t('$x')()
+        Traceback (most recent call last):
+            ...
+        NameError: global name 'x' is not defined
+        >>> t('$x', globals={'x': 1})()
+        u'1\n'
+        
+    Can't change globals.
+    
+        >>> t('$ x = 2\n$x', globals={'x': 1})()
+        u'2\n'
+        >>> t('$ x = x + 1\n$x', globals={'x': 1})()
+        Traceback (most recent call last):
+            ...
+        UnboundLocalError: local variable 'x' referenced before assignment
+    
+    Make sure builtins are customizable.
+    
+        >>> t('$min(1, 2)')()
+        u'1\n'
+        >>> t('$min(1, 2)', builtins={})()
+        Traceback (most recent call last):
+            ...
+        NameError: global name 'min' is not defined
+        
+    Test vars.
+    
+        >>> x = t('$var x: 1')()
+        >>> x.x
+        u'1'
+        >>> x = t('$var x = 1')()
+        >>> x.x
+        1
+        >>> x = t('$var x:  \n    foo\n    bar')()
+        >>> x.x
+        u'foo\nbar\n'
+
+    Test BOM chars.
+
+        >>> t('\xef\xbb\xbf$def with(x)\n$x')('foo')
+        u'foo\n'
+
+    Test for with weird cases.
+
+        >>> t('$for i in range(10)[1:5]:\n    $i')()
+        u'1\n2\n3\n4\n'
+        >>> t("$for k, v in {'a': 1, 'b': 2}.items():\n    $k $v")()
+        u'a 1\nb 2\n'
+        >>> t("$for k, v in ({'a': 1, 'b': 2}.items():\n    $k $v")()
+        Traceback (most recent call last):
+            ...
+        SyntaxError: invalid syntax
+
+    Test datetime.
+
+        >>> import datetime
+        >>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1))
+        u'01 2009\n'
+    """
+    pass
+            
+if __name__ == "__main__":
+    import sys
+    if '--compile' in sys.argv:
+        compile_templates(sys.argv[2])
+    else:
+        import doctest
+        doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/test.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,51 @@
+"""test utilities
+(part of web.py)
+"""
+import unittest
+import sys, os
+import web
+
+TestCase = unittest.TestCase
+TestSuite = unittest.TestSuite
+
+def load_modules(names):
+    return [__import__(name, None, None, "x") for name in names]
+
+def module_suite(module, classnames=None):
+    """Makes a suite from a module."""
+    if classnames:
+        return unittest.TestLoader().loadTestsFromNames(classnames, module)
+    elif hasattr(module, 'suite'):
+        return module.suite()
+    else:
+        return unittest.TestLoader().loadTestsFromModule(module)
+
+def doctest_suite(module_names):
+    """Makes a test suite from doctests."""
+    import doctest
+    suite = TestSuite()
+    for mod in load_modules(module_names):
+        suite.addTest(doctest.DocTestSuite(mod))
+    return suite
+    
+def suite(module_names):
+    """Creates a suite from multiple modules."""
+    suite = TestSuite()
+    for mod in load_modules(module_names):
+        suite.addTest(module_suite(mod))
+    return suite
+
+def runTests(suite):
+    runner = unittest.TextTestRunner()
+    return runner.run(suite)
+
+def main(suite=None):
+    if not suite:
+        main_module = __import__('__main__')
+        # allow command line switches
+        args = [a for a in sys.argv[1:] if not a.startswith('-')]
+        suite = module_suite(main_module, args or None)
+
+    result = runTests(suite)
+    sys.exit(not result.wasSuccessful())
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/utils.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,1111 @@
+#!/usr/bin/env python
+"""
+General Utilities
+(part of web.py)
+"""
+
+__all__ = [
+  "Storage", "storage", "storify", 
+  "iters", 
+  "rstrips", "lstrips", "strips", 
+  "safeunicode", "safestr", "utf8",
+  "TimeoutError", "timelimit",
+  "Memoize", "memoize",
+  "re_compile", "re_subm",
+  "group", "uniq", "iterview",
+  "IterBetter", "iterbetter",
+  "dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd",
+  "listget", "intget", "datestr",
+  "numify", "denumify", "commify", "dateify",
+  "nthstr",
+  "CaptureStdout", "capturestdout", "Profile", "profile",
+  "tryall",
+  "ThreadedDict", "threadeddict",
+  "autoassign",
+  "to36",
+  "safemarkdown",
+  "sendmail"
+]
+
+import re, sys, time, threading, itertools
+
+try:
+    import subprocess
+except ImportError: 
+    subprocess = None
+
+try: import datetime
+except ImportError: pass
+
+try: set
+except NameError:
+    from sets import Set as set
+
+class Storage(dict):
+    """
+    A Storage object is like a dictionary except `obj.foo` can be used
+    in addition to `obj['foo']`.
+    
+        >>> o = storage(a=1)
+        >>> o.a
+        1
+        >>> o['a']
+        1
+        >>> o.a = 2
+        >>> o['a']
+        2
+        >>> del o.a
+        >>> o.a
+        Traceback (most recent call last):
+            ...
+        AttributeError: 'a'
+    
+    """
+    def __getattr__(self, key): 
+        try:
+            return self[key]
+        except KeyError, k:
+            raise AttributeError, k
+    
+    def __setattr__(self, key, value): 
+        self[key] = value
+    
+    def __delattr__(self, key):
+        try:
+            del self[key]
+        except KeyError, k:
+            raise AttributeError, k
+    
+    def __repr__(self):     
+        return '<Storage ' + dict.__repr__(self) + '>'
+
+storage = Storage
+
+def storify(mapping, *requireds, **defaults):
+    """
+    Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
+    d doesn't have all of the keys in `requireds` and using the default 
+    values for keys found in `defaults`.
+
+    For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
+    `storage({'a':1, 'b':2, 'c':3})`.
+    
+    If a `storify` value is a list (e.g. multiple values in a form submission), 
+    `storify` returns the last element of the list, unless the key appears in 
+    `defaults` as a list. Thus:
+    
+        >>> storify({'a':[1, 2]}).a
+        2
+        >>> storify({'a':[1, 2]}, a=[]).a
+        [1, 2]
+        >>> storify({'a':1}, a=[]).a
+        [1]
+        >>> storify({}, a=[]).a
+        []
+    
+    Similarly, if the value has a `value` attribute, `storify will return _its_
+    value, unless the key appears in `defaults` as a dictionary.
+    
+        >>> storify({'a':storage(value=1)}).a
+        1
+        >>> storify({'a':storage(value=1)}, a={}).a
+        <Storage {'value': 1}>
+        >>> storify({}, a={}).a
+        {}
+        
+    Optionally, keyword parameter `_unicode` can be passed to convert all values to unicode.
+    
+        >>> storify({'x': 'a'}, _unicode=True)
+        <Storage {'x': u'a'}>
+        >>> storify({'x': storage(value='a')}, x={}, _unicode=True)
+        <Storage {'x': <Storage {'value': 'a'}>}>
+        >>> storify({'x': storage(value='a')}, _unicode=True)
+        <Storage {'x': u'a'}>
+    """
+    _unicode = defaults.pop('_unicode', False)
+    def unicodify(s):
+        if _unicode and isinstance(s, str): return safeunicode(s)
+        else: return s
+        
+    def getvalue(x):
+        if hasattr(x, 'value'):
+            return unicodify(x.value)
+        else:
+            return unicodify(x)
+    
+    stor = Storage()
+    for key in requireds + tuple(mapping.keys()):
+        value = mapping[key]
+        if isinstance(value, list):
+            if isinstance(defaults.get(key), list):
+                value = [getvalue(x) for x in value]
+            else:
+                value = value[-1]
+        if not isinstance(defaults.get(key), dict):
+            value = getvalue(value)
+        if isinstance(defaults.get(key), list) and not isinstance(value, list):
+            value = [value]
+        setattr(stor, key, value)
+
+    for (key, value) in defaults.iteritems():
+        result = value
+        if hasattr(stor, key): 
+            result = stor[key]
+        if value == () and not isinstance(result, tuple): 
+            result = (result,)
+        setattr(stor, key, result)
+    
+    return stor
+
+iters = [list, tuple]
+import __builtin__
+if hasattr(__builtin__, 'set'):
+    iters.append(set)
+if hasattr(__builtin__, 'frozenset'):
+    iters.append(set)
+if sys.version_info < (2,6): # sets module deprecated in 2.6
+    try:
+        from sets import Set
+        iters.append(Set)
+    except ImportError: 
+        pass
+    
+class _hack(tuple): pass
+iters = _hack(iters)
+iters.__doc__ = """
+A list of iterable items (like lists, but not strings). Includes whichever
+of lists, tuples, sets, and Sets are available in this version of Python.
+"""
+
+def _strips(direction, text, remove):
+    if direction == 'l': 
+        if text.startswith(remove): 
+            return text[len(remove):]
+    elif direction == 'r':
+        if text.endswith(remove):   
+            return text[:-len(remove)]
+    else: 
+        raise ValueError, "Direction needs to be r or l."
+    return text
+
+def rstrips(text, remove):
+    """
+    removes the string `remove` from the right of `text`
+
+        >>> rstrips("foobar", "bar")
+        'foo'
+    
+    """
+    return _strips('r', text, remove)
+
+def lstrips(text, remove):
+    """
+    removes the string `remove` from the left of `text`
+    
+        >>> lstrips("foobar", "foo")
+        'bar'
+    
+    """
+    return _strips('l', text, remove)
+
+def strips(text, remove):
+    """
+    removes the string `remove` from the both sides of `text`
+
+        >>> strips("foobarfoo", "foo")
+        'bar'
+    
+    """
+    return rstrips(lstrips(text, remove), remove)
+
+def safeunicode(obj, encoding='utf-8'):
+    r"""
+    Converts any given object to unicode string.
+    
+        >>> safeunicode('hello')
+        u'hello'
+        >>> safeunicode(2)
+        u'2'
+        >>> safeunicode('\xe1\x88\xb4')
+        u'\u1234'
+    """
+    if isinstance(obj, unicode):
+        return obj
+    elif isinstance(obj, str):
+        return obj.decode(encoding)
+    else:
+        if hasattr(obj, '__unicode__'):
+            return unicode(obj)
+        else:
+            return str(obj).decode(encoding)
+    
+def safestr(obj, encoding='utf-8'):
+    r"""
+    Converts any given object to utf-8 encoded string. 
+    
+        >>> safestr('hello')
+        'hello'
+        >>> safestr(u'\u1234')
+        '\xe1\x88\xb4'
+        >>> safestr(2)
+        '2'
+    """
+    if isinstance(obj, unicode):
+        return obj.encode('utf-8')
+    elif isinstance(obj, str):
+        return obj
+    elif hasattr(obj, 'next') and hasattr(obj, '__iter__'): # iterator
+        return itertools.imap(safestr, obj)
+    else:
+        return str(obj)
+
+# for backward-compatibility
+utf8 = safestr
+    
+class TimeoutError(Exception): pass
+def timelimit(timeout):
+    """
+    A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
+    if it takes longer.
+    
+        >>> import time
+        >>> def meaningoflife():
+        ...     time.sleep(.2)
+        ...     return 42
+        >>> 
+        >>> timelimit(.1)(meaningoflife)()
+        Traceback (most recent call last):
+            ...
+        TimeoutError: took too long
+        >>> timelimit(1)(meaningoflife)()
+        42
+
+    _Caveat:_ The function isn't stopped after `timeout` seconds but continues 
+    executing in a separate thread. (There seems to be no way to kill a thread.)
+
+    inspired by <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878>
+    """
+    def _1(function):
+        def _2(*args, **kw):
+            class Dispatch(threading.Thread):
+                def __init__(self):
+                    threading.Thread.__init__(self)
+                    self.result = None
+                    self.error = None
+
+                    self.setDaemon(True)
+                    self.start()
+
+                def run(self):
+                    try:
+                        self.result = function(*args, **kw)
+                    except:
+                        self.error = sys.exc_info()
+
+            c = Dispatch()
+            c.join(timeout)
+            if c.isAlive():
+                raise TimeoutError, 'took too long'
+            if c.error:
+                raise c.error[0], c.error[1]
+            return c.result
+        return _2
+    return _1
+
+class Memoize:
+    """
+    'Memoizes' a function, caching its return values for each input.
+    
+        >>> import time
+        >>> def meaningoflife():
+        ...     time.sleep(.2)
+        ...     return 42
+        >>> fastlife = memoize(meaningoflife)
+        >>> meaningoflife()
+        42
+        >>> timelimit(.1)(meaningoflife)()
+        Traceback (most recent call last):
+            ...
+        TimeoutError: took too long
+        >>> fastlife()
+        42
+        >>> timelimit(.1)(fastlife)()
+        42
+    
+    """
+    def __init__(self, func): 
+        self.func = func
+        self.cache = {}
+    def __call__(self, *args, **keywords):
+        key = (args, tuple(keywords.items()))
+        if key not in self.cache: 
+            self.cache[key] = self.func(*args, **keywords)
+        return self.cache[key]
+
+memoize = Memoize
+
+re_compile = memoize(re.compile) #@@ threadsafe?
+re_compile.__doc__ = """
+A memoized version of re.compile.
+"""
+
+class _re_subm_proxy:
+    def __init__(self): 
+        self.match = None
+    def __call__(self, match): 
+        self.match = match
+        return ''
+
+def re_subm(pat, repl, string):
+    """
+    Like re.sub, but returns the replacement _and_ the match object.
+    
+        >>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
+        >>> t
+        'foooooolish'
+        >>> m.groups()
+        ('oooooo',)
+    """
+    compiled_pat = re_compile(pat)
+    proxy = _re_subm_proxy()
+    compiled_pat.sub(proxy.__call__, string)
+    return compiled_pat.sub(repl, string), proxy.match
+
+def group(seq, size): 
+    """
+    Returns an iterator over a series of lists of length size from iterable.
+
+        >>> list(group([1,2,3,4], 2))
+        [[1, 2], [3, 4]]
+    """
+    if not hasattr(seq, 'next'):  
+        seq = iter(seq)
+    while True: 
+        yield [seq.next() for i in xrange(size)]
+
+def uniq(seq):
+   """
+   Removes duplicate elements from a list.
+
+       >>> uniq([1,2,3,1,4,5,6])
+       [1, 2, 3, 4, 5, 6]
+   """
+   seen = set()
+   result = []
+   for item in seq:
+       if item in seen: continue
+       seen.add(item)
+       result.append(item)
+   return result
+
+def iterview(x):
+   """
+   Takes an iterable `x` and returns an iterator over it
+   which prints its progress to stderr as it iterates through.
+   """
+   WIDTH = 70
+
+   def plainformat(n, lenx):
+       return '%5.1f%% (%*d/%d)' % ((float(n)/lenx)*100, len(str(lenx)), n, lenx)
+
+   def bars(size, n, lenx):
+       val = int((float(n)*size)/lenx + 0.5)
+       if size - val:
+           spacing = ">" + (" "*(size-val))[1:]
+       else:
+           spacing = ""
+       return "[%s%s]" % ("="*val, spacing)
+
+   def eta(elapsed, n, lenx):
+       if n == 0:
+           return '--:--:--'
+       if n == lenx:
+           secs = int(elapsed)
+       else:
+           secs = int((elapsed/n) * (lenx-n))
+       mins, secs = divmod(secs, 60)
+       hrs, mins = divmod(mins, 60)
+
+       return '%02d:%02d:%02d' % (hrs, mins, secs)
+
+   def format(starttime, n, lenx):
+       out = plainformat(n, lenx) + ' '
+       if n == lenx:
+           end = '     '
+       else:
+           end = ' ETA '
+       end += eta(time.time() - starttime, n, lenx)
+       out += bars(WIDTH - len(out) - len(end), n, lenx)
+       out += end
+       return out
+
+   starttime = time.time()
+   lenx = len(x)
+   for n, y in enumerate(x):
+       sys.stderr.write('\r' + format(starttime, n, lenx))
+       yield y
+   sys.stderr.write('\r' + format(starttime, n+1, lenx) + '\n')
+
+class IterBetter:
+    """
+    Returns an object that can be used as an iterator 
+    but can also be used via __getitem__ (although it 
+    cannot go backwards -- that is, you cannot request 
+    `iterbetter[0]` after requesting `iterbetter[1]`).
+    
+        >>> import itertools
+        >>> c = iterbetter(itertools.count())
+        >>> c[1]
+        1
+        >>> c[5]
+        5
+        >>> c[3]
+        Traceback (most recent call last):
+            ...
+        IndexError: already passed 3
+    """
+    def __init__(self, iterator): 
+        self.i, self.c = iterator, 0
+    def __iter__(self): 
+        while 1:    
+            yield self.i.next()
+            self.c += 1
+    def __getitem__(self, i):
+        #todo: slices
+        if i < self.c: 
+            raise IndexError, "already passed "+str(i)
+        try:
+            while i > self.c: 
+                self.i.next()
+                self.c += 1
+            # now self.c == i
+            self.c += 1
+            return self.i.next()
+        except StopIteration: 
+            raise IndexError, str(i)
+iterbetter = IterBetter
+
+def dictreverse(mapping):
+    """
+    Returns a new dictionary with keys and values swapped.
+    
+        >>> dictreverse({1: 2, 3: 4})
+        {2: 1, 4: 3}
+    """
+    return dict([(value, key) for (key, value) in mapping.iteritems()])
+
+def dictfind(dictionary, element):
+    """
+    Returns a key whose value in `dictionary` is `element` 
+    or, if none exists, None.
+    
+        >>> d = {1:2, 3:4}
+        >>> dictfind(d, 4)
+        3
+        >>> dictfind(d, 5)
+    """
+    for (key, value) in dictionary.iteritems():
+        if element is value: 
+            return key
+
+def dictfindall(dictionary, element):
+    """
+    Returns the keys whose values in `dictionary` are `element`
+    or, if none exists, [].
+    
+        >>> d = {1:4, 3:4}
+        >>> dictfindall(d, 4)
+        [1, 3]
+        >>> dictfindall(d, 5)
+        []
+    """
+    res = []
+    for (key, value) in dictionary.iteritems():
+        if element is value:
+            res.append(key)
+    return res
+
+def dictincr(dictionary, element):
+    """
+    Increments `element` in `dictionary`, 
+    setting it to one if it doesn't exist.
+    
+        >>> d = {1:2, 3:4}
+        >>> dictincr(d, 1)
+        3
+        >>> d[1]
+        3
+        >>> dictincr(d, 5)
+        1
+        >>> d[5]
+        1
+    """
+    dictionary.setdefault(element, 0)
+    dictionary[element] += 1
+    return dictionary[element]
+
+def dictadd(*dicts):
+    """
+    Returns a dictionary consisting of the keys in the argument dictionaries.
+    If they share a key, the value from the last argument is used.
+    
+        >>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
+        {1: 0, 2: 1, 3: 1}
+    """
+    result = {}
+    for dct in dicts:
+        result.update(dct)
+    return result
+
+def listget(lst, ind, default=None):
+    """
+    Returns `lst[ind]` if it exists, `default` otherwise.
+    
+        >>> listget(['a'], 0)
+        'a'
+        >>> listget(['a'], 1)
+        >>> listget(['a'], 1, 'b')
+        'b'
+    """
+    if len(lst)-1 < ind: 
+        return default
+    return lst[ind]
+
+def intget(integer, default=None):
+    """
+    Returns `integer` as an int or `default` if it can't.
+    
+        >>> intget('3')
+        3
+        >>> intget('3a')
+        >>> intget('3a', 0)
+        0
+    """
+    try:
+        return int(integer)
+    except (TypeError, ValueError):
+        return default
+
+def datestr(then, now=None):
+    """
+    Converts a (UTC) datetime object to a nice string representation.
+    
+        >>> from datetime import datetime, timedelta
+        >>> d = datetime(1970, 5, 1)
+        >>> datestr(d, now=d)
+        '0 microseconds ago'
+        >>> for t, v in {
+        ...   timedelta(microseconds=1): '1 microsecond ago',
+        ...   timedelta(microseconds=2): '2 microseconds ago',
+        ...   -timedelta(microseconds=1): '1 microsecond from now',
+        ...   -timedelta(microseconds=2): '2 microseconds from now',
+        ...   timedelta(microseconds=2000): '2 milliseconds ago',
+        ...   timedelta(seconds=2): '2 seconds ago',
+        ...   timedelta(seconds=2*60): '2 minutes ago',
+        ...   timedelta(seconds=2*60*60): '2 hours ago',
+        ...   timedelta(days=2): '2 days ago',
+        ... }.iteritems():
+        ...     assert datestr(d, now=d+t) == v
+        >>> datestr(datetime(1970, 1, 1), now=d)
+        'January  1'
+        >>> datestr(datetime(1969, 1, 1), now=d)
+        'January  1, 1969'
+        >>> datestr(datetime(1970, 6, 1), now=d)
+        'June  1, 1970'
+        >>> datestr(None)
+        ''
+    """
+    def agohence(n, what, divisor=None):
+        if divisor: n = n // divisor
+
+        out = str(abs(n)) + ' ' + what       # '2 day'
+        if abs(n) != 1: out += 's'           # '2 days'
+        out += ' '                           # '2 days '
+        if n < 0:
+            out += 'from now'
+        else:
+            out += 'ago'
+        return out                           # '2 days ago'
+
+    oneday = 24 * 60 * 60
+
+    if not then: return ""
+    if not now: now = datetime.datetime.utcnow()
+    if type(now).__name__ == "DateTime":
+        now = datetime.datetime.fromtimestamp(now)
+    if type(then).__name__ == "DateTime":
+        then = datetime.datetime.fromtimestamp(then)
+    elif type(then).__name__ == "date":
+        then = datetime.datetime(then.year, then.month, then.day)
+
+    delta = now - then
+    deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
+    deltadays = abs(deltaseconds) // oneday
+    if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
+
+    if deltadays:
+        if abs(deltadays) < 4:
+            return agohence(deltadays, 'day')
+
+        out = then.strftime('%B %e') # e.g. 'June 13'
+        if then.year != now.year or deltadays < 0:
+            out += ', %s' % then.year
+        return out
+
+    if int(deltaseconds):
+        if abs(deltaseconds) > (60 * 60):
+            return agohence(deltaseconds, 'hour', 60 * 60)
+        elif abs(deltaseconds) > 60:
+            return agohence(deltaseconds, 'minute', 60)
+        else:
+            return agohence(deltaseconds, 'second')
+
+    deltamicroseconds = delta.microseconds
+    if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
+    if abs(deltamicroseconds) > 1000:
+        return agohence(deltamicroseconds, 'millisecond', 1000)
+
+    return agohence(deltamicroseconds, 'microsecond')
+
+def numify(string):
+    """
+    Removes all non-digit characters from `string`.
+    
+        >>> numify('800-555-1212')
+        '8005551212'
+        >>> numify('800.555.1212')
+        '8005551212'
+    
+    """
+    return ''.join([c for c in str(string) if c.isdigit()])
+
+def denumify(string, pattern):
+    """
+    Formats `string` according to `pattern`, where the letter X gets replaced
+    by characters from `string`.
+    
+        >>> denumify("8005551212", "(XXX) XXX-XXXX")
+        '(800) 555-1212'
+    
+    """
+    out = []
+    for c in pattern:
+        if c == "X":
+            out.append(string[0])
+            string = string[1:]
+        else:
+            out.append(c)
+    return ''.join(out)
+
+def commify(n):
+    """
+    Add commas to an integer `n`.
+
+        >>> commify(1)
+        '1'
+        >>> commify(123)
+        '123'
+        >>> commify(1234)
+        '1,234'
+        >>> commify(1234567890)
+        '1,234,567,890'
+        >>> commify(123.0)
+        '123.0'
+        >>> commify(1234.5)
+        '1,234.5'
+        >>> commify(1234.56789)
+        '1,234.56789'
+        >>> commify('%.2f' % 1234.5)
+        '1,234.50'
+        >>> commify(None)
+        >>>
+
+    """
+    if n is None: return None
+    n = str(n)
+    if '.' in n:
+        dollars, cents = n.split('.')
+    else:
+        dollars, cents = n, None
+
+    r = []
+    for i, c in enumerate(str(dollars)[::-1]):
+        if i and (not (i % 3)):
+            r.insert(0, ',')
+        r.insert(0, c)
+    out = ''.join(r)
+    if cents:
+        out += '.' + cents
+    return out
+
+def dateify(datestring):
+    """
+    Formats a numified `datestring` properly.
+    """
+    return denumify(datestring, "XXXX-XX-XX XX:XX:XX")
+
+
+def nthstr(n):
+    """
+    Formats an ordinal.
+    Doesn't handle negative numbers.
+
+        >>> nthstr(1)
+        '1st'
+        >>> nthstr(0)
+        '0th'
+        >>> [nthstr(x) for x in [2, 3, 4, 5, 10, 11, 12, 13, 14, 15]]
+        ['2nd', '3rd', '4th', '5th', '10th', '11th', '12th', '13th', '14th', '15th']
+        >>> [nthstr(x) for x in [91, 92, 93, 94, 99, 100, 101, 102]]
+        ['91st', '92nd', '93rd', '94th', '99th', '100th', '101st', '102nd']
+        >>> [nthstr(x) for x in [111, 112, 113, 114, 115]]
+        ['111th', '112th', '113th', '114th', '115th']
+
+    """
+    
+    assert n >= 0
+    if n % 100 in [11, 12, 13]: return '%sth' % n
+    return {1: '%sst', 2: '%snd', 3: '%srd'}.get(n % 10, '%sth') % n
+
+def cond(predicate, consequence, alternative=None):
+    """
+    Function replacement for if-else to use in expressions.
+        
+        >>> x = 2
+        >>> cond(x % 2 == 0, "even", "odd")
+        'even'
+        >>> cond(x % 2 == 0, "even", "odd") + '_row'
+        'even_row'
+    """
+    if predicate:
+        return consequence
+    else:
+        return alternative
+
+class CaptureStdout:
+    """
+    Captures everything `func` prints to stdout and returns it instead.
+    
+        >>> def idiot():
+        ...     print "foo"
+        >>> capturestdout(idiot)()
+        'foo\\n'
+    
+    **WARNING:** Not threadsafe!
+    """
+    def __init__(self, func): 
+        self.func = func
+    def __call__(self, *args, **keywords):
+        from cStringIO import StringIO
+        # Not threadsafe!
+        out = StringIO()
+        oldstdout = sys.stdout
+        sys.stdout = out
+        try: 
+            self.func(*args, **keywords)
+        finally: 
+            sys.stdout = oldstdout
+        return out.getvalue()
+
+capturestdout = CaptureStdout
+
+class Profile:
+    """
+    Profiles `func` and returns a tuple containing its output
+    and a string with human-readable profiling information.
+        
+        >>> import time
+        >>> out, inf = profile(time.sleep)(.001)
+        >>> out
+        >>> inf[:10].strip()
+        'took 0.0'
+    """
+    def __init__(self, func): 
+        self.func = func
+    def __call__(self, *args): ##, **kw):   kw unused
+        import hotshot, hotshot.stats, tempfile ##, time already imported
+        temp = tempfile.NamedTemporaryFile()
+        prof = hotshot.Profile(temp.name)
+
+        stime = time.time()
+        result = prof.runcall(self.func, *args)
+        stime = time.time() - stime
+        prof.close()
+
+        import cStringIO
+        out = cStringIO.StringIO()
+        stats = hotshot.stats.load(temp.name)
+        stats.stream = out
+        stats.strip_dirs()
+        stats.sort_stats('time', 'calls')
+        stats.print_stats(40)
+        stats.print_callers()
+
+        x =  '\n\ntook '+ str(stime) + ' seconds\n'
+        x += out.getvalue()
+
+        return result, x
+
+profile = Profile
+
+
+import traceback
+# hack for compatibility with Python 2.3:
+if not hasattr(traceback, 'format_exc'):
+    from cStringIO import StringIO
+    def format_exc(limit=None):
+        strbuf = StringIO()
+        traceback.print_exc(limit, strbuf)
+        return strbuf.getvalue()
+    traceback.format_exc = format_exc
+
+def tryall(context, prefix=None):
+    """
+    Tries a series of functions and prints their results. 
+    `context` is a dictionary mapping names to values; 
+    the value will only be tried if it's callable.
+    
+        >>> tryall(dict(j=lambda: True))
+        j: True
+        ----------------------------------------
+        results:
+           True: 1
+
+    For example, you might have a file `test/stuff.py` 
+    with a series of functions testing various things in it. 
+    At the bottom, have a line:
+
+        if __name__ == "__main__": tryall(globals())
+
+    Then you can run `python test/stuff.py` and get the results of 
+    all the tests.
+    """
+    context = context.copy() # vars() would update
+    results = {}
+    for (key, value) in context.iteritems():
+        if not hasattr(value, '__call__'): 
+            continue
+        if prefix and not key.startswith(prefix): 
+            continue
+        print key + ':',
+        try:
+            r = value()
+            dictincr(results, r)
+            print r
+        except:
+            print 'ERROR'
+            dictincr(results, 'ERROR')
+            print '   ' + '\n   '.join(traceback.format_exc().split('\n'))
+        
+    print '-'*40
+    print 'results:'
+    for (key, value) in results.iteritems():
+        print ' '*2, str(key)+':', value
+        
+class ThreadedDict:
+    """
+    Thread local storage.
+    
+        >>> d = ThreadedDict()
+        >>> d.x = 1
+        >>> d.x
+        1
+        >>> import threading
+        >>> def f(): d.x = 2
+        ...
+        >>> t = threading.Thread(target=f)
+        >>> t.start()
+        >>> t.join()
+        >>> d.x
+        1
+    """
+    def __getattr__(self, key):
+        return getattr(self._getd(), key)
+
+    def __setattr__(self, key, value):
+        return setattr(self._getd(), key, value)
+
+    def __delattr__(self, key):
+        return delattr(self._getd(), key)
+
+    def __hash__(self): 
+        return id(self)
+
+    def _getd(self):
+        t = threading.currentThread()
+        if not hasattr(t, '_d'):
+            # using __dict__ of thread as thread local storage
+            t._d = {}
+
+        # there could be multiple instances of ThreadedDict.
+        # use self as key
+        if self not in t._d:
+            t._d[self] = storage()
+        return t._d[self]
+
+threadeddict = ThreadedDict
+
+def autoassign(self, locals):
+    """
+    Automatically assigns local variables to `self`.
+    
+        >>> self = storage()
+        >>> autoassign(self, dict(a=1, b=2))
+        >>> self
+        <Storage {'a': 1, 'b': 2}>
+    
+    Generally used in `__init__` methods, as in:
+
+        def __init__(self, foo, bar, baz=1): autoassign(self, locals())
+    """
+    for (key, value) in locals.iteritems():
+        if key == 'self': 
+            continue
+        setattr(self, key, value)
+
+def to36(q):
+    """
+    Converts an integer to base 36 (a useful scheme for human-sayable IDs).
+    
+        >>> to36(35)
+        'z'
+        >>> to36(119292)
+        '2k1o'
+        >>> int(to36(939387374), 36)
+        939387374
+        >>> to36(0)
+        '0'
+        >>> to36(-393)
+        Traceback (most recent call last):
+            ... 
+        ValueError: must supply a positive integer
+    
+    """
+    if q < 0: raise ValueError, "must supply a positive integer"
+    letters = "0123456789abcdefghijklmnopqrstuvwxyz"
+    converted = []
+    while q != 0:
+        q, r = divmod(q, 36)
+        converted.insert(0, letters[r])
+    return "".join(converted) or '0'
+
+
+r_url = re_compile('(?<!\()(http://(\S+))')
+def safemarkdown(text):
+    """
+    Converts text to HTML following the rules of Markdown, but blocking any
+    outside HTML input, so that only the things supported by Markdown
+    can be used. Also converts raw URLs to links.
+
+    (requires [markdown.py](http://webpy.org/markdown.py))
+    """
+    from markdown import markdown
+    if text:
+        text = text.replace('<', '&lt;')
+        # TODO: automatically get page title?
+        text = r_url.sub(r'<\1>', text)
+        text = markdown(text)
+        return text
+
+def sendmail(from_address, to_address, subject, message, headers=None, **kw):
+    """
+    Sends the email message `message` with mail and envelope headers
+    for from `from_address_` to `to_address` with `subject`. 
+    Additional email headers can be specified with the dictionary 
+    `headers.
+
+    If `web.config.smtp_server` is set, it will send the message
+    to that SMTP server. Otherwise it will look for 
+    `/usr/sbin/sendmail`, the typical location for the sendmail-style
+    binary. To use sendmail from a different path, set `web.config.sendmail_path`.
+    """
+    try:
+        import webapi
+    except ImportError:
+        webapi = Storage(config=Storage())
+    
+    if headers is None: headers = {}
+    
+    cc = kw.get('cc', [])
+    bcc = kw.get('bcc', [])
+    
+    def listify(x):
+        if not isinstance(x, list):
+            return [safestr(x)]
+        else:
+            return [safestr(a) for a in x]
+
+    from_address = safestr(from_address)
+
+    to_address = listify(to_address)
+    cc = listify(cc)
+    bcc = listify(bcc)
+
+    recipients = to_address + cc + bcc
+    
+    headers = dictadd({
+      'MIME-Version': '1.0',
+      'Content-Type': 'text/plain; charset=UTF-8',
+      'Content-Disposition': 'inline',
+      'From': from_address,
+      'To': ", ".join(to_address),
+      'Subject': subject
+    }, headers)
+
+    if cc:
+        headers['Cc'] = ", ".join(cc)
+    
+    import email.Utils
+    from_address = email.Utils.parseaddr(from_address)[1]
+    recipients = [email.Utils.parseaddr(r)[1] for r in recipients]
+    message = ('\n'.join([safestr('%s: %s' % x) for x in headers.iteritems()])
+      + "\n\n" +  safestr(message))
+
+    if webapi.config.get('smtp_server'):
+        server = webapi.config.get('smtp_server')
+        port = webapi.config.get('smtp_port', 0)
+        username = webapi.config.get('smtp_username') 
+        password = webapi.config.get('smtp_password')
+        debug_level = webapi.config.get('smtp_debuglevel', None)
+        starttls = webapi.config.get('smtp_starttls', False)
+
+        import smtplib
+        smtpserver = smtplib.SMTP(server, port)
+
+        if debug_level:
+            smtpserver.set_debuglevel(debug_level)
+
+        if starttls:
+            smtpserver.ehlo()
+            smtpserver.starttls()
+            smtpserver.ehlo()
+
+        if username and password:
+            smtpserver.login(username, password)
+
+        smtpserver.sendmail(from_address, recipients, message)
+        smtpserver.quit()
+    else:
+        sendmail = webapi.config.get('sendmail_path', '/usr/sbin/sendmail')
+        
+        assert not from_address.startswith('-'), 'security'
+        for r in recipients:
+            assert not r.startswith('-'), 'security'
+                
+
+        if subprocess:
+            p = subprocess.Popen(['/usr/sbin/sendmail', '-f', from_address] + recipients, stdin=subprocess.PIPE)
+            p.stdin.write(message)
+            p.stdin.close()
+            p.wait()
+        else:
+            import os
+            i, o = os.popen2(["/usr/lib/sendmail", '-f', from_address] + recipients)
+            i.write(message)
+            i.close()
+            o.close()
+            del i, o
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/webapi.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,368 @@
+"""
+Web API (wrapper around WSGI)
+(from web.py)
+"""
+
+__all__ = [
+    "config",
+    "header", "debug",
+    "input", "data",
+    "setcookie", "cookies",
+    "ctx", 
+    "HTTPError", 
+
+    # 200, 201, 202
+    "OK", "Created", "Accepted",    
+    "ok", "created", "accepted",
+    
+    # 301, 302, 303, 304, 407
+    "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect", 
+    "redirect", "found", "seeother", "notmodified", "tempredirect",
+
+    # 400, 401, 403, 404, 405, 406, 409, 410, 412
+    "BadRequest", "Unauthorized", "Forbidden", "NoMethod", "NotFound", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed",
+    "badrequest", "unauthorized", "forbidden", "nomethod", "notfound", "notacceptable", "conflict", "gone", "preconditionfailed",
+
+    # 500
+    "InternalError", 
+    "internalerror",
+]
+
+import sys, cgi, Cookie, pprint, urlparse, urllib
+from utils import storage, storify, threadeddict, dictadd, intget, utf8
+
+config = storage()
+config.__doc__ = """
+A configuration object for various aspects of web.py.
+
+`debug`
+   : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
+"""
+
+class HTTPError(Exception):
+    def __init__(self, status, headers={}, data=""):
+        ctx.status = status
+        for k, v in headers.items():
+            header(k, v)
+        self.data = data
+        Exception.__init__(self, status)
+        
+def _status_code(status, data=None, classname=None, docstring=None):
+    if data is None:
+        data = status.split(" ", 1)[1]
+    classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified    
+    docstring = docstring or '`%s` status' % status
+
+    def __init__(self, data=data, headers={}):
+        HTTPError.__init__(self, status, headers, data)
+        
+    # trick to create class dynamically with dynamic docstring.
+    return type(classname, (HTTPError, object), {
+        '__doc__': docstring,
+        '__init__': __init__
+    })
+
+ok = OK = _status_code("200 OK", data="")
+created = Created = _status_code("201 Created")
+accepted = Accepted = _status_code("202 Accepted")
+
+class Redirect(HTTPError):
+    """A `301 Moved Permanently` redirect."""
+    def __init__(self, url, status='301 Moved Permanently', absolute=False):
+        """
+        Returns a `status` redirect to the new URL. 
+        `url` is joined with the base URL so that things like 
+        `redirect("about") will work properly.
+        """
+        newloc = urlparse.urljoin(ctx.path, url)
+
+        if newloc.startswith('/'):
+            if absolute:
+                home = ctx.realhome
+            else:
+                home = ctx.home
+            newloc = home + newloc
+
+        headers = {
+            'Content-Type': 'text/html',
+            'Location': newloc
+        }
+        HTTPError.__init__(self, status, headers, "")
+
+redirect = Redirect
+
+class Found(Redirect):
+    """A `302 Found` redirect."""
+    def __init__(self, url, absolute=False):
+        Redirect.__init__(self, url, '302 Found', absolute=absolute)
+
+found = Found
+
+class SeeOther(Redirect):
+    """A `303 See Other` redirect."""
+    def __init__(self, url, absolute=False):
+        Redirect.__init__(self, url, '303 See Other', absolute=absolute)
+    
+seeother = SeeOther
+
+class NotModified(HTTPError):
+    """A `304 Not Modified` status."""
+    def __init__(self):
+        HTTPError.__init__(self, "304 Not Modified")
+
+notmodified = NotModified
+
+class TempRedirect(Redirect):
+    """A `307 Temporary Redirect` redirect."""
+    def __init__(self, url, absolute=False):
+        Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
+
+tempredirect = TempRedirect
+
+class BadRequest(HTTPError):
+    """`400 Bad Request` error."""
+    message = "bad request"
+    def __init__(self):
+        status = "400 Bad Request"
+        headers = {'Content-Type': 'text/html'}
+        HTTPError.__init__(self, status, headers, self.message)
+
+badrequest = BadRequest
+
+class _NotFound(HTTPError):
+    """`404 Not Found` error."""
+    message = "not found"
+    def __init__(self, message=None):
+        status = '404 Not Found'
+        headers = {'Content-Type': 'text/html'}
+        HTTPError.__init__(self, status, headers, message or self.message)
+
+def NotFound(message=None):
+    """Returns HTTPError with '404 Not Found' error from the active application.
+    """
+    if message:
+        return _NotFound(message)
+    elif ctx.get('app_stack'):
+        return ctx.app_stack[-1].notfound()
+    else:
+        return _NotFound()
+
+notfound = NotFound
+
+unauthorized = Unauthorized = _status_code("401 Unauthorized")
+forbidden = Forbidden = _status_code("403 Forbidden")
+notacceptable = NotAcceptable = _status_code("406 Not Acceptable")
+conflict = Conflict = _status_code("409 Conflict")
+preconditionfailed = PreconditionFailed = _status_code("412 Precondition Failed")
+
+class NoMethod(HTTPError):
+    """A `405 Method Not Allowed` error."""
+    def __init__(self, cls=None):
+        status = '405 Method Not Allowed'
+        headers = {}
+        headers['Content-Type'] = 'text/html'
+        
+        methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
+        if cls:
+            methods = [method for method in methods if hasattr(cls, method)]
+
+        headers['Allow'] = ', '.join(methods)
+        data = None
+        HTTPError.__init__(self, status, headers, data)
+        
+nomethod = NoMethod
+
+class Gone(HTTPError):
+    """`410 Gone` error."""
+    message = "gone"
+    def __init__(self):
+        status = '410 Gone'
+        headers = {'Content-Type': 'text/html'}
+        HTTPError.__init__(self, status, headers, self.message)
+
+gone = Gone
+
+class _InternalError(HTTPError):
+    """500 Internal Server Error`."""
+    message = "internal server error"
+    
+    def __init__(self, message=None):
+        status = '500 Internal Server Error'
+        headers = {'Content-Type': 'text/html'}
+        HTTPError.__init__(self, status, headers, message or self.message)
+
+def InternalError(message=None):
+    """Returns HTTPError with '500 internal error' error from the active application.
+    """
+    if message:
+        return _InternalError(message)
+    elif ctx.get('app_stack'):
+        return ctx.app_stack[-1].internalerror()
+    else:
+        return _InternalError()
+
+internalerror = InternalError
+
+def header(hdr, value, unique=False):
+    """
+    Adds the header `hdr: value` with the response.
+    
+    If `unique` is True and a header with that name already exists,
+    it doesn't add a new one. 
+    """
+    hdr, value = utf8(hdr), utf8(value)
+    # protection against HTTP response splitting attack
+    if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
+        raise ValueError, 'invalid characters in header'
+        
+    if unique is True:
+        for h, v in ctx.headers:
+            if h.lower() == hdr.lower(): return
+    
+    ctx.headers.append((hdr, value))
+
+def input(*requireds, **defaults):
+    """
+    Returns a `storage` object with the GET and POST arguments. 
+    See `storify` for how `requireds` and `defaults` work.
+    """
+    from cStringIO import StringIO
+    def dictify(fs): 
+        # hack to make web.input work with enctype='text/plain.
+        if fs.list is None:
+            fs.list = [] 
+
+        return dict([(k, fs[k]) for k in fs.keys()])
+    
+    _method = defaults.pop('_method', 'both')
+    
+    e = ctx.env.copy()
+    a = b = {}
+    
+    if _method.lower() in ['both', 'post', 'put']:
+        if e['REQUEST_METHOD'] in ['POST', 'PUT']:
+            if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
+                # since wsgi.input is directly passed to cgi.FieldStorage, 
+                # it can not be called multiple times. Saving the FieldStorage
+                # object in ctx to allow calling web.input multiple times.
+                a = ctx.get('_fieldstorage')
+                if not a:
+                    fp = e['wsgi.input']
+                    a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
+                    ctx._fieldstorage = a
+            else:
+                fp = StringIO(data())
+                a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
+            a = dictify(a)
+
+    if _method.lower() in ['both', 'get']:
+        e['REQUEST_METHOD'] = 'GET'
+        b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
+
+    out = dictadd(b, a)
+    try:
+        defaults.setdefault('_unicode', True) # force unicode conversion by default.
+        return storify(out, *requireds, **defaults)
+    except KeyError:
+        raise badrequest()
+
+def data():
+    """Returns the data sent with the request."""
+    if 'data' not in ctx:
+        cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
+        ctx.data = ctx.env['wsgi.input'].read(cl)
+    return ctx.data
+
+def setcookie(name, value, expires="", domain=None, secure=False):
+    """Sets a cookie."""
+    if expires < 0: 
+        expires = -1000000000 
+    kargs = {'expires': expires, 'path':'/'}
+    if domain: 
+        kargs['domain'] = domain
+    if secure:
+        kargs['secure'] = secure
+    # @@ should we limit cookies to a different path?
+    cookie = Cookie.SimpleCookie()
+    cookie[name] = urllib.quote(utf8(value))
+    for key, val in kargs.iteritems(): 
+        cookie[name][key] = val
+    header('Set-Cookie', cookie.items()[0][1].OutputString())
+
+def cookies(*requireds, **defaults):
+    """
+    Returns a `storage` object with all the cookies in it.
+    See `storify` for how `requireds` and `defaults` work.
+    """
+    cookie = Cookie.SimpleCookie()
+    cookie.load(ctx.env.get('HTTP_COOKIE', ''))
+    try:
+        d = storify(cookie, *requireds, **defaults)
+        for k, v in d.items():
+            d[k] = v and urllib.unquote(v)
+        return d
+    except KeyError:
+        badrequest()
+        raise StopIteration
+
+def debug(*args):
+    """
+    Prints a prettyprinted version of `args` to stderr.
+    """
+    try: 
+        out = ctx.environ['wsgi.errors']
+    except: 
+        out = sys.stderr
+    for arg in args:
+        print >> out, pprint.pformat(arg)
+    return ''
+
+def _debugwrite(x):
+    try: 
+        out = ctx.environ['wsgi.errors']
+    except: 
+        out = sys.stderr
+    out.write(x)
+debug.write = _debugwrite
+
+ctx = context = threadeddict()
+
+ctx.__doc__ = """
+A `storage` object containing various information about the request:
+  
+`environ` (aka `env`)
+   : A dictionary containing the standard WSGI environment variables.
+
+`host`
+   : The domain (`Host` header) requested by the user.
+
+`home`
+   : The base path for the application.
+
+`ip`
+   : The IP address of the requester.
+
+`method`
+   : The HTTP method used.
+
+`path`
+   : The path request.
+   
+`query`
+   : If there are no query arguments, the empty string. Otherwise, a `?` followed
+     by the query string.
+
+`fullpath`
+   : The full path requested, including query arguments (`== path + query`).
+
+### Response Data
+
+`status` (default: "200 OK")
+   : The status code to be used in the response.
+
+`headers`
+   : A list of 2-tuples to be used in the response.
+
+`output`
+   : A string to be used as the response.
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/webopenid.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,115 @@
+"""openid.py: an openid library for web.py
+
+Notes:
+
+ - This will create a file called .openid_secret_key in the 
+   current directory with your secret key in it. If someone 
+   has access to this file they can log in as any user. And 
+   if the app can't find this file for any reason (e.g. you 
+   moved the app somewhere else) then each currently logged 
+   in user will get logged out.
+
+ - State must be maintained through the entire auth process 
+   -- this means that if you have multiple web.py processes 
+   serving one set of URLs or if you restart your app often 
+   then log ins will fail. You have to replace sessions and 
+   store for things to work.
+
+ - We set cookies starting with "openid_".
+
+"""
+
+import os
+import random
+import hmac
+import __init__ as web
+import openid.consumer.consumer
+import openid.store.memstore
+
+sessions = {}
+store = openid.store.memstore.MemoryStore()
+
+def _secret():
+    try:
+        secret = file('.openid_secret_key').read()
+    except IOError:
+        # file doesn't exist
+        secret = os.urandom(20)
+        file('.openid_secret_key', 'w').write(secret)
+    return secret
+
+def _hmac(identity_url):
+    return hmac.new(_secret(), identity_url).hexdigest()
+
+def _random_session():
+    n = random.random()
+    while n in sessions:
+        n = random.random()
+    n = str(n)
+    return n
+
+def status():
+    oid_hash = web.cookies().get('openid_identity_hash', '').split(',', 1)
+    if len(oid_hash) > 1:
+        oid_hash, identity_url = oid_hash
+        if oid_hash == _hmac(identity_url):
+            return identity_url
+    return None
+
+def form(openid_loc):
+    oid = status()
+    if oid:
+        return '''
+        <form method="post" action="%s">
+          <img src="http://openid.net/login-bg.gif" alt="OpenID" />
+          <strong>%s</strong>
+          <input type="hidden" name="action" value="logout" />
+          <input type="hidden" name="return_to" value="%s" />
+          <button type="submit">log out</button>
+        </form>''' % (openid_loc, oid, web.ctx.fullpath)
+    else:
+        return '''
+        <form method="post" action="%s">
+          <input type="text" name="openid" value="" 
+            style="background: url(http://openid.net/login-bg.gif) no-repeat; padding-left: 18px; background-position: 0 50%%;" />
+          <input type="hidden" name="return_to" value="%s" />
+          <button type="submit">log in</button>
+        </form>''' % (openid_loc, web.ctx.fullpath)
+
+def logout():
+    web.setcookie('openid_identity_hash', '', expires=-1)
+
+class host:
+    def POST(self):
+        # unlike the usual scheme of things, the POST is actually called
+        # first here
+        i = web.input(return_to='/')
+        if i.get('action') == 'logout':
+            logout()
+            return web.redirect(i.return_to)
+
+        i = web.input('openid', return_to='/')
+
+        n = _random_session()
+        sessions[n] = {'webpy_return_to': i.return_to}
+        
+        c = openid.consumer.consumer.Consumer(sessions[n], store)
+        a = c.begin(i.openid)
+        f = a.redirectURL(web.ctx.home, web.ctx.home + web.ctx.fullpath)
+
+        web.setcookie('openid_session_id', n)
+        return web.redirect(f)
+
+    def GET(self):
+        n = web.cookies('openid_session_id').openid_session_id
+        web.setcookie('openid_session_id', '', expires=-1)
+        return_to = sessions[n]['webpy_return_to']
+
+        c = openid.consumer.consumer.Consumer(sessions[n], store)
+        a = c.complete(web.input(), web.ctx.home + web.ctx.fullpath)
+
+        if a.status.lower() == 'success':
+            web.setcookie('openid_identity_hash', _hmac(a.identity_url) + ',' + a.identity_url)
+
+        del sessions[n]
+        return web.redirect(return_to)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/wsgi.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,66 @@
+"""
+WSGI Utilities
+(from web.py)
+"""
+
+import os, sys
+
+import http
+import webapi as web
+from utils import listget
+from net import validaddr, validip
+import httpserver
+    
+def runfcgi(func, addr=('localhost', 8000)):
+    """Runs a WSGI function as a FastCGI server."""
+    import flup.server.fcgi as flups
+    return flups.WSGIServer(func, multiplexed=True, bindAddress=addr).run()
+
+def runscgi(func, addr=('localhost', 4000)):
+    """Runs a WSGI function as an SCGI server."""
+    import flup.server.scgi as flups
+    return flups.WSGIServer(func, bindAddress=addr).run()
+
+def runwsgi(func):
+    """
+    Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
+    as appropriate based on context and `sys.argv`.
+    """
+    
+    if os.environ.has_key('SERVER_SOFTWARE'): # cgi
+        os.environ['FCGI_FORCE_CGI'] = 'Y'
+
+    if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
+      or os.environ.has_key('SERVER_SOFTWARE')):
+        return runfcgi(func, None)
+    
+    if 'fcgi' in sys.argv or 'fastcgi' in sys.argv:
+        args = sys.argv[1:]
+        if 'fastcgi' in args: args.remove('fastcgi')
+        elif 'fcgi' in args: args.remove('fcgi')
+        if args:
+            return runfcgi(func, validaddr(args[0]))
+        else:
+            return runfcgi(func, None)
+    
+    if 'scgi' in sys.argv:
+        args = sys.argv[1:]
+        args.remove('scgi')
+        if args:
+            return runscgi(func, validaddr(args[0]))
+        else:
+            return runscgi(func)
+    
+    return httpserver.runsimple(func, validip(listget(sys.argv, 1, '')))
+    
+def _is_dev_mode():
+    # quick hack to check if the program is running in dev mode.
+    if os.environ.has_key('SERVER_SOFTWARE') \
+        or os.environ.has_key('PHP_FCGI_CHILDREN') \
+        or 'fcgi' in sys.argv or 'fastcgi' in sys.argv \
+        or 'mod_wsgi' in sys.argv:
+            return False
+    return True
+
+# When running the builtin-server, enable debug mode if not already set.
+web.config.setdefault('debug', _is_dev_mode())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/wsgiserver/LICENSE.txt	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,25 @@
+Copyright (c) 2004-2007, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bundled/webpy/web/wsgiserver/__init__.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,1794 @@
+"""A high-speed, production ready, thread pooled, generic WSGI server.
+
+Simplest example on how to use this module directly
+(without using CherryPy's application machinery):
+
+    from cherrypy import wsgiserver
+    
+    def my_crazy_app(environ, start_response):
+        status = '200 OK'
+        response_headers = [('Content-type','text/plain')]
+        start_response(status, response_headers)
+        return ['Hello world!\n']
+    
+    server = wsgiserver.CherryPyWSGIServer(
+                ('0.0.0.0', 8070), my_crazy_app,
+                server_name='www.cherrypy.example')
+    
+The CherryPy WSGI server can serve as many WSGI applications 
+as you want in one instance by using a WSGIPathInfoDispatcher:
+    
+    d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
+    server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
+    
+Want SSL support? Just set these attributes:
+    
+    server.ssl_certificate = <filename>
+    server.ssl_private_key = <filename>
+    
+    if __name__ == '__main__':
+        try:
+            server.start()
+        except KeyboardInterrupt:
+            server.stop()
+
+This won't call the CherryPy engine (application side) at all, only the
+WSGI server, which is independant from the rest of CherryPy. Don't
+let the name "CherryPyWSGIServer" throw you; the name merely reflects
+its origin, not its coupling.
+
+For those of you wanting to understand internals of this module, here's the
+basic call flow. The server's listening thread runs a very tight loop,
+sticking incoming connections onto a Queue:
+
+    server = CherryPyWSGIServer(...)
+    server.start()
+    while True:
+        tick()
+        # This blocks until a request comes in:
+        child = socket.accept()
+        conn = HTTPConnection(child, ...)
+        server.requests.put(conn)
+
+Worker threads are kept in a pool and poll the Queue, popping off and then
+handling each connection in turn. Each connection can consist of an arbitrary
+number of requests and their responses, so we run a nested loop:
+
+    while True:
+        conn = server.requests.get()
+        conn.communicate()
+        ->  while True:
+                req = HTTPRequest(...)
+                req.parse_request()
+                ->  # Read the Request-Line, e.g. "GET /page HTTP/1.1"
+                    req.rfile.readline()
+                    req.read_headers()
+                req.respond()
+                ->  response = wsgi_app(...)
+                    try:
+                        for chunk in response:
+                            if chunk:
+                                req.write(chunk)
+                    finally:
+                        if hasattr(response, "close"):
+                            response.close()
+                if req.close_connection:
+                    return
+"""
+
+
+import base64
+import os
+import Queue
+import re
+quoted_slash = re.compile("(?i)%2F")
+import rfc822
+import socket
+try:
+    import cStringIO as StringIO
+except ImportError:
+    import StringIO
+
+_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
+
+import sys
+import threading
+import time
+import traceback
+from urllib import unquote
+from urlparse import urlparse
+import warnings
+
+try:
+    from OpenSSL import SSL
+    from OpenSSL import crypto
+except ImportError:
+    SSL = None
+
+import errno
+
+def plat_specific_errors(*errnames):
+    """Return error numbers for all errors in errnames on this platform.
+    
+    The 'errno' module contains different global constants depending on
+    the specific platform (OS). This function will return the list of
+    numeric values for a given list of potential names.
+    """
+    errno_names = dir(errno)
+    nums = [getattr(errno, k) for k in errnames if k in errno_names]
+    # de-dupe the list
+    return dict.fromkeys(nums).keys()
+
+socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
+
+socket_errors_to_ignore = plat_specific_errors(
+    "EPIPE",
+    "EBADF", "WSAEBADF",
+    "ENOTSOCK", "WSAENOTSOCK",
+    "ETIMEDOUT", "WSAETIMEDOUT",
+    "ECONNREFUSED", "WSAECONNREFUSED",
+    "ECONNRESET", "WSAECONNRESET",
+    "ECONNABORTED", "WSAECONNABORTED",
+    "ENETRESET", "WSAENETRESET",
+    "EHOSTDOWN", "EHOSTUNREACH",
+    )
+socket_errors_to_ignore.append("timed out")
+
+socket_errors_nonblocking = plat_specific_errors(
+    'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
+
+comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
+    'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
+    'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
+    'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
+    'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
+    'WWW-AUTHENTICATE']
+
+
+class WSGIPathInfoDispatcher(object):
+    """A WSGI dispatcher for dispatch based on the PATH_INFO.
+    
+    apps: a dict or list of (path_prefix, app) pairs.
+    """
+    
+    def __init__(self, apps):
+        try:
+            apps = apps.items()
+        except AttributeError:
+            pass
+        
+        # Sort the apps by len(path), descending
+        apps.sort()
+        apps.reverse()
+        
+        # The path_prefix strings must start, but not end, with a slash.
+        # Use "" instead of "/".
+        self.apps = [(p.rstrip("/"), a) for p, a in apps]
+    
+    def __call__(self, environ, start_response):
+        path = environ["PATH_INFO"] or "/"
+        for p, app in self.apps:
+            # The apps list should be sorted by length, descending.
+            if path.startswith(p + "/") or path == p:
+                environ = environ.copy()
+                environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
+                environ["PATH_INFO"] = path[len(p):]
+                return app(environ, start_response)
+        
+        start_response('404 Not Found', [('Content-Type', 'text/plain'),
+                                         ('Content-Length', '0')])
+        return ['']
+
+
+class MaxSizeExceeded(Exception):
+    pass
+
+class SizeCheckWrapper(object):
+    """Wraps a file-like object, raising MaxSizeExceeded if too large."""
+    
+    def __init__(self, rfile, maxlen):
+        self.rfile = rfile
+        self.maxlen = maxlen
+        self.bytes_read = 0
+    
+    def _check_length(self):
+        if self.maxlen and self.bytes_read > self.maxlen:
+            raise MaxSizeExceeded()
+    
+    def read(self, size=None):
+        data = self.rfile.read(size)
+        self.bytes_read += len(data)
+        self._check_length()
+        return data
+    
+    def readline(self, size=None):
+        if size is not None:
+            data = self.rfile.readline(size)
+            self.bytes_read += len(data)
+            self._check_length()
+            return data
+        
+        # User didn't specify a size ...
+        # We read the line in chunks to make sure it's not a 100MB line !
+        res = []
+        while True:
+            data = self.rfile.readline(256)
+            self.bytes_read += len(data)
+            self._check_length()
+            res.append(data)
+            # See http://www.cherrypy.org/ticket/421
+            if len(data) < 256 or data[-1:] == "\n":
+                return ''.join(res)
+    
+    def readlines(self, sizehint=0):
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline()
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline()
+        return lines
+    
+    def close(self):
+        self.rfile.close()
+    
+    def __iter__(self):
+        return self
+    
+    def next(self):
+        data = self.rfile.next()
+        self.bytes_read += len(data)
+        self._check_length()
+        return data
+
+
+class HTTPRequest(object):
+    """An HTTP Request (and response).
+    
+    A single HTTP connection may consist of multiple request/response pairs.
+    
+    send: the 'send' method from the connection's socket object.
+    wsgi_app: the WSGI application to call.
+    environ: a partial WSGI environ (server and connection entries).
+        The caller MUST set the following entries:
+        * All wsgi.* entries, including .input
+        * SERVER_NAME and SERVER_PORT
+        * Any SSL_* entries
+        * Any custom entries like REMOTE_ADDR and REMOTE_PORT
+        * SERVER_SOFTWARE: the value to write in the "Server" response header.
+        * ACTUAL_SERVER_PROTOCOL: the value to write in the Status-Line of
+            the response. From RFC 2145: "An HTTP server SHOULD send a
+            response version equal to the highest version for which the
+            server is at least conditionally compliant, and whose major
+            version is less than or equal to the one received in the
+            request.  An HTTP server MUST NOT send a version for which
+            it is not at least conditionally compliant."
+    
+    outheaders: a list of header tuples to write in the response.
+    ready: when True, the request has been parsed and is ready to begin
+        generating the response. When False, signals the calling Connection
+        that the response should not be generated and the connection should
+        close.
+    close_connection: signals the calling Connection that the request
+        should close. This does not imply an error! The client and/or
+        server may each request that the connection be closed.
+    chunked_write: if True, output will be encoded with the "chunked"
+        transfer-coding. This value is set automatically inside
+        send_headers.
+    """
+    
+    max_request_header_size = 0
+    max_request_body_size = 0
+    
+    def __init__(self, wfile, environ, wsgi_app):
+        self.rfile = environ['wsgi.input']
+        self.wfile = wfile
+        self.environ = environ.copy()
+        self.wsgi_app = wsgi_app
+        
+        self.ready = False
+        self.started_response = False
+        self.status = ""
+        self.outheaders = []
+        self.sent_headers = False
+        self.close_connection = False
+        self.chunked_write = False
+    
+    def parse_request(self):
+        """Parse the next HTTP request start-line and message-headers."""
+        self.rfile.maxlen = self.max_request_header_size
+        self.rfile.bytes_read = 0
+        
+        try:
+            self._parse_request()
+        except MaxSizeExceeded:
+            self.simple_response("413 Request Entity Too Large")
+            return
+    
+    def _parse_request(self):
+        # HTTP/1.1 connections are persistent by default. If a client
+        # requests a page, then idles (leaves the connection open),
+        # then rfile.readline() will raise socket.error("timed out").
+        # Note that it does this based on the value given to settimeout(),
+        # and doesn't need the client to request or acknowledge the close
+        # (although your TCP stack might suffer for it: cf Apache's history
+        # with FIN_WAIT_2).
+        request_line = self.rfile.readline()
+        if not request_line:
+            # Force self.ready = False so the connection will close.
+            self.ready = False
+            return
+        
+        if request_line == "\r\n":
+            # RFC 2616 sec 4.1: "...if the server is reading the protocol
+            # stream at the beginning of a message and receives a CRLF
+            # first, it should ignore the CRLF."
+            # But only ignore one leading line! else we enable a DoS.
+            request_line = self.rfile.readline()
+            if not request_line:
+                self.ready = False
+                return
+        
+        environ = self.environ
+        
+        try:
+            method, path, req_protocol = request_line.strip().split(" ", 2)
+        except ValueError:
+            self.simple_response(400, "Malformed Request-Line")
+            return
+        
+        environ["REQUEST_METHOD"] = method
+        
+        # path may be an abs_path (including "http://host.domain.tld");
+        scheme, location, path, params, qs, frag = urlparse(path)
+        
+        if frag:
+            self.simple_response("400 Bad Request",
+                                 "Illegal #fragment in Request-URI.")
+            return
+        
+        if scheme:
+            environ["wsgi.url_scheme"] = scheme
+        if params:
+            path = path + ";" + params
+        
+        environ["SCRIPT_NAME"] = ""
+        
+        # Unquote the path+params (e.g. "/this%20path" -> "this path").
+        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
+        #
+        # But note that "...a URI must be separated into its components
+        # before the escaped characters within those components can be
+        # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
+        atoms = [unquote(x) for x in quoted_slash.split(path)]
+        path = "%2F".join(atoms)
+        environ["PATH_INFO"] = path
+        
+        # Note that, like wsgiref and most other WSGI servers,
+        # we unquote the path but not the query string.
+        environ["QUERY_STRING"] = qs
+        
+        # Compare request and server HTTP protocol versions, in case our
+        # server does not support the requested protocol. Limit our output
+        # to min(req, server). We want the following output:
+        #     request    server     actual written   supported response
+        #     protocol   protocol  response protocol    feature set
+        # a     1.0        1.0           1.0                1.0
+        # b     1.0        1.1           1.1                1.0
+        # c     1.1        1.0           1.0                1.0
+        # d     1.1        1.1           1.1                1.1
+        # Notice that, in (b), the response will be "HTTP/1.1" even though
+        # the client only understands 1.0. RFC 2616 10.5.6 says we should
+        # only return 505 if the _major_ version is different.
+        rp = int(req_protocol[5]), int(req_protocol[7])
+        server_protocol = environ["ACTUAL_SERVER_PROTOCOL"]
+        sp = int(server_protocol[5]), int(server_protocol[7])
+        if sp[0] != rp[0]:
+            self.simple_response("505 HTTP Version Not Supported")
+            return
+        # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
+        environ["SERVER_PROTOCOL"] = req_protocol
+        self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
+        
+        # If the Request-URI was an absoluteURI, use its location atom.
+        if location:
+            environ["SERVER_NAME"] = location
+        
+        # then all the http headers
+        try:
+            self.read_headers()
+        except ValueError, ex:
+            self.simple_response("400 Bad Request", repr(ex.args))
+            return
+        
+        mrbs = self.max_request_body_size
+        if mrbs and int(environ.get("CONTENT_LENGTH", 0)) > mrbs:
+            self.simple_response("413 Request Entity Too Large")
+            return
+        
+        # Persistent connection support
+        if self.response_protocol == "HTTP/1.1":
+            # Both server and client are HTTP/1.1
+            if environ.get("HTTP_CONNECTION", "") == "close":
+                self.close_connection = True
+        else:
+            # Either the server or client (or both) are HTTP/1.0
+            if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
+                self.close_connection = True
+        
+        # Transfer-Encoding support
+        te = None
+        if self.response_protocol == "HTTP/1.1":
+            te = environ.get("HTTP_TRANSFER_ENCODING")
+            if te:
+                te = [x.strip().lower() for x in te.split(",") if x.strip()]
+        
+        self.chunked_read = False
+        
+        if te:
+            for enc in te:
+                if enc == "chunked":
+                    self.chunked_read = True
+                else:
+                    # Note that, even if we see "chunked", we must reject
+                    # if there is an extension we don't recognize.
+                    self.simple_response("501 Unimplemented")
+                    self.close_connection = True
+                    return
+        
+        # From PEP 333:
+        # "Servers and gateways that implement HTTP 1.1 must provide
+        # transparent support for HTTP 1.1's "expect/continue" mechanism.
+        # This may be done in any of several ways:
+        #   1. Respond to requests containing an Expect: 100-continue request
+        #      with an immediate "100 Continue" response, and proceed normally.
+        #   2. Proceed with the request normally, but provide the application
+        #      with a wsgi.input stream that will send the "100 Continue"
+        #      response if/when the application first attempts to read from
+        #      the input stream. The read request must then remain blocked
+        #      until the client responds.
+        #   3. Wait until the client decides that the server does not support
+        #      expect/continue, and sends the request body on its own.
+        #      (This is suboptimal, and is not recommended.)
+        #
+        # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
+        # but it seems like it would be a big slowdown for such a rare case.
+        if environ.get("HTTP_EXPECT", "") == "100-continue":
+            self.simple_response(100)
+        
+        self.ready = True
+    
+    def read_headers(self):
+        """Read header lines from the incoming stream."""
+        environ = self.environ
+        
+        while True:
+            line = self.rfile.readline()
+            if not line:
+                # No more data--illegal end of headers
+                raise ValueError("Illegal end of headers.")
+            
+            if line == '\r\n':
+                # Normal end of headers
+                break
+            
+            if line[0] in ' \t':
+                # It's a continuation line.
+                v = line.strip()
+            else:
+                k, v = line.split(":", 1)
+                k, v = k.strip().upper(), v.strip()
+                envname = "HTTP_" + k.replace("-", "_")
+            
+            if k in comma_separated_headers:
+                existing = environ.get(envname)
+                if existing:
+                    v = ", ".join((existing, v))
+            environ[envname] = v
+        
+        ct = environ.pop("HTTP_CONTENT_TYPE", None)
+        if ct is not None:
+            environ["CONTENT_TYPE"] = ct
+        cl = environ.pop("HTTP_CONTENT_LENGTH", None)
+        if cl is not None:
+            environ["CONTENT_LENGTH"] = cl
+    
+    def decode_chunked(self):
+        """Decode the 'chunked' transfer coding."""
+        cl = 0
+        data = StringIO.StringIO()
+        while True:
+            line = self.rfile.readline().strip().split(";", 1)
+            chunk_size = int(line.pop(0), 16)
+            if chunk_size <= 0:
+                break
+##            if line: chunk_extension = line[0]
+            cl += chunk_size
+            data.write(self.rfile.read(chunk_size))
+            crlf = self.rfile.read(2)
+            if crlf != "\r\n":
+                self.simple_response("400 Bad Request",
+                                     "Bad chunked transfer coding "
+                                     "(expected '\\r\\n', got %r)" % crlf)
+                return
+        
+        # Grab any trailer headers
+        self.read_headers()
+        
+        data.seek(0)
+        self.environ["wsgi.input"] = data
+        self.environ["CONTENT_LENGTH"] = str(cl) or ""
+        return True
+    
+    def respond(self):
+        """Call the appropriate WSGI app and write its iterable output."""
+        # Set rfile.maxlen to ensure we don't read past Content-Length.
+        # This will also be used to read the entire request body if errors
+        # are raised before the app can read the body.
+        if self.chunked_read:
+            # If chunked, Content-Length will be 0.
+            self.rfile.maxlen = self.max_request_body_size
+        else:
+            cl = int(self.environ.get("CONTENT_LENGTH", 0))
+            if self.max_request_body_size:
+                self.rfile.maxlen = min(cl, self.max_request_body_size)
+            else:
+                self.rfile.maxlen = cl
+        self.rfile.bytes_read = 0
+        
+        try:
+            self._respond()
+        except MaxSizeExceeded:
+            if not self.sent_headers:
+                self.simple_response("413 Request Entity Too Large")
+            return
+    
+    def _respond(self):
+        if self.chunked_read:
+            if not self.decode_chunked():
+                self.close_connection = True
+                return
+        
+        response = self.wsgi_app(self.environ, self.start_response)
+        try:
+            for chunk in response:
+                # "The start_response callable must not actually transmit
+                # the response headers. Instead, it must store them for the
+                # server or gateway to transmit only after the first
+                # iteration of the application return value that yields
+                # a NON-EMPTY string, or upon the application's first
+                # invocation of the write() callable." (PEP 333)
+                if chunk:
+                    self.write(chunk)
+        finally:
+            if hasattr(response, "close"):
+                response.close()
+        
+        if (self.ready and not self.sent_headers):
+            self.sent_headers = True
+            self.send_headers()
+        if self.chunked_write:
+            self.wfile.sendall("0\r\n\r\n")
+    
+    def simple_response(self, status, msg=""):
+        """Write a simple response back to the client."""
+        status = str(status)
+        buf = ["%s %s\r\n" % (self.environ['ACTUAL_SERVER_PROTOCOL'], status),
+               "Content-Length: %s\r\n" % len(msg),
+               "Content-Type: text/plain\r\n"]
+        
+        if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
+            # Request Entity Too Large
+            self.close_connection = True
+            buf.append("Connection: close\r\n")
+        
+        buf.append("\r\n")
+        if msg:
+            buf.append(msg)
+        
+        try:
+            self.wfile.sendall("".join(buf))
+        except socket.error, x:
+            if x.args[0] not in socket_errors_to_ignore:
+                raise
+    
+    def start_response(self, status, headers, exc_info = None):
+        """WSGI callable to begin the HTTP response."""
+        # "The application may call start_response more than once,
+        # if and only if the exc_info argument is provided."
+        if self.started_response and not exc_info:
+            raise AssertionError("WSGI start_response called a second "
+                                 "time with no exc_info.")
+        
+        # "if exc_info is provided, and the HTTP headers have already been
+        # sent, start_response must raise an error, and should raise the
+        # exc_info tuple."
+        if self.sent_headers:
+            try:
+                raise exc_info[0], exc_info[1], exc_info[2]
+            finally:
+                exc_info = None
+        
+        self.started_response = True
+        self.status = status
+        self.outheaders.extend(headers)
+        return self.write
+    
+    def write(self, chunk):
+        """WSGI callable to write unbuffered data to the client.
+        
+        This method is also used internally by start_response (to write
+        data from the iterable returned by the WSGI application).
+        """
+        if not self.started_response:
+            raise AssertionError("WSGI write called before start_response.")
+        
+        if not self.sent_headers:
+            self.sent_headers = True
+            self.send_headers()
+        
+        if self.chunked_write and chunk:
+            buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
+            self.wfile.sendall("".join(buf))
+        else:
+            self.wfile.sendall(chunk)
+    
+    def send_headers(self):
+        """Assert, process, and send the HTTP response message-headers."""
+        hkeys = [key.lower() for key, value in self.outheaders]
+        status = int(self.status[:3])
+        
+        if status == 413:
+            # Request Entity Too Large. Close conn to avoid garbage.
+            self.close_connection = True
+        elif "content-length" not in hkeys:
+            # "All 1xx (informational), 204 (no content),
+            # and 304 (not modified) responses MUST NOT
+            # include a message-body." So no point chunking.
+            if status < 200 or status in (204, 205, 304):
+                pass
+            else:
+                if (self.response_protocol == 'HTTP/1.1'
+                    and self.environ["REQUEST_METHOD"] != 'HEAD'):
+                    # Use the chunked transfer-coding
+                    self.chunked_write = True
+                    self.outheaders.append(("Transfer-Encoding", "chunked"))
+                else:
+                    # Closing the conn is the only way to determine len.
+                    self.close_connection = True
+        
+        if "connection" not in hkeys:
+            if self.response_protocol == 'HTTP/1.1':
+                # Both server and client are HTTP/1.1 or better
+                if self.close_connection:
+                    self.outheaders.append(("Connection", "close"))
+            else:
+                # Server and/or client are HTTP/1.0
+                if not self.close_connection:
+                    self.outheaders.append(("Connection", "Keep-Alive"))
+        
+        if (not self.close_connection) and (not self.chunked_read):
+            # Read any remaining request body data on the socket.
+            # "If an origin server receives a request that does not include an
+            # Expect request-header field with the "100-continue" expectation,
+            # the request includes a request body, and the server responds
+            # with a final status code before reading the entire request body
+            # from the transport connection, then the server SHOULD NOT close
+            # the transport connection until it has read the entire request,
+            # or until the client closes the connection. Otherwise, the client
+            # might not reliably receive the response message. However, this
+            # requirement is not be construed as preventing a server from
+            # defending itself against denial-of-service attacks, or from
+            # badly broken client implementations."
+            size = self.rfile.maxlen - self.rfile.bytes_read
+            if size > 0:
+                self.rfile.read(size)
+        
+        if "date" not in hkeys:
+            self.outheaders.append(("Date", rfc822.formatdate()))
+        
+        if "server" not in hkeys:
+            self.outheaders.append(("Server", self.environ['SERVER_SOFTWARE']))
+        
+        buf = [self.environ['ACTUAL_SERVER_PROTOCOL'], " ", self.status, "\r\n"]
+        try:
+            buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
+        except TypeError:
+            if not isinstance(k, str):
+                raise TypeError("WSGI response header key %r is not a string.")
+            if not isinstance(v, str):
+                raise TypeError("WSGI response header value %r is not a string.")
+            else:
+                raise
+        buf.append("\r\n")
+        self.wfile.sendall("".join(buf))
+
+
+class NoSSLError(Exception):
+    """Exception raised when a client speaks HTTP to an HTTPS socket."""
+    pass
+
+
+class FatalSSLAlert(Exception):
+    """Exception raised when the SSL implementation signals a fatal alert."""
+    pass
+
+
+if not _fileobject_uses_str_type:
+    class CP_fileobject(socket._fileobject):
+        """Faux file object attached to a socket object."""
+
+        def sendall(self, data):
+            """Sendall for non-blocking sockets."""
+            while data:
+                try:
+                    bytes_sent = self.send(data)
+                    data = data[bytes_sent:]
+                except socket.error, e:
+                    if e.args[0] not in socket_errors_nonblocking:
+                        raise
+
+        def send(self, data):
+            return self._sock.send(data)
+
+        def flush(self):
+            if self._wbuf:
+                buffer = "".join(self._wbuf)
+                self._wbuf = []
+                self.sendall(buffer)
+
+        def recv(self, size):
+            while True:
+                try:
+                    return self._sock.recv(size)
+                except socket.error, e:
+                    if (e.args[0] not in socket_errors_nonblocking
+                        and e.args[0] not in socket_error_eintr):
+                        raise
+
+        def read(self, size=-1):
+            # Use max, disallow tiny reads in a loop as they are very inefficient.
+            # We never leave read() with any leftover data from a new recv() call
+            # in our internal buffer.
+            rbufsize = max(self._rbufsize, self.default_bufsize)
+            # Our use of StringIO rather than lists of string objects returned by
+            # recv() minimizes memory usage and fragmentation that occurs when
+            # rbufsize is large compared to the typical return value of recv().
+            buf = self._rbuf
+            buf.seek(0, 2)  # seek end
+            if size < 0:
+                # Read until EOF
+                self._rbuf = StringIO.StringIO()  # reset _rbuf.  we consume it via buf.
+                while True:
+                    data = self.recv(rbufsize)
+                    if not data:
+                        break
+                    buf.write(data)
+                return buf.getvalue()
+            else:
+                # Read until size bytes or EOF seen, whichever comes first
+                buf_len = buf.tell()
+                if buf_len >= size:
+                    # Already have size bytes in our buffer?  Extract and return.
+                    buf.seek(0)
+                    rv = buf.read(size)
+                    self._rbuf = StringIO.StringIO()
+                    self._rbuf.write(buf.read())
+                    return rv
+
+                self._rbuf = StringIO.StringIO()  # reset _rbuf.  we consume it via buf.
+                while True:
+                    left = size - buf_len
+                    # recv() will malloc the amount of memory given as its
+                    # parameter even though it often returns much less data
+                    # than that.  The returned data string is short lived
+                    # as we copy it into a StringIO and free it.  This avoids
+                    # fragmentation issues on many platforms.
+                    data = self.recv(left)
+                    if not data:
+                        break
+                    n = len(data)
+                    if n == size and not buf_len:
+                        # Shortcut.  Avoid buffer data copies when:
+                        # - We have no data in our buffer.
+                        # AND
+                        # - Our call to recv returned exactly the
+                        #   number of bytes we were asked to read.
+                        return data
+                    if n == left:
+                        buf.write(data)
+                        del data  # explicit free
+                        break
+                    assert n <= left, "recv(%d) returned %d bytes" % (left, n)
+                    buf.write(data)
+                    buf_len += n
+                    del data  # explicit free
+                    #assert buf_len == buf.tell()
+                return buf.getvalue()
+
+        def readline(self, size=-1):
+            buf = self._rbuf
+            buf.seek(0, 2)  # seek end
+            if buf.tell() > 0:
+                # check if we already have it in our buffer
+                buf.seek(0)
+                bline = buf.readline(size)
+                if bline.endswith('\n') or len(bline) == size:
+                    self._rbuf = StringIO.StringIO()
+                    self._rbuf.write(buf.read())
+                    return bline
+                del bline
+            if size < 0:
+                # Read until \n or EOF, whichever comes first
+                if self._rbufsize <= 1:
+                    # Speed up unbuffered case
+                    buf.seek(0)
+                    buffers = [buf.read()]
+                    self._rbuf = StringIO.StringIO()  # reset _rbuf.  we consume it via buf.
+                    data = None
+                    recv = self.recv
+                    while data != "\n":
+                        data = recv(1)
+                        if not data:
+                            break
+                        buffers.append(data)
+                    return "".join(buffers)
+
+                buf.seek(0, 2)  # seek end
+                self._rbuf = StringIO.StringIO()  # reset _rbuf.  we consume it via buf.
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    nl = data.find('\n')
+                    if nl >= 0:
+                        nl += 1
+                        buf.write(data[:nl])
+                        self._rbuf.write(data[nl:])
+                        del data
+                        break
+                    buf.write(data)
+                return buf.getvalue()
+            else:
+                # Read until size bytes or \n or EOF seen, whichever comes first
+                buf.seek(0, 2)  # seek end
+                buf_len = buf.tell()
+                if buf_len >= size:
+                    buf.seek(0)
+                    rv = buf.read(size)
+                    self._rbuf = StringIO.StringIO()
+                    self._rbuf.write(buf.read())
+                    return rv
+                self._rbuf = StringIO.StringIO()  # reset _rbuf.  we consume it via buf.
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    left = size - buf_len
+                    # did we just receive a newline?
+                    nl = data.find('\n', 0, left)
+                    if nl >= 0:
+                        nl += 1
+                        # save the excess data to _rbuf
+                        self._rbuf.write(data[nl:])
+                        if buf_len:
+                            buf.write(data[:nl])
+                            break
+                        else:
+                            # Shortcut.  Avoid data copy through buf when returning
+                            # a substring of our first recv().
+                            return data[:nl]
+                    n = len(data)
+                    if n == size and not buf_len:
+                        # Shortcut.  Avoid data copy through buf when
+                        # returning exactly all of our first recv().
+                        return data
+                    if n >= left:
+                        buf.write(data[:left])
+                        self._rbuf.write(data[left:])
+                        break
+                    buf.write(data)
+                    buf_len += n
+                    #assert buf_len == buf.tell()
+                return buf.getvalue()
+
+else:
+    class CP_fileobject(socket._fileobject):
+        """Faux file object attached to a socket object."""
+
+        def sendall(self, data):
+            """Sendall for non-blocking sockets."""
+            while data:
+                try:
+                    bytes_sent = self.send(data)
+                    data = data[bytes_sent:]
+                except socket.error, e:
+                    if e.args[0] not in socket_errors_nonblocking:
+                        raise
+
+        def send(self, data):
+            return self._sock.send(data)
+
+        def flush(self):
+            if self._wbuf:
+                buffer = "".join(self._wbuf)
+                self._wbuf = []
+                self.sendall(buffer)
+
+        def recv(self, size):
+            while True:
+                try:
+                    return self._sock.recv(size)
+                except socket.error, e:
+                    if (e.args[0] not in socket_errors_nonblocking
+                        and e.args[0] not in socket_error_eintr):
+                        raise
+
+        def read(self, size=-1):
+            if size < 0:
+                # Read until EOF
+                buffers = [self._rbuf]
+                self._rbuf = ""
+                if self._rbufsize <= 1:
+                    recv_size = self.default_bufsize
+                else:
+                    recv_size = self._rbufsize
+
+                while True:
+                    data = self.recv(recv_size)
+                    if not data:
+                        break
+                    buffers.append(data)
+                return "".join(buffers)
+            else:
+                # Read until size bytes or EOF seen, whichever comes first
+                data = self._rbuf
+                buf_len = len(data)
+                if buf_len >= size:
+                    self._rbuf = data[size:]
+                    return data[:size]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ""
+                while True:
+                    left = size - buf_len
+                    recv_size = max(self._rbufsize, left)
+                    data = self.recv(recv_size)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    n = len(data)
+                    if n >= left:
+                        self._rbuf = data[left:]
+                        buffers[-1] = data[:left]
+                        break
+                    buf_len += n
+                return "".join(buffers)
+
+        def readline(self, size=-1):
+            data = self._rbuf
+            if size < 0:
+                # Read until \n or EOF, whichever comes first
+                if self._rbufsize <= 1:
+                    # Speed up unbuffered case
+                    assert data == ""
+                    buffers = []
+                    while data != "\n":
+                        data = self.recv(1)
+                        if not data:
+                            break
+                        buffers.append(data)
+                    return "".join(buffers)
+                nl = data.find('\n')
+                if nl >= 0:
+                    nl += 1
+                    self._rbuf = data[nl:]
+                    return data[:nl]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ""
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    nl = data.find('\n')
+                    if nl >= 0:
+                        nl += 1
+                        self._rbuf = data[nl:]
+                        buffers[-1] = data[:nl]
+                        break
+                return "".join(buffers)
+            else:
+                # Read until size bytes or \n or EOF seen, whichever comes first
+                nl = data.find('\n', 0, size)
+                if nl >= 0:
+                    nl += 1
+                    self._rbuf = data[nl:]
+                    return data[:nl]
+                buf_len = len(data)
+                if buf_len >= size:
+                    self._rbuf = data[size:]
+                    return data[:size]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ""
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    left = size - buf_len
+                    nl = data.find('\n', 0, left)
+                    if nl >= 0:
+                        nl += 1
+                        self._rbuf = data[nl:]
+                        buffers[-1] = data[:nl]
+                        break
+                    n = len(data)
+                    if n >= left:
+                        self._rbuf = data[left:]
+                        buffers[-1] = data[:left]
+                        break
+                    buf_len += n
+                return "".join(buffers)
+    
+
+class SSL_fileobject(CP_fileobject):
+    """SSL file object attached to a socket object."""
+    
+    ssl_timeout = 3
+    ssl_retry = .01
+    
+    def _safe_call(self, is_reader, call, *args, **kwargs):
+        """Wrap the given call with SSL error-trapping.
+        
+        is_reader: if False EOF errors will be raised. If True, EOF errors
+            will return "" (to emulate normal sockets).
+        """
+        start = time.time()
+        while True:
+            try:
+                return call(*args, **kwargs)
+            except SSL.WantReadError:
+                # Sleep and try again. This is dangerous, because it means
+                # the rest of the stack has no way of differentiating
+                # between a "new handshake" error and "client dropped".
+                # Note this isn't an endless loop: there's a timeout below.
+                time.sleep(self.ssl_retry)
+            except SSL.WantWriteError:
+                time.sleep(self.ssl_retry)
+            except SSL.SysCallError, e:
+                if is_reader and e.args == (-1, 'Unexpected EOF'):
+                    return ""
+                
+                errnum = e.args[0]
+                if is_reader and errnum in socket_errors_to_ignore:
+                    return ""
+                raise socket.error(errnum)
+            except SSL.Error, e:
+                if is_reader and e.args == (-1, 'Unexpected EOF'):
+                    return ""
+                
+                thirdarg = None
+                try:
+                    thirdarg = e.args[0][0][2]
+                except IndexError:
+                    pass
+                
+                if thirdarg == 'http request':
+                    # The client is talking HTTP to an HTTPS server.
+                    raise NoSSLError()
+                raise FatalSSLAlert(*e.args)
+            except:
+                raise
+            
+            if time.time() - start > self.ssl_timeout:
+                raise socket.timeout("timed out")
+
+    def recv(self, *args, **kwargs):
+        buf = []
+        r = super(SSL_fileobject, self).recv
+        while True:
+            data = self._safe_call(True, r, *args, **kwargs)
+            buf.append(data)
+            p = self._sock.pending()
+            if not p:
+                return "".join(buf)
+    
+    def sendall(self, *args, **kwargs):
+        return self._safe_call(False, super(SSL_fileobject, self).sendall, *args, **kwargs)
+
+    def send(self, *args, **kwargs):
+        return self._safe_call(False, super(SSL_fileobject, self).send, *args, **kwargs)
+
+
+class HTTPConnection(object):
+    """An HTTP connection (active socket).
+    
+    socket: the raw socket object (usually TCP) for this connection.
+    wsgi_app: the WSGI application for this server/connection.
+    environ: a WSGI environ template. This will be copied for each request.
+    
+    rfile: a fileobject for reading from the socket.
+    send: a function for writing (+ flush) to the socket.
+    """
+    
+    rbufsize = -1
+    RequestHandlerClass = HTTPRequest
+    environ = {"wsgi.version": (1, 0),
+               "wsgi.url_scheme": "http",
+               "wsgi.multithread": True,
+               "wsgi.multiprocess": False,
+               "wsgi.run_once": False,
+               "wsgi.errors": sys.stderr,
+               }
+    
+    def __init__(self, sock, wsgi_app, environ):
+        self.socket = sock
+        self.wsgi_app = wsgi_app
+        
+        # Copy the class environ into self.
+        self.environ = self.environ.copy()
+        self.environ.update(environ)
+        
+        if SSL and isinstance(sock, SSL.ConnectionType):
+            timeout = sock.gettimeout()
+            self.rfile = SSL_fileobject(sock, "rb", self.rbufsize)
+            self.rfile.ssl_timeout = timeout
+            self.wfile = SSL_fileobject(sock, "wb", -1)
+            self.wfile.ssl_timeout = timeout
+        else:
+            self.rfile = CP_fileobject(sock, "rb", self.rbufsize)
+            self.wfile = CP_fileobject(sock, "wb", -1)
+        
+        # Wrap wsgi.input but not HTTPConnection.rfile itself.
+        # We're also not setting maxlen yet; we'll do that separately
+        # for headers and body for each iteration of self.communicate
+        # (if maxlen is 0 the wrapper doesn't check length).
+        self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0)
+    
+    def communicate(self):
+        """Read each request and respond appropriately."""
+        try:
+            while True:
+                # (re)set req to None so that if something goes wrong in
+                # the RequestHandlerClass constructor, the error doesn't
+                # get written to the previous request.
+                req = None
+                req = self.RequestHandlerClass(self.wfile, self.environ,
+                                               self.wsgi_app)
+                
+                # This order of operations should guarantee correct pipelining.
+                req.parse_request()
+                if not req.ready:
+                    return
+                
+                req.respond()
+                if req.close_connection:
+                    return
+        
+        except socket.error, e:
+            errnum = e.args[0]
+            if errnum == 'timed out':
+                if req and not req.sent_headers:
+                    req.simple_response("408 Request Timeout")
+            elif errnum not in socket_errors_to_ignore:
+                if req and not req.sent_headers:
+                    req.simple_response("500 Internal Server Error",
+                                        format_exc())
+            return
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except FatalSSLAlert, e:
+            # Close the connection.
+            return
+        except NoSSLError:
+            if req and not req.sent_headers:
+                # Unwrap our wfile
+                req.wfile = CP_fileobject(self.socket._sock, "wb", -1)
+                req.simple_response("400 Bad Request",
+                    "The client sent a plain HTTP request, but "
+                    "this server only speaks HTTPS on this port.")
+                self.linger = True
+        except Exception, e:
+            if req and not req.sent_headers:
+                req.simple_response("500 Internal Server Error", format_exc())
+    
+    linger = False
+    
+    def close(self):
+        """Close the socket underlying this connection."""
+        self.rfile.close()
+        
+        if not self.linger:
+            # Python's socket module does NOT call close on the kernel socket
+            # when you call socket.close(). We do so manually here because we
+            # want this server to send a FIN TCP segment immediately. Note this
+            # must be called *before* calling socket.close(), because the latter
+            # drops its reference to the kernel socket.
+            self.socket._sock.close()
+            self.socket.close()
+        else:
+            # On the other hand, sometimes we want to hang around for a bit
+            # to make sure the client has a chance to read our entire
+            # response. Skipping the close() calls here delays the FIN
+            # packet until the socket object is garbage-collected later.
+            # Someday, perhaps, we'll do the full lingering_close that
+            # Apache does, but not today.
+            pass
+
+
+def format_exc(limit=None):
+    """Like print_exc() but return a string. Backport for Python 2.3."""
+    try:
+        etype, value, tb = sys.exc_info()
+        return ''.join(traceback.format_exception(etype, value, tb, limit))
+    finally:
+        etype = value = tb = None
+
+
+_SHUTDOWNREQUEST = None
+
+class WorkerThread(threading.Thread):
+    """Thread which continuously polls a Queue for Connection objects.
+    
+    server: the HTTP Server which spawned this thread, and which owns the
+        Queue and is placing active connections into it.
+    ready: a simple flag for the calling server to know when this thread
+        has begun polling the Queue.
+    
+    Due to the timing issues of polling a Queue, a WorkerThread does not
+    check its own 'ready' flag after it has started. To stop the thread,
+    it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
+    (one for each running WorkerThread).
+    """
+    
+    conn = None
+    
+    def __init__(self, server):
+        self.ready = False
+        self.server = server
+        threading.Thread.__init__(self)
+    
+    def run(self):
+        try:
+            self.ready = True
+            while True:
+                conn = self.server.requests.get()
+                if conn is _SHUTDOWNREQUEST:
+                    return
+                
+                self.conn = conn
+                try:
+                    conn.communicate()
+                finally:
+                    conn.close()
+                    self.conn = None
+        except (KeyboardInterrupt, SystemExit), exc:
+            self.server.interrupt = exc
+
+
+class ThreadPool(object):
+    """A Request Queue for the CherryPyWSGIServer which pools threads.
+    
+    ThreadPool objects must provide min, get(), put(obj), start()
+    and stop(timeout) attributes.
+    """
+    
+    def __init__(self, server, min=10, max=-1):
+        self.server = server
+        self.min = min
+        self.max = max
+        self._threads = []
+        self._queue = Queue.Queue()
+        self.get = self._queue.get
+    
+    def start(self):
+        """Start the pool of threads."""
+        for i in xrange(self.min):
+            self._threads.append(WorkerThread(self.server))
+        for worker in self._threads:
+            worker.setName("CP WSGIServer " + worker.getName())
+            worker.start()
+        for worker in self._threads:
+            while not worker.ready:
+                time.sleep(.1)
+    
+    def _get_idle(self):
+        """Number of worker threads which are idle. Read-only."""
+        return len([t for t in self._threads if t.conn is None])
+    idle = property(_get_idle, doc=_get_idle.__doc__)
+    
+    def put(self, obj):
+        self._queue.put(obj)
+        if obj is _SHUTDOWNREQUEST:
+            return
+    
+    def grow(self, amount):
+        """Spawn new worker threads (not above self.max)."""
+        for i in xrange(amount):
+            if self.max > 0 and len(self._threads) >= self.max:
+                break
+            worker = WorkerThread(self.server)
+            worker.setName("CP WSGIServer " + worker.getName())
+            self._threads.append(worker)
+            worker.start()
+    
+    def shrink(self, amount):
+        """Kill off worker threads (not below self.min)."""
+        # Grow/shrink the pool if necessary.
+        # Remove any dead threads from our list
+        for t in self._threads:
+            if not t.isAlive():
+                self._threads.remove(t)
+                amount -= 1
+        
+        if amount > 0:
+            for i in xrange(min(amount, len(self._threads) - self.min)):
+                # Put a number of shutdown requests on the queue equal
+                # to 'amount'. Once each of those is processed by a worker,
+                # that worker will terminate and be culled from our list
+                # in self.put.
+                self._queue.put(_SHUTDOWNREQUEST)
+    
+    def stop(self, timeout=5):
+        # Must shut down threads here so the code that calls
+        # this method can know when all threads are stopped.
+        for worker in self._threads:
+            self._queue.put(_SHUTDOWNREQUEST)
+        
+        # Don't join currentThread (when stop is called inside a request).
+        current = threading.currentThread()
+        while self._threads:
+            worker = self._threads.pop()
+            if worker is not current and worker.isAlive():
+                try:
+                    if timeout is None or timeout < 0:
+                        worker.join()
+                    else:
+                        worker.join(timeout)
+                        if worker.isAlive():
+                            # We exhausted the timeout.
+                            # Forcibly shut down the socket.
+                            c = worker.conn
+                            if c and not c.rfile.closed:
+                                if SSL and isinstance(c.socket, SSL.ConnectionType):
+                                    # pyOpenSSL.socket.shutdown takes no args
+                                    c.socket.shutdown()
+                                else:
+                                    c.socket.shutdown(socket.SHUT_RD)
+                            worker.join()
+                except (AssertionError,
+                        # Ignore repeated Ctrl-C.
+                        # See http://www.cherrypy.org/ticket/691.
+                        KeyboardInterrupt), exc1:
+                    pass
+
+
+
+class SSLConnection:
+    """A thread-safe wrapper for an SSL.Connection.
+    
+    *args: the arguments to create the wrapped SSL.Connection(*args).
+    """
+    
+    def __init__(self, *args):
+        self._ssl_conn = SSL.Connection(*args)
+        self._lock = threading.RLock()
+    
+    for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
+              'renegotiate', 'bind', 'listen', 'connect', 'accept',
+              'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list',
+              'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
+              'makefile', 'get_app_data', 'set_app_data', 'state_string',
+              'sock_shutdown', 'get_peer_certificate', 'want_read',
+              'want_write', 'set_connect_state', 'set_accept_state',
+              'connect_ex', 'sendall', 'settimeout'):
+        exec """def %s(self, *args):
+        self._lock.acquire()
+        try:
+            return self._ssl_conn.%s(*args)
+        finally:
+            self._lock.release()
+""" % (f, f)
+
+
+try:
+    import fcntl
+except ImportError:
+    try:
+        from ctypes import windll, WinError
+    except ImportError:
+        def prevent_socket_inheritance(sock):
+            """Dummy function, since neither fcntl nor ctypes are available."""
+            pass
+    else:
+        def prevent_socket_inheritance(sock):
+            """Mark the given socket fd as non-inheritable (Windows)."""
+            if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
+                raise WinError()
+else:
+    def prevent_socket_inheritance(sock):
+        """Mark the given socket fd as non-inheritable (POSIX)."""
+        fd = sock.fileno()
+        old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+        fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
+
+
+class CherryPyWSGIServer(object):
+    """An HTTP server for WSGI.
+    
+    bind_addr: The interface on which to listen for connections.
+        For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+        or IPv6 address, or any valid hostname. The string 'localhost' is a
+        synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+        The string '0.0.0.0' is a special IPv4 entry meaning "any active
+        interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+        IPv6. The empty string or None are not allowed.
+        
+        For UNIX sockets, supply the filename as a string.
+    wsgi_app: the WSGI 'application callable'; multiple WSGI applications
+        may be passed as (path_prefix, app) pairs.
+    numthreads: the number of worker threads to create (default 10).
+    server_name: the string to set for WSGI's SERVER_NAME environ entry.
+        Defaults to socket.gethostname().
+    max: the maximum number of queued requests (defaults to -1 = no limit).
+    request_queue_size: the 'backlog' argument to socket.listen();
+        specifies the maximum number of queued connections (default 5).
+    timeout: the timeout in seconds for accepted connections (default 10).
+    
+    nodelay: if True (the default since 3.1), sets the TCP_NODELAY socket
+        option.
+    
+    protocol: the version string to write in the Status-Line of all
+        HTTP responses. For example, "HTTP/1.1" (the default). This
+        also limits the supported features used in the response.
+    
+    
+    SSL/HTTPS
+    ---------
+    The OpenSSL module must be importable for SSL functionality.
+    You can obtain it from http://pyopenssl.sourceforge.net/
+    
+    ssl_certificate: the filename of the server SSL certificate.
+    ssl_privatekey: the filename of the server's private key file.
+    
+    If either of these is None (both are None by default), this server
+    will not use SSL. If both are given and are valid, they will be read
+    on server start and used in the SSL context for the listening socket.
+    """
+    
+    protocol = "HTTP/1.1"
+    _bind_addr = "127.0.0.1"
+    version = "CherryPy/3.1.2"
+    ready = False
+    _interrupt = None
+    
+    nodelay = True
+    
+    ConnectionClass = HTTPConnection
+    environ = {}
+    
+    # Paths to certificate and private key files
+    ssl_certificate = None
+    ssl_private_key = None
+    
+    def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
+                 max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
+        self.requests = ThreadPool(self, min=numthreads or 1, max=max)
+        
+        if callable(wsgi_app):
+            # We've been handed a single wsgi_app, in CP-2.1 style.
+            # Assume it's mounted at "".
+            self.wsgi_app = wsgi_app
+        else:
+            # We've been handed a list of (path_prefix, wsgi_app) tuples,
+            # so that the server can call different wsgi_apps, and also
+            # correctly set SCRIPT_NAME.
+            warnings.warn("The ability to pass multiple apps is deprecated "
+                          "and will be removed in 3.2. You should explicitly "
+                          "include a WSGIPathInfoDispatcher instead.",
+                          DeprecationWarning)
+            self.wsgi_app = WSGIPathInfoDispatcher(wsgi_app)
+        
+        self.bind_addr = bind_addr
+        if not server_name:
+            server_name = socket.gethostname()
+        self.server_name = server_name
+        self.request_queue_size = request_queue_size
+        
+        self.timeout = timeout
+        self.shutdown_timeout = shutdown_timeout
+    
+    def _get_numthreads(self):
+        return self.requests.min
+    def _set_numthreads(self, value):
+        self.requests.min = value
+    numthreads = property(_get_numthreads, _set_numthreads)
+    
+    def __str__(self):
+        return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
+                              self.bind_addr)
+    
+    def _get_bind_addr(self):
+        return self._bind_addr
+    def _set_bind_addr(self, value):
+        if isinstance(value, tuple) and value[0] in ('', None):
+            # Despite the socket module docs, using '' does not
+            # allow AI_PASSIVE to work. Passing None instead
+            # returns '0.0.0.0' like we want. In other words:
+            #     host    AI_PASSIVE     result
+            #      ''         Y         192.168.x.y
+            #      ''         N         192.168.x.y
+            #     None        Y         0.0.0.0
+            #     None        N         127.0.0.1
+            # But since you can get the same effect with an explicit
+            # '0.0.0.0', we deny both the empty string and None as values.
+            raise ValueError("Host values of '' or None are not allowed. "
+                             "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
+                             "to listen on all active interfaces.")
+        self._bind_addr = value
+    bind_addr = property(_get_bind_addr, _set_bind_addr,
+        doc="""The interface on which to listen for connections.
+        
+        For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+        or IPv6 address, or any valid hostname. The string 'localhost' is a
+        synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+        The string '0.0.0.0' is a special IPv4 entry meaning "any active
+        interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+        IPv6. The empty string or None are not allowed.
+        
+        For UNIX sockets, supply the filename as a string.""")
+    
+    def start(self):
+        """Run the server forever."""
+        # We don't have to trap KeyboardInterrupt or SystemExit here,
+        # because cherrpy.server already does so, calling self.stop() for us.
+        # If you're using this server with another framework, you should
+        # trap those exceptions in whatever code block calls start().
+        self._interrupt = None
+        
+        # Select the appropriate socket
+        if isinstance(self.bind_addr, basestring):
+            # AF_UNIX socket
+            
+            # So we can reuse the socket...
+            try: os.unlink(self.bind_addr)
+            except: pass
+            
+            # So everyone can access the socket...
+            try: os.chmod(self.bind_addr, 0777)
+            except: pass
+            
+            info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
+        else:
+            # AF_INET or AF_INET6 socket
+            # Get the correct address family for our host (allows IPv6 addresses)
+            host, port = self.bind_addr
+            try:
+                info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+                                          socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
+            except socket.gaierror:
+                # Probably a DNS issue. Assume IPv4.
+                info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)]
+        
+        self.socket = None
+        msg = "No socket could be created"
+        for res in info:
+            af, socktype, proto, canonname, sa = res
+            try:
+                self.bind(af, socktype, proto)
+            except socket.error, msg:
+                if self.socket:
+                    self.socket.close()
+                self.socket = None
+                continue
+            break
+        if not self.socket:
+            raise socket.error, msg
+        
+        # Timeout so KeyboardInterrupt can be caught on Win32
+        self.socket.settimeout(1)
+        self.socket.listen(self.request_queue_size)
+        
+        # Create worker threads
+        self.requests.start()
+        
+        self.ready = True
+        while self.ready:
+            self.tick()
+            if self.interrupt:
+                while self.interrupt is True:
+                    # Wait for self.stop() to complete. See _set_interrupt.
+                    time.sleep(0.1)
+                if self.interrupt:
+                    raise self.interrupt
+    
+    def bind(self, family, type, proto=0):
+        """Create (or recreate) the actual socket object."""
+        self.socket = socket.socket(family, type, proto)
+        prevent_socket_inheritance(self.socket)
+        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if self.nodelay:
+            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+        if self.ssl_certificate and self.ssl_private_key:
+            if SSL is None:
+                raise ImportError("You must install pyOpenSSL to use HTTPS.")
+            
+            # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
+            ctx = SSL.Context(SSL.SSLv23_METHOD)
+            ctx.use_privatekey_file(self.ssl_private_key)
+            ctx.use_certificate_file(self.ssl_certificate)
+            self.socket = SSLConnection(ctx, self.socket)
+            self.populate_ssl_environ()
+            
+            # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
+            # activate dual-stack. See http://www.cherrypy.org/ticket/871.
+            if (not isinstance(self.bind_addr, basestring)
+                and self.bind_addr[0] == '::' and family == socket.AF_INET6):
+                try:
+                    self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
+                except (AttributeError, socket.error):
+                    # Apparently, the socket option is not available in
+                    # this machine's TCP stack
+                    pass
+        
+        self.socket.bind(self.bind_addr)
+    
+    def tick(self):
+        """Accept a new connection and put it on the Queue."""
+        try:
+            s, addr = self.socket.accept()
+            prevent_socket_inheritance(s)
+            if not self.ready:
+                return
+            if hasattr(s, 'settimeout'):
+                s.settimeout(self.timeout)
+            
+            environ = self.environ.copy()
+            # SERVER_SOFTWARE is common for IIS. It's also helpful for
+            # us to pass a default value for the "Server" response header.
+            if environ.get("SERVER_SOFTWARE") is None:
+                environ["SERVER_SOFTWARE"] = "%s WSGI Server" % self.version
+            # set a non-standard environ entry so the WSGI app can know what
+            # the *real* server protocol is (and what features to support).
+            # See http://www.faqs.org/rfcs/rfc2145.html.
+            environ["ACTUAL_SERVER_PROTOCOL"] = self.protocol
+            environ["SERVER_NAME"] = self.server_name
+            
+            if isinstance(self.bind_addr, basestring):
+                # AF_UNIX. This isn't really allowed by WSGI, which doesn't
+                # address unix domain sockets. But it's better than nothing.
+                environ["SERVER_PORT"] = ""
+            else:
+                environ["SERVER_PORT"] = str(self.bind_addr[1])
+                # optional values
+                # Until we do DNS lookups, omit REMOTE_HOST
+                environ["REMOTE_ADDR"] = addr[0]
+                environ["REMOTE_PORT"] = str(addr[1])
+            
+            conn = self.ConnectionClass(s, self.wsgi_app, environ)
+            self.requests.put(conn)
+        except socket.timeout:
+            # The only reason for the timeout in start() is so we can
+            # notice keyboard interrupts on Win32, which don't interrupt
+            # accept() by default
+            return
+        except socket.error, x:
+            if x.args[0] in socket_error_eintr:
+                # I *think* this is right. EINTR should occur when a signal
+                # is received during the accept() call; all docs say retry
+                # the call, and I *think* I'm reading it right that Python
+                # will then go ahead and poll for and handle the signal
+                # elsewhere. See http://www.cherrypy.org/ticket/707.
+                return
+            if x.args[0] in socket_errors_nonblocking:
+                # Just try again. See http://www.cherrypy.org/ticket/479.
+                return
+            if x.args[0] in socket_errors_to_ignore:
+                # Our socket was closed.
+                # See http://www.cherrypy.org/ticket/686.
+                return
+            raise
+    
+    def _get_interrupt(self):
+        return self._interrupt
+    def _set_interrupt(self, interrupt):
+        self._interrupt = True
+        self.stop()
+        self._interrupt = interrupt
+    interrupt = property(_get_interrupt, _set_interrupt,
+                         doc="Set this to an Exception instance to "
+                             "interrupt the server.")
+    
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        self.ready = False
+        
+        sock = getattr(self, "socket", None)
+        if sock:
+            if not isinstance(self.bind_addr, basestring):
+                # Touch our own socket to make accept() return immediately.
+                try:
+                    host, port = sock.getsockname()[:2]
+                except socket.error, x:
+                    if x.args[0] not in socket_errors_to_ignore:
+                        raise
+                else:
+                    # Note that we're explicitly NOT using AI_PASSIVE,
+                    # here, because we want an actual IP to touch.
+                    # localhost won't work if we've bound to a public IP,
+                    # but it will if we bound to '0.0.0.0' (INADDR_ANY).
+                    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+                                                  socket.SOCK_STREAM):
+                        af, socktype, proto, canonname, sa = res
+                        s = None
+                        try:
+                            s = socket.socket(af, socktype, proto)
+                            # See http://groups.google.com/group/cherrypy-users/
+                            #        browse_frm/thread/bbfe5eb39c904fe0
+                            s.settimeout(1.0)
+                            s.connect((host, port))
+                            s.close()
+                        except socket.error:
+                            if s:
+                                s.close()
+            if hasattr(sock, "close"):
+                sock.close()
+            self.socket = None
+        
+        self.requests.stop(self.shutdown_timeout)
+    
+    def populate_ssl_environ(self):
+        """Create WSGI environ entries to be merged into each request."""
+        cert = open(self.ssl_certificate, 'rb').read()
+        cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+        ssl_environ = {
+            "wsgi.url_scheme": "https",
+            "HTTPS": "on",
+            # pyOpenSSL doesn't provide access to any of these AFAICT
+##            'SSL_PROTOCOL': 'SSLv2',
+##            SSL_CIPHER 	string 	The cipher specification name
+##            SSL_VERSION_INTERFACE 	string 	The mod_ssl program version
+##            SSL_VERSION_LIBRARY 	string 	The OpenSSL program version
+            }
+        
+        # Server certificate attributes
+        ssl_environ.update({
+            'SSL_SERVER_M_VERSION': cert.get_version(),
+            'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
+##            'SSL_SERVER_V_START': Validity of server's certificate (start time),
+##            'SSL_SERVER_V_END': Validity of server's certificate (end time),
+            })
+        
+        for prefix, dn in [("I", cert.get_issuer()),
+                           ("S", cert.get_subject())]:
+            # X509Name objects don't seem to have a way to get the
+            # complete DN string. Use str() and slice it instead,
+            # because str(dn) == "<X509Name object '/C=US/ST=...'>"
+            dnstr = str(dn)[18:-2]
+            
+            wsgikey = 'SSL_SERVER_%s_DN' % prefix
+            ssl_environ[wsgikey] = dnstr
+            
+            # The DN should be of the form: /k1=v1/k2=v2, but we must allow
+            # for any value to contain slashes itself (in a URL).
+            while dnstr:
+                pos = dnstr.rfind("=")
+                dnstr, value = dnstr[:pos], dnstr[pos + 1:]
+                pos = dnstr.rfind("/")
+                dnstr, key = dnstr[:pos], dnstr[pos + 1:]
+                if key and value:
+                    wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
+                    ssl_environ[wsgikey] = value
+        
+        self.environ.update(ssl_environ)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/review/web_ui.py	Tue Oct 13 18:34:55 2009 -0400
@@ -0,0 +1,24 @@
+"""The review extension's web UI."""
+
+import sys, os
+
+top_path = os.path.split(os.path.split(os.path.realpath(__file__))[0])[0]
+bundled_path = os.path.join(top_path, 'bundled')
+webpy_path = os.path.join(bundled_path, 'webpy')
+
+sys.path.insert(0, webpy_path)
+import web
+
+
+urls = (
+    '/', 'index'
+)
+
+class index:
+    def GET(self):
+        return "Hello, world!"
+
+app = web.application(urls, globals())
+
+if __name__ == "__main__":
+    app.run()
\ No newline at end of file