add python-webpy, a lightweight framework for web applications
authorMichael Lauer <mickey@vanille-media.de>
Sat, 8 Jul 2006 11:02:01 +0000 (11:02 +0000)
committerOpenEmbedded Project <openembedded-devel@lists.openembedded.org>
Sat, 8 Jul 2006 11:02:01 +0000 (11:02 +0000)
packages/meta/task-python-everything_20060425.bb
packages/meta/task-python-sharprom_20060425.bb
packages/python/python-webpy/.mtn2git_empty [new file with mode: 0644]
packages/python/python-webpy/web.py [new file with mode: 0644]
packages/python/python-webpy_0.138.bb [new file with mode: 0644]

index 738b823..213ed0f 100644 (file)
@@ -2,7 +2,7 @@ DESCRIPTION= "Everything Python"
 MAINTAINER = "Michael 'Mickey' Lauer <mickey@Vanille.de>"
 HOMEPAGE = "http://www.vanille.de/projects/python.spy"
 LICENSE = "MIT"
-PR = "ml7"
+PR = "ml8"
 
 BROKEN_BECAUSE_GCC4 = "\
                python-egenix-mx-base"
@@ -68,6 +68,7 @@ RDEPENDS = "\
                python-urwid            \
                python-vmaps            \
                python-vorbis           \
+               python-webpy            \
                moin                    \
                plone                   \
                twisted                 \
index 8ce0dcf..4984127 100644 (file)
@@ -2,7 +2,7 @@ DESCRIPTION = "Everything Python for SharpROM"
 MAINTAINER = "Michael 'Mickey' Lauer <mickey@Vanille.de>"
 HOMEPAGE = "http://www.vanille.de/projects/python.spy"
 LICENSE = "MIT"
-PR = "ml4"
+PR = "ml5"
 
 NONWORKING = "\
                python-codes            \
@@ -63,6 +63,7 @@ RDEPENDS = "\
                python-tlslite          \
                python-urwid            \
                python-vmaps            \
+               python-webpy            \
                moin                    \
                plone                   \
                twisted                 \
diff --git a/packages/python/python-webpy/.mtn2git_empty b/packages/python/python-webpy/.mtn2git_empty
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/packages/python/python-webpy/web.py b/packages/python/python-webpy/web.py
new file mode 100644 (file)
index 0000000..2761fa3
--- /dev/null
@@ -0,0 +1,2349 @@
+#!/usr/bin/env python
+"""web.py: makes web apps (http://webpy.org)"""
+__version__ = "0.1381"
+__revision__ = "$Rev: 72 $"
+__license__ = "public domain"
+__author__ = "Aaron Swartz <me@aaronsw.com>"
+__contributors__ = "see http://webpy.org/changes"
+
+from __future__ import generators
+
+# long term todo:
+#   - new form system
+#   - new templating system
+#   - unit tests?
+
+# todo:
+#   - get rid of upvars
+#   - break up into separate files
+#   - provide an option to use .write()
+#   - allow people to do $self.id from inside a reparam
+#   - add sqlite support
+#   - convert datetimes, floats in WebSafe
+#   - locks around memoize
+#   - fix memoize to use cacheify style techniques
+#   - merge curval query with the insert
+#   - figure out how to handle squid, etc. for web.ctx.ip
+
+import os, os.path, sys, time, types, traceback, threading
+import cgi, re, urllib, urlparse, Cookie, pprint
+from threading import currentThread
+from tokenize import tokenprog
+iters = (list, tuple)
+if hasattr(__builtins__, 'set') or (
+  hasattr(__builtins__, 'has_key') and __builtins__.has_key('set')):
+    iters += (set,)
+try: 
+    from sets import Set
+    iters += (Set,)
+except ImportError: 
+    pass
+try: 
+    import datetime, itertools
+except ImportError: 
+    pass
+try:
+    from Cheetah.Compiler import Compiler
+    from Cheetah.Filters import Filter
+    _hasTemplating = True
+except ImportError:
+    _hasTemplating = False
+
+try:
+    from DBUtils.PooledDB import PooledDB
+    _hasPooling = True
+except ImportError:
+    _hasPooling = False
+
+# 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
+
+## General Utilities
+
+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`"""
+    return _strips('r', text, remove)
+
+def lstrips(text, remove):
+    """removes the string `remove` from the left of `text`"""
+    return _strips('l', text, remove)
+
+def strips(text, remove):
+    """removes the string `remove` from the both sides of `text`"""
+    return rstrips(lstrips(text, remove), remove)
+
+def autoassign(self, locals):
+    """
+    Automatically assigns local variables to `self`.
+    Generally used in `__init__` methods, as in:
+
+        def __init__(self, foo, bar, baz=1): autoassign(self, locals())
+    """
+    #locals = sys._getframe(1).f_locals
+    #self = locals['self']
+    for (key, value) in locals.iteritems():
+        if key == 'self': 
+            continue
+        setattr(self, key, value)
+
+class Storage(dict):
+    """
+    A Storage object is like a dictionary except `obj.foo` can be used
+    instead of `obj['foo']`. Create one by doing `storage({'a':1})`.
+    """
+    def __getattr__(self, key): 
+        if self.has_key(key): 
+            return self[key]
+        raise AttributeError, repr(key)
+    def __setattr__(self, key, value): 
+        self[key] = value
+    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
+        {}
+    
+    """
+    def getvalue(x):
+        if hasattr(x, 'value'):
+            return x.value
+        else:
+            return 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
+
+class Memoize:
+    """
+    'Memoizes' a function, caching its return values for each input.
+    """
+    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."""
+    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.
+
+    For example, `list(group([1,2,3,4], 2))` returns `[[1,2],[3,4]]`.
+    """
+    if not hasattr(seq, 'next'):  
+        seq = iter(seq)
+    while True: 
+        yield [seq.next() for i in xrange(size)]
+
+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]`).
+    """
+    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):
+    """Takes a dictionary like `{1:2, 3:4}` and returns `{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.
+    """
+    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, [].
+    """
+    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.
+    """
+    dictionary.setdefault(element, 0)
+    dictionary[element] += 1
+    return dictionary[element]
+
+def dictadd(dict_a, dict_b):
+    """
+    Returns a dictionary consisting of the keys in `a` and `b`.
+    If they share a key, the value from b is used.
+    """
+    result = {}
+    result.update(dict_a)
+    result.update(dict_b)
+    return result
+
+sumdicts = dictadd # deprecated
+
+def listget(lst, ind, default=None):
+    """Returns `lst[ind]` if it exists, `default` otherwise."""
+    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."""
+    try:
+        return int(integer)
+    except (TypeError, ValueError):
+        return default
+
+def datestr(then, now=None):
+    """Converts a (UTC) datetime object to a nice string representation."""
+    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 now: now = datetime.datetime.utcnow()
+    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 upvars(level=2):
+    """Guido van Rossum doesn't want you to use this function."""
+    return dictadd(
+      sys._getframe(level).f_globals,
+      sys._getframe(level).f_locals)
+
+class CaptureStdout:
+    """
+    Captures everything func prints to stdout and returns it instead.
+
+    **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.
+    """
+    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()
+        stats = hotshot.stats.load(temp.name)
+        stats.strip_dirs()
+        stats.sort_stats('time', 'calls')
+        x =  '\n\ntook '+ str(stime) + ' seconds\n'
+        x += capturestdout(stats.print_stats)(40)
+        x += capturestdout(stats.print_callers)()
+        return result, x
+profile = Profile
+
+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.
+
+    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:
+    """
+    Takes a dictionary that maps threads to objects. 
+    When a thread tries to get or set an attribute or item 
+    of the threadeddict, it passes it on to the object 
+    for that thread in dictionary.
+    """
+    def __init__(self, dictionary): 
+        self.__dict__['_ThreadedDict__d'] = dictionary
+    def __getattr__(self, attr): 
+        return getattr(self.__d[currentThread()], attr)
+    def __getitem__(self, item): 
+        return self.__d[currentThread()][item]
+    def __setattr__(self, attr, value):
+        if attr == '__doc__':
+            self.__dict__[attr] = value
+        else:
+            return setattr(self.__d[currentThread()], attr, value)
+    def __setitem__(self, item, value): 
+        self.__d[currentThread()][item] = value
+    def __hash__(self): 
+        return hash(self.__d[currentThread()])
+threadeddict = ThreadedDict
+
+## IP Utilities
+
+def validipaddr(address):
+    """returns True if `address` is a valid IPv4 address"""
+    try:
+        octets = address.split('.')
+        assert len(octets) == 4
+        for x in octets:
+            assert 0 <= int(x) <= 255
+    except (AssertionError, ValueError):
+        return False
+    return True
+
+def validipport(port):
+    """returns True if `port` is a valid IPv4 port"""
+    try:
+        assert 0 <= int(port) <= 65535
+    except (AssertionError, 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_"""
+    if '/' in string_:
+        return string_
+    else:
+        return validip(string_)
+
+## URL Utilities
+
+def prefixurl(base=''):
+    """
+    Sorry, this function is really difficult to explain.
+    Maybe some other time.
+    """
+    url = ctx.path.lstrip('/')
+    for i in xrange(url.count('/')): 
+        base += '../'
+    if not base: 
+        base = './'
+    return base
+
+def urlquote(x): return urllib.quote(websafe(x).encode('utf-8'))
+
+## Formatting
+
+try:
+    from markdown import markdown # http://webpy.org/markdown.py
+except ImportError: 
+    pass
+
+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))
+    """
+    if text:
+        text = text.replace('<', '&lt;')
+        # TODO: automatically get page title?
+        text = r_url.sub(r'<\1>', text)
+        text = markdown(text)
+        return text
+
+## Databases
+
+class _ItplError(ValueError):
+    """String Interpolation Error
+    from <http://lfw.org/python/Itpl.py>
+    (cf. below for license)
+    """
+    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)
+
+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)
+    """
+    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
+
+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.
+    
+    For example:
+    
+        web.sqlors('foo =', [1,2,3])
+    
+    would result in:
+    
+        foo = 1 OR foo = 2 OR foo = 3
+    """
+    if isinstance(lst, iters):
+        lst = list(lst)
+        ln = len(lst)
+        if ln == 0:
+            return ("2+2=5", [])
+        if ln == 1: 
+            lst = lst[0]
+
+    if isinstance(lst, iters):
+        return '(' + left + \
+               (' OR ' + left).join([aparam() for param in lst]) + ")", lst
+    else:
+        return left + aparam(), [lst]
+
+class UnknownParamstyle(Exception):
+    """raised for unsupported db paramstyles
+    
+    Currently supported: qmark,numeric, format, pyformat
+    """
+    pass
+
+def aparam():
+    """Use in a SQL string to make a spot for a db value."""
+    style = ctx.db_module.paramstyle
+    if style == 'qmark': 
+        return '?'
+    elif style == 'numeric': 
+        return ':1'
+    elif style in ['format', 'pyformat']: 
+        return '%s'
+    raise UnknownParamstyle, style
+
+def reparam(string_, dictionary):
+    """
+    Takes a string and a dictionary and interpolates the string
+    using values from the dictionary. Returns a 2-tuple containing
+    the a string with `aparam()`s in it and a list of the matching values.
+    
+    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.
+    """
+    vals = []
+    result = []
+    for live, chunk in _interpolate(string_):
+        if live:
+            result.append(aparam())
+            vals.append(eval(chunk, dictionary))
+        else: result.append(chunk)
+    return ''.join(result), vals
+
+class UnknownDB(Exception):
+    """raised for unsupported dbms"""
+    pass
+def connect(dbn, **keywords):
+    """
+    Connects to the specified database. 
+    db currently must be "postgres" or "mysql". 
+    If DBUtils is installed, connection pooling will be used.
+    """
+    if dbn == "postgres": 
+        try: 
+            import psycopg2 as db
+        except ImportError: 
+            try: 
+                import psycopg as db
+            except ImportError: 
+                import pgdb as db
+        keywords['password'] = keywords['pw']
+        del keywords['pw']
+        keywords['database'] = keywords['db']
+        del keywords['db']
+    elif dbn == "mysql":
+        import MySQLdb as db
+        keywords['passwd'] = keywords['pw']
+        del keywords['pw']
+        db.paramstyle = 'pyformat' # it's both, like psycopg
+    elif dbn == "sqlite":
+        try: ## try first sqlite3 version
+            from pysqlite2 import dbapi2 as db
+            db.paramstyle = 'qmark'
+        except ImportError: ## else try sqlite2
+            import sqlite as db
+        keywords['database'] = keywords['db']
+        del keywords['db']
+    else: 
+        raise UnknownDB, dbn
+    ctx.db_name = dbn
+    ctx.db_module = db
+    ctx.db_transaction = False
+    if _hasPooling:
+        if 'db' not in globals(): 
+            globals()['db'] = PooledDB(dbapi=db, **keywords)
+        ctx.db = globals()['db'].connection()
+    else:
+        ctx.db = db.connect(**keywords)
+    ctx.dbq_count = 0
+    if globals().get('db_printing'):
+        def db_execute(cur, sql_query, d=None):
+            """executes an sql query"""
+            
+            def sqlquote(obj):
+                """converts `obj` to its proper SQL version"""
+                
+                # 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 isinstance(obj, datetime.datetime):
+                    return repr(obj.isoformat())
+                else:
+                    return repr(obj)
+            
+            ctx.dbq_count += 1
+            try: 
+                outq = sql_query % tuple(map(sqlquote, d))
+            except TypeError:
+                outq = sql_query
+            print >> debug, str(ctx.dbq_count)+':', outq
+            a = time.time()
+            out = cur.execute(sql_query, d)
+            b = time.time()
+            print >> debug, '(%s)' % round(b - a, 2)
+            return out
+        ctx.db_execute = db_execute
+    else:
+        ctx.db_execute = lambda cur, sql_query, d=None: \
+                                cur.execute(sql_query, d)
+    return ctx.db
+
+def transact():
+    """Start a transaction."""
+    # commit everything up to now, so we don't rollback it later
+    ctx.db.commit()
+    ctx.db_transaction = True
+
+def commit():
+    """Commits a transaction."""
+    ctx.db.commit()
+    ctx.db_transaction = False
+
+def rollback():
+    """Rolls back a transaction."""
+    ctx.db.rollback()
+    ctx.db_transaction = False    
+
+def query(sql_query, vars=None, processed=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.
+    """
+    if vars is None: 
+        vars = {}
+    db_cursor = ctx.db.cursor()
+
+    if not processed: 
+        sql_query, vars = reparam(sql_query, vars)
+    ctx.db_execute(db_cursor, sql_query, vars)
+    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 ctx.db_transaction: 
+        ctx.db.commit()    
+    return out
+
+def sqllist(lst):
+    """
+    If a list, converts it to a comma-separated string. 
+    Otherwise, returns the string.
+    """
+    if isinstance(lst, str): 
+        return lst
+    else: return ', '.join(lst)
+
+def sqlwhere(dictionary):
+    """
+    Converts a `dictionary` to an SQL WHERE clause in
+    `reparam` format. Thus,
+    
+        {'cust_id': 2, 'order_id':3}
+    
+    would result in the equivalent of:
+    
+        'cust_id = 2 AND order_id = 3'
+    
+    but properly quoted.
+    """
+    
+    return ' AND '.join([
+      '%s = %s' % (k, aparam()) for k in dictionary.keys()
+    ]), dictionary.values()
+
+def select(tables, vars=None, what='*', where=None, order=None, group=None, 
+           limit=None, offset=None):
+    """
+    Selects `what` from `tables` with clauses `where`, `order`, 
+    `group`, `limit`, and `offset. Uses vars to interpolate. 
+    Otherwise, each clause can take a reparam-style list.
+    """
+    if vars is None: 
+        vars = {}
+    values = []
+    qout = ""
+    
+    for (sql, val) in (
+      ('SELECT', what),
+      ('FROM', sqllist(tables)),
+      ('WHERE', where), 
+      ('GROUP BY', group), 
+      ('ORDER BY', order), 
+      ('LIMIT', limit), 
+      ('OFFSET', offset)):
+        if isinstance(val, (int, long)):
+            if sql == 'WHERE':
+                nquery, nvalue = 'id = '+aparam(), [val]
+            else:
+                nquery, nvalue = str(val), ()
+        elif isinstance(val, (list, tuple)) and len(val) == 2:
+            nquery, nvalue = val
+        elif val:
+            nquery, nvalue = reparam(val, vars)
+        else: 
+            continue
+        qout += " " + sql + " " + nquery
+        values.extend(nvalue)
+    return query(qout, values, processed=True)
+
+def insert(tablename, seqname=None, **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_cursor = ctx.db.cursor()
+
+    if values:
+        sql_query, v = "INSERT INTO %s (%s) VALUES (%s)" % (
+            tablename,
+            ", ".join(values.keys()),
+            ', '.join([aparam() for x in values])
+        ), values.values()
+    else:
+        sql_query, v = "INSERT INTO %s DEFAULT VALUES" % tablename, None
+
+    if seqname is False: 
+        pass
+    elif ctx.db_name == "postgres": 
+        if seqname is None: 
+            seqname = tablename + "_id_seq"
+        sql_query += "; SELECT currval('%s')" % seqname
+    elif ctx.db_name == "mysql":
+        ctx.db_execute(db_cursor, sql_query, v)
+        sql_query = "SELECT last_insert_id()"
+        v = ()
+    elif ctx.db_name == "sqlite":
+        ctx.db_execute(db_cursor, sql_query, v)
+        # not really the same...
+        sql_query = "SELECT last_insert_rowid()"
+        v = ()
+
+    ctx.db_execute(db_cursor, sql_query, v)
+    try: 
+        out = db_cursor.fetchone()[0]
+    except Exception: 
+        out = None
+    
+    if not ctx.db_transaction: 
+        ctx.db.commit()
+
+    return out
+
+def update(tables, where, vars=None, **values):
+    """
+    Update `tables` with clause `where` (interpolated using `vars`)
+    and setting `values`.
+    """
+    if vars is None: 
+        vars = {}
+    if isinstance(where, (int, long)):
+        vars = [where]
+        where = "id = " + aparam()
+    elif isinstance(where, (list, tuple)) and len(where) == 2:
+        where, vars = where
+    else:
+        where, vars = reparam(where, vars)
+    
+    db_cursor = ctx.db.cursor()
+    ctx.db_execute(db_cursor, "UPDATE %s SET %s WHERE %s" % (
+        sqllist(tables),
+        ', '.join([k + '=' + aparam() for k in values.keys()]),
+        where),
+    values.values() + vars)
+    
+    if not ctx.db_transaction: 
+        ctx.db.commit()        
+    return db_cursor.rowcount
+
+def delete(table, where, using=None, vars=None):
+    """
+    Deletes from `table` with clauses `where` and `using`.
+    """
+    if vars is None: 
+        vars = {}
+    db_cursor = ctx.db.cursor()
+
+    if isinstance(where, (int, long)):
+        vars = [where]
+        where = "id = " + aparam()
+    elif isinstance(where, (list, tuple)) and len(where) == 2:
+        where, vars = where
+    else:
+        where, vars = reparam(where, vars)
+    q = 'DELETE FROM %s WHERE %s' % (table, where)
+    if using: 
+        q += ' USING ' + sqllist(using)
+    ctx.db_execute(db_cursor, q, vars)
+
+    if not ctx.db_transaction: 
+        ctx.db.commit()
+    return db_cursor.rowcount
+
+## Request Handlers
+
+def handle(mapping, fvars=None):
+    """
+    Call the appropriate function based on the url to function mapping in `mapping`.
+    If no module for the function is specified, look up the function in `fvars`. If
+    `fvars` is empty, using the caller's context.
+
+    `mapping` should be a tuple of paired regular expressions with function name
+    substitutions. `handle` will import modules as necessary.
+    """
+    for url, ofno in group(mapping, 2):
+        if isinstance(ofno, tuple): 
+            ofn, fna = ofno[0], list(ofno[1:])
+        else: 
+            ofn, fna = ofno, []
+        fn, result = re_subm('^' + url + '$', ofn, ctx.path)
+        if result: # it's a match
+            if fn.split(' ', 1)[0] == "redirect":
+                url = fn.split(' ', 1)[1]
+                if ctx.method == "GET":
+                    x = ctx.env.get('QUERY_STRING', '')
+                    if x: 
+                        url += '?' + x
+                return redirect(url)
+            elif '.' in fn: 
+                x = fn.split('.')
+                mod, cls = '.'.join(x[:-1]), x[-1]
+                mod = __import__(mod, globals(), locals(), [""])
+                cls = getattr(mod, cls)
+            else:
+                cls = fn
+                mod = fvars or upvars()
+                if isinstance(mod, types.ModuleType): 
+                    mod = vars(mod)
+                try: 
+                    cls = mod[cls]
+                except KeyError: 
+                    return notfound()
+            
+            meth = ctx.method
+            if meth == "HEAD":
+                if not hasattr(cls, meth): 
+                    meth = "GET"
+            if not hasattr(cls, meth): 
+                return nomethod(cls)
+            tocall = getattr(cls(), meth)
+            args = list(result.groups())
+            for d in re.findall(r'\\(\d+)', ofn):
+                args.pop(int(d) - 1)
+            return tocall(*([urllib.unquote(x) for x in args] + fna))
+
+    return notfound()
+
+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 notfound()
+        else:
+            return notfound()
+    return internal
+
+def background(func):
+    """A function decorator to run a long-running function as a background thread."""
+    def internal(*a, **kw):
+        data() # cache it
+        ctx = _context[currentThread()]
+        _context[currentThread()] = storage(ctx.copy())
+
+        def newfunc():
+            _context[currentThread()] = ctx
+            func(*a, **kw)
+
+        t = threading.Thread(target=newfunc)
+        background.threaddb[id(t)] = t
+        t.start()
+        ctx.headers = []
+        return seeother(changequery(_t=id(t)))
+    return internal
+background.threaddb = {}
+
+def backgrounder(func):
+    def internal(*a, **kw):
+        i = input(_method='get')
+        if '_t' in i:
+            try:
+                t = background.threaddb[int(i._t)]
+            except KeyError:
+                return notfound()
+            _context[currentThread()] = _context[t]
+            return
+        else:
+            return func(*a, **kw)
+    return internal
+
+## HTTP Functions
+
+def httpdate(date_obj):
+    """Formats a datetime object for use in HTTP headers."""
+    return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
+
+def parsehttpdate(string_):
+    """Parses an HTTP date into a datetime object."""
+    try:
+        t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
+    except ValueError:
+        return None
+    return datetime.datetime(*t[:6])
+
+def expires(delta):
+    """
+    Outputs an `Expires` header for `delta` from now. 
+    `delta` is a `timedelta` object or a number of seconds.
+    """
+    try:    
+        datetime
+    except NameError: 
+        raise Exception, "requires Python 2.3 or later"
+    if isinstance(delta, (int, long)):
+        delta = datetime.timedelta(seconds=delta)
+    date_obj = datetime.datetime.utcnow() + delta
+    header('Expires', httpdate(date_obj))
+
+def lastmodified(date_obj):
+    """Outputs a `Last-Modified` header for `datetime`."""
+    header('Last-Modified', httpdate(date_obj))
+
+def modified(date=None, etag=None):
+    n = ctx.env.get('HTTP_IF_NONE_MATCH')
+    m = parsehttpdate(ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
+    validate = False
+    if etag:
+        raise NotImplementedError, "no etag support yet"
+        # should really be a warning
+    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: ctx.status = '304 Not Modified'
+    return not validate
+    
+"""
+By default, these all return simple error messages that send very short messages 
+(like "bad request") to the user. They can and should be overridden 
+to return nicer ones.
+"""
+def redirect(url, status='301 Moved Permanently'):
+    """
+    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.home + ctx.path, url)
+    ctx.status = status
+    ctx.output = ''    
+    header('Content-Type', 'text/html')
+    header('Location', newloc)
+    # seems to add a three-second delay for some reason:
+    # output('<a href="'+ newloc + '">moved permanently</a>')
+
+def found(url):
+    """A `302 Found` redirect."""
+    return redirect(url, '302 Found')
+
+def seeother(url):
+    """A `303 See Other` redirect."""
+    return redirect(url, '303 See Other')
+
+def tempredirect(url):
+    """A `307 Temporary Redirect` redirect."""
+    return redirect(url, '307 Temporary Redirect')
+
+def badrequest():
+    """Return a `400 Bad Request` error."""
+    ctx.status = '400 Bad Request'
+    header('Content-Type', 'text/html')
+    return output('bad request')
+
+def notfound():
+    """Returns a `404 Not Found` error."""
+    ctx.status = '404 Not Found'
+    header('Content-Type', 'text/html')
+    return output('not found')
+
+def nomethod(cls):
+    """Returns a `405 Method Not Allowed` error for `cls`."""
+    ctx.status = '405 Method Not Allowed'
+    header('Content-Type', 'text/html')
+    header('Allow', \
+           ', '.join([method for method in \
+                     ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \
+                        if hasattr(cls, method)]))
+    
+    # commented out for the same reason redirect is
+    # return output('method not allowed')
+
+def gone():
+    """Returns a `410 Gone` error."""
+    ctx.status = '410 Gone'
+    header('Content-Type', 'text/html')
+    return output("gone")
+
+def internalerror():
+    """Returns a `500 Internal Server` error."""
+    ctx.status = "500 Internal Server Error"
+    ctx.headers = [('Content-Type', 'text/html')]
+    ctx.output = "internal server error"
+
+
+# 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
+
+DJANGO_500_PAGE = """#import inspect
+<!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>
+
+<div id="summary">
+  <h1>$exception_type at $ctx.path</h1>
+  <h2>$exception_value</h2>
+  <table><tr>
+    <th>Python</th>
+    <td>$lastframe.filename in $lastframe.function, line $lastframe.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>#end for#</ol>
+            #end if
+            <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>#end for#</ol>
+            #end if
+          </div>
+        #end if
+
+        #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>
+          <table class="vars" id="v$frame.id">
+            <thead>
+              <tr>
+                <th>Variable</th>
+                <th>Value</th>
+              </tr>
+            </thead>
+            <tbody>
+              #set frameitems = $frame.vars
+              #silent frameitems.sort(lambda x,y: cmp(x[0], y[0]))
+              #for (key, val) in frameitems
+                <tr>
+                  <td>$key</td>
+                  <td class="code"><div>$prettify(val)</div></td>
+                </tr>
+              #end for
+            </tbody>
+          </table>
+        #end if
+      </li>
+    #end for
+  </ul>
+</div>
+
+<div id="requestinfo">
+  #if $context_.output or $context_.headers
+    <h2>Response so far</h2>
+    <h3>HEADERS</h3>
+    #if $ctx.headers
+      <p class="req"><code>
+      #for (k, v) in $context_.headers
+        $k: $v<br />
+      #end for
+      
+      </code></p>
+    #else
+      <p>No headers.</p>
+    #end if
+    <h3>BODY</h3>
+    <p class="req" style="padding-bottom: 2em"><code>
+    $context_.output
+    </code></p>
+  #end if
+  
+  <h2>Request information</h2>
+
+  <h3>INPUT</h3>
+  #if $input_
+    <table class="req">
+      <thead>
+        <tr>
+          <th>Variable</th>
+          <th>Value</th>
+        </tr>
+      </thead>
+      <tbody>
+        #set myitems = $input_.items()
+        #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
+        #for (key, val) in myitems
+          <tr>
+            <td>$key</td>
+            <td class="code"><div>$val</div></td>
+          </tr>
+        #end for
+      </tbody>
+    </table>
+  #else
+  <p>No input data.</p>
+  #end if
+
+  <h3 id="cookie-info">COOKIES</h3>
+  #if $cookies_
+    <table class="req">
+      <thead>
+        <tr>
+          <th>Variable</th>
+          <th>Value</th>
+        </tr>
+      </thead>
+      <tbody>
+        #for (key, val) in $cookies_.items()
+          <tr>
+            <td>$key</td>
+            <td class="code"><div>$val</div></td>
+          </tr>
+        #end for
+      </tbody>
+    </table>
+  #else
+    <p>No cookie data</p>
+  #end if
+
+  <h3 id="meta-info">META</h3>
+  <table class="req">
+    <thead>
+      <tr>
+        <th>Variable</th>
+        <th>Value</th>
+      </tr>
+    </thead>
+    <tbody>
+      #set myitems = $context_.items()
+      #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
+      #for (key, val) in $myitems
+      #if not $key.startswith('_') and $key not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']
+        <tr>
+          <td>$key</td>
+          <td class="code"><div>$prettify($val)</div></td>
+        </tr>
+      #end if
+      #end for
+    </tbody>
+  </table>
+
+  <h3 id="meta-info">ENVIRONMENT</h3>
+  <table class="req">
+    <thead>
+      <tr>
+        <th>Variable</th>
+        <th>Value</th>
+      </tr>
+    </thead>
+    <tbody>
+      #set myitems = $context_.env.items()
+      #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))  
+      #for (key, val) in $myitems
+        <tr>
+          <td>$key</td>
+          <td class="code"><div>$prettify($val)</div></td>
+        </tr>
+      #end for
+    </tbody>
+  </table>
+
+</div>
+
+<div id="explanation">
+  <p>
+    You're seeing this error because you have <code>web.internalerror</code>
+    set to <code>web.debugerror</code>. Change that if you want a different one.
+  </p>
+</div>
+
+</body>
+</html>"""
+
+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({
+            'tback': tback,
+            'filename': filename,
+            'function': function,
+            'lineno': lineno,
+            'vars': tback.tb_frame.f_locals.items(),
+            '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
+    lastframe = frames[-1]
+    frames.reverse()
+    urljoin = urlparse.urljoin
+    input_ = input()
+    cookies_ = cookies()
+    context_ = ctx
+    def prettify(x):
+        try: 
+            out = pprint.pformat(x)
+        except Exception, e: 
+            out = '[could not display: <' + e.__class__.__name__ + \
+                  ': '+str(e)+'>]'
+        return out
+    return render(DJANGO_500_PAGE, asTemplate=True, isString=True)
+
+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/).)
+
+    Requires [Cheetah](http://cheetahtemplate.org/).
+    """
+    # need to do django first, so it can get the old stuff
+    if _hasTemplating:
+        out = str(djangoerror())
+    else:
+        # Cheetah isn't installed
+        out = """<p>You've set web.py to use the fancier debugerror error 
+messages, but these messages require you install the Cheetah template 
+system. For more information, see 
+<a href="http://webpy.org/">the web.py website</a>.</p>
+
+<p>In the meantime, here's a plain old error message:</p>
+
+<pre>%s</pre>
+
+<p>(If it says something about 'Compiler', then it's probably
+because you're trying to use templates and you haven't
+installed Cheetah. See above.)</p>
+""" % htmlquote(traceback.format_exc())
+    ctx.status = "500 Internal Server Error"
+    ctx.headers = [('Content-Type', 'text/html')]
+    ctx.output = out
+
+
+## Rendering
+
+r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
+def __compiletemplate(template, base=None, isString=False):
+    if isString: 
+        text = template
+    else: 
+        text = open('templates/'+template).read()
+    # implement #include at compile-time
+    def do_include(match):
+        text = open('templates/'+match.groups()[0]).read()
+        return text
+    while r_include.findall(text): 
+        text = r_include.sub(do_include, text)
+
+    execspace = _compiletemplate.bases.copy()
+    tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
+    tmpl_compiler.addImportedVarNames(execspace.keys())
+    exec str(tmpl_compiler) in execspace
+    if base: 
+        _compiletemplate.bases[base] = execspace['GenTemplate']
+
+    return execspace['GenTemplate']
+
+_compiletemplate = memoize(__compiletemplate)
+_compiletemplate.bases = {}
+
+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 websafe(val):
+    """
+    Converts `val` so that it's safe for use in HTML.
+
+    HTML metacharacters are encoded,
+    None becomes the empty string, and
+    unicode is converted to UTF-8.
+    """
+    if val is None: return ''
+    if not isinstance(val, unicode): val = str(val)
+    return htmlquote(val)
+
+if _hasTemplating:
+    class WebSafe(Filter):
+        def filter(self, val, **keywords): 
+            return websafe(val)
+
+def render(template, terms=None, asTemplate=False, base=None, 
+           isString=False):
+    """
+    Renders a template, caching where it can.
+    
+    `template` is the name of a file containing the a template in
+    the `templates/` folder, unless `isString`, in which case it's the 
+    template itself.
+
+    `terms` is a dictionary used to fill the template. If it's None, then
+    the caller's local variables are used instead, plus context, if it's not 
+    already set, is set to `context`.
+
+    If asTemplate is False, it `output`s the template directly. Otherwise,
+    it returns the template object.
+
+    If the template is a potential base template (that is, something other templates)
+    can extend, then base should be a string with the name of the template. The
+    template will be cached and made available for future calls to `render`.
+
+    Requires [Cheetah](http://cheetahtemplate.org/).
+    """
+    # terms=['var1', 'var2'] means grab those variables
+    if isinstance(terms, list):
+        new = {}
+        old = upvars()
+        for k in terms: 
+            new[k] = old[k]
+        terms = new
+    # default: grab all locals
+    elif terms is None:
+        terms = {'context': context, 'ctx':ctx}
+        terms.update(sys._getframe(1).f_locals)
+    # terms=d means use d as the searchList
+    if not isinstance(terms, tuple): 
+        terms = (terms,)
+    
+    if not isString and template.endswith('.html'): 
+        header('Content-Type','text/html; charset=utf-8', unique=True)
+        
+    compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
+    compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
+    if asTemplate: 
+        return compiled_tmpl
+    else: 
+        return output(str(compiled_tmpl))
+
+## Input Forms
+
+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): return dict([(k, fs[k]) for k in fs.keys()])
+    
+    _method = defaults.pop('_method', 'both')
+    
+    e = ctx.env.copy()
+    out = {}
+    if _method.lower() in ['both', 'post']:
+        a = {}
+        if e['REQUEST_METHOD'] == 'POST':
+            a = cgi.FieldStorage(fp = StringIO(data()), environ=e, 
+              keep_blank_values=1)
+            a = dictify(a)
+        out = dictadd(out, a)
+
+    if _method.lower() in ['both', 'get']:
+        e['REQUEST_METHOD'] = 'GET'
+        a = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
+        out = dictadd(out, a)
+    
+    try:
+        return storify(out, *requireds, **defaults)
+    except KeyError:
+        badrequest()
+        raise StopIteration
+
+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 changequery(**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.
+    """
+    query = input(_method='get')
+    for k, v in kw.iteritems():
+        if v is None:
+            query.pop(k, None)
+        else:
+            query[k] = v
+    out = ctx.path
+    if query:
+        out += '?' + urllib.urlencode(query)
+    return out
+
+## Cookies
+
+def setcookie(name, value, expires="", domain=None):
+    """Sets a cookie."""
+    if expires < 0: 
+        expires = -1000000000 
+    kargs = {'expires': expires, 'path':'/'}
+    if domain: 
+        kargs['domain'] = domain
+    # @@ should we limit cookies to a different path?
+    cookie = Cookie.SimpleCookie()
+    cookie[name] = 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:
+        return storify(cookie, *requireds, **defaults)
+    except KeyError:
+        badrequest()
+        raise StopIteration
+
+## WSGI Sugar
+
+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. If `unique` is None and a header with
+    that name already exists, it replaces it with this one.
+    """
+    if unique is True:
+        for h, v in ctx.headers:
+            if h == hdr: return
+    elif unique is None:
+        ctx.headers = [h for h in ctx.headers if h[0] != hdr]
+    
+    ctx.headers.append((hdr, value))
+
+def output(string_):
+    """Appends `string_` to the response."""
+    if isinstance(string_, unicode): string_ = string_.encode('utf8')
+    if ctx.get('flush'):
+        ctx._write(string_)
+    else:
+        ctx.output += str(string_)
+
+def flush():
+    ctx.flush = True
+    return flush
+
+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": 
+            ctx.status = value
+        else: 
+            header(hdr, value)
+
+    output(body)
+
+def webpyfunc(inp, fvars=None, autoreload=False):
+    """If `inp` is a url mapping, returns a function that calls handle."""
+    if not fvars: 
+        fvars = upvars()
+    if not hasattr(inp, '__call__'):
+        if autoreload:
+            # black magic to make autoreload work:
+            mod = \
+                __import__(
+                    fvars['__file__'].split(os.path.sep).pop().split('.')[0])
+            #@@probably should replace this with some inspect magic
+            name = dictfind(fvars, inp)
+            func = lambda: handle(getattr(mod, name), mod)
+        else:
+            func = lambda: handle(inp, fvars)
+    else:
+        func = inp
+    return func
+
+def wsgifunc(func, *middleware):
+    """Returns a WSGI-compatible function from a webpy-function."""
+    middleware = list(middleware)
+    if reloader in middleware:
+        relr = reloader(None)
+        relrcheck = relr.check
+        middleware.remove(reloader)
+    else:
+        relr = None
+        relrcheck = lambda: None
+    
+    def wsgifunc(env, start_resp):
+        _load(env)
+        relrcheck()
+        try:
+            result = func()
+        except StopIteration:
+            result = None
+        
+        is_generator = result and hasattr(result, 'next')
+        if is_generator:
+            # wsgi requires the headers first
+            # so we need to do an iteration
+            # and save the result for later
+            try:
+                firstchunk = result.next()
+            except StopIteration:
+                firstchunk = ''
+
+        status, headers, output = ctx.status, ctx.headers, ctx.output
+        ctx._write = start_resp(status, headers)
+
+        # and now, the fun:
+        
+        def cleanup():
+            # we insert this little generator
+            # at the end of our itertools.chain
+            # so that it unloads the request
+            # when everything else is done
+            
+            yield '' # force it to be a generator
+            _unload()
+
+        # result is the output of calling the webpy function
+        #   it could be a generator...
+        
+        if is_generator:
+            if firstchunk is flush:
+                # oh, it's just our special flush mode
+                # ctx._write is set up, so just continue execution
+                try:
+                    result.next()
+                except StopIteration:
+                    pass
+
+                _unload()
+                return []
+            else:
+                return itertools.chain([firstchunk], result, cleanup())
+        
+        #   ... but it's usually just None
+        # 
+        # output is the stuff in ctx.output
+        #   it's usually a string...
+        if isinstance(output, str): #@@ other stringlikes?
+            _unload()
+            return [output] 
+        #   it could be a generator...
+        elif hasattr(output, 'next'):
+            return itertools.chain(output, cleanup())
+        else:
+            _unload()
+            raise Exception, "Invalid web.ctx.output"
+    
+    for mw_func in middleware: 
+        wsgifunc = mw_func(wsgifunc)
+    
+    if relr:
+        relr.func = wsgifunc
+        return wsgifunc
+    return wsgifunc
+
+def run(inp, *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.
+
+    `input` is a callable, then it's called with no arguments.
+    Otherwise, it's a `mapping` object to be passed to `handle(...)`.
+
+    **Caveat:** So that `reloader` will work correctly, input has to be a variable,
+    it can't be a tuple passed in directly.
+
+    `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
+    function.
+    """
+    autoreload = reloader in middleware
+    fvars = upvars()
+    return runwsgi(wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware))
+
+def runwsgi(func):
+    """
+    Runs a WSGI-compatible function using FCGI, SCGI, or a simple web server,
+    as appropriate.
+    """
+    #@@ improve detection
+    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')
+      or 'fcgi' in sys.argv or 'fastcgi' in sys.argv):
+        return runfcgi(func)
+
+    if 'scgi' in sys.argv:
+        return runscgi(func)
+
+    # command line:
+    return runsimple(func, validip(listget(sys.argv, 1, '')))
+    
+def runsimple(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 >> debug, traceback.format_exc(),
+                internalerror()
+                if not self.wsgi_sent_headers:
+                    self.wsgi_start_response(ctx.status, ctx.headers)
+                self.wsgi_write_data(ctx.output)
+
+            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 "Launching server: http://%s:%d/" % server_address
+    WSGIServer(func, server_address).serve_forever()
+
+def makeserver(wsgi_server):
+    """Updates a flup-style WSGIServer with web.py-style error support."""
+    class MyServer(wsgi_server):
+        def error(self, req):
+            w = req.stdout.write
+            internalerror()
+            w('Status: ' + ctx.status + '\r\n')
+            for (h, v) in ctx.headers:
+                w(h + ': ' + v + '\r\n')
+            w('\r\n' + ctx.output)
+                
+    return MyServer
+    
+def runfcgi(func):
+    """Runs a WSGI-function with a FastCGI server."""
+    from flup.server.fcgi import WSGIServer
+    if len(sys.argv) > 2: # progname, scgi
+        args = sys.argv[:]
+        if 'fastcgi' in args: args.remove('fastcgi')
+        elif 'fcgi' in args: args.remove('fcgi')
+        hostport = validaddr(args[1])
+    elif len(sys.argv) > 1: 
+        hostport = ('localhost', 8000)
+    else:
+        hostport = None
+    return makeserver(WSGIServer)(func, multiplexed=True, bindAddress=hostport).run()
+
+def runscgi(func):
+    """Runs a WSGI-function with an SCGI server."""
+    from flup.server.scgi import WSGIServer
+    my_server = makeserver(WSGIServer)
+    if len(sys.argv) > 2: # progname, scgi
+        args = sys.argv[:]
+        args.remove('scgi')
+        hostport = validaddr(args[1])
+    else: 
+        hostport = ('localhost', 4000)
+    return my_server(func, bindAddress=hostport).run()
+
+## Debugging
+
+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):
+    """writes debug data to error stream"""
+    try: 
+        out = ctx.environ['wsgi.errors']
+    except: 
+        out = sys.stderr
+    out.write(x)
+debug.write = debugwrite
+
+class Reloader:
+    """
+    Before every request, checks to see if any loaded modules have changed on 
+    disk and, if so, reloads them.
+    """
+    def __init__(self, func):
+        self.func = func
+        self.mtimes = {}
+        global _compiletemplate
+        b = _compiletemplate.bases
+        _compiletemplate = globals()['__compiletemplate']
+        _compiletemplate.bases = b
+    
+    def check(self):
+        for mod in sys.modules.values():
+            try: 
+                mtime = os.stat(mod.__file__).st_mtime
+            except (AttributeError, OSError, IOError): 
+                continue
+            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)
+                except ImportError: 
+                    pass
+        return True
+    
+    def __call__(self, e, o): 
+        self.check()
+        return self.func(e, o)
+reloader = Reloader
+
+def profiler(app):
+    """Outputs basic profiling information at the bottom of each response."""
+    def profile_internal(e, o):
+        out, result = profile(app)(e, o)
+        return out + ['<pre>' + result + '</pre>'] #@@encode
+    return profile_internal
+
+## Context
+
+class _outputter:
+    """Wraps `sys.stdout` so that print statements go into the response."""
+    def write(self, string_): 
+        if hasattr(ctx, 'output'): 
+            return output(string_)
+        else: 
+            _oldstdout.write(string_)
+    def flush(self): 
+        return _oldstdout.flush()
+    def close(self): 
+        return _oldstdout.close()
+
+_context = {currentThread():Storage()}
+ctx = context = threadeddict(_context)
+
+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.
+
+`fullpath`
+   : The full path requested, including query arguments.
+
+### 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.
+"""
+
+if not '_oldstdout' in globals(): 
+    _oldstdout = sys.stdout
+    sys.stdout = _outputter()
+
+loadhooks = {}
+
+def load():
+    """
+    Loads a new context for the thread.
+    
+    You can ask for a function to be run at loadtime by 
+    adding it to the dictionary `loadhooks`.
+    """
+    _context[currentThread()] = Storage()
+    ctx.status = '200 OK'
+    ctx.headers = []
+    if 'db_parameters' in globals():
+        connect(**db_parameters)
+    
+    for x in loadhooks.values(): x()
+
+def _load(env):
+    load()
+    ctx.output = ''
+    ctx.environ = ctx.env = env
+    ctx.host = env.get('HTTP_HOST')
+    ctx.home = 'http://' + env.get('HTTP_HOST', '[unknown]') + \
+                os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
+    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], 
+                           os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
+
+    ctx.fullpath = ctx.path
+    if env.get('QUERY_STRING'):
+        ctx.fullpath += '?' + env.get('QUERY_STRING', '')
+
+unloadhooks = {}
+
+def unload():
+    """
+    Unloads the context for the thread.
+    
+    You can ask for a function to be run at loadtime by
+    adding it ot the dictionary `unloadhooks`.
+    """
+    for x in unloadhooks.values(): x()
+    # ensures db cursors and such are GCed promptly
+    del _context[currentThread()]
+
+def _unload():
+    unload()
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
+    
+    urls = ('/web.py', 'source')
+    class source:
+        def GET(self):
+            header('Content-Type', 'text/python')
+            print open(sys.argv[0]).read()
+    run(urls)
diff --git a/packages/python/python-webpy_0.138.bb b/packages/python/python-webpy_0.138.bb
new file mode 100644 (file)
index 0000000..557b9ee
--- /dev/null
@@ -0,0 +1,19 @@
+DESCRIPTION = "A Lightweight Web Application Framework"
+SECTION = "devel/python"
+PRIORITY = "optional"
+MAINTAINER = "Michael 'Mickey' Lauer <mickey@Vanille.de>"
+LICENSE = "PSF"
+RDEPENDS = "python-netserver"
+
+PR = "ml0"
+
+SRC_URI = "file://web.py"
+S = "${WORKDIR}"
+
+inherit distutils-base
+
+do_install() {
+       install -d ${D}${libdir}/${PYTHON_DIR}
+       install -m 0755 web.py ${D}${libdir}/${PYTHON_DIR}
+}
+