split pvl.web from pvl.verkko, rename test.py -> pvl.verkko-dhcp
authorTero Marttila <terom@paivola.fi>
Sun, 20 Jan 2013 18:26:12 +0200
changeset 151 8a9f01036091
parent 150 f12e83f8c34b
child 152 33b98b46d8fb
split pvl.web from pvl.verkko, rename test.py -> pvl.verkko-dhcp
bin/pvl.verkko-dhcp
pvl/html.py
pvl/verkko/hosts.py
pvl/verkko/urls.py
pvl/verkko/web.py
pvl/verkko/wsgi.py
pvl/web/__init__.py
pvl/web/application.py
pvl/web/html.py
pvl/web/urls.py
test.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/pvl.verkko-dhcp	Sun Jan 20 18:26:12 2013 +0200
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+
+from werkzeug.serving import run_simple
+
+from pvl import __version__
+import pvl.args
+import pvl.verkko.wsgi
+
+import optparse
+import logging; log = logging.getLogger('main')
+
+def parse_argv (argv, doc = __doc__) :
+    """
+        Parse command-line argv, returning (options, args).
+    """
+
+    prog = argv.pop(0)
+    args = argv
+
+    # optparse
+    parser = optparse.OptionParser(
+        prog        = prog,
+        usage       = '%prog: [options] [<user> [...]]',
+        version     = __version__,
+        description = doc,
+    )
+
+    # common
+    parser.add_option_group(pvl.args.parser(parser))
+
+    parser.add_option('-d', '--database-read', metavar='URI', default='sqlite:///var/verkko.db',
+        help="Database to use (readonly)")
+
+    # parse
+    options, args = parser.parse_args(args)
+
+    # apply
+    pvl.args.apply(options)
+
+    return options, args
+
+def main (argv) :
+    """
+        pvl.verkko wsgi development server.
+    """
+
+    # parse cmdline
+    options, args = parse_argv(argv, doc=__doc__)
+
+    # app
+    application = pvl.verkko.wsgi.Application(options.database_read)
+
+    # wsgi wrapper
+    run_simple('0.0.0.0', 8080, application,
+            #use_reloader    = True, 
+            use_debugger    = (options.loglevel == logging.DEBUG),
+            static_files    = {
+                '/static':      'static',
+            },
+    )
+
+if __name__ == '__main__' :
+    import sys
+
+    sys.exit(main(sys.argv))
+
+
--- a/pvl/html.py	Sun Jan 20 18:19:03 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,607 +0,0 @@
-"""
-    Generate XHTML output from python code.
-
-    >>> from html import tags
-    >>> unicode(tags.a(href="http://www.google.com")("Google <this>!"))
-    u'<a href="http://www.google.com">\\n\\tGoogle &lt;this&gt;!\\n</a>'
-"""
-
-import itertools as itertools
-import types as types
-from xml.sax import saxutils
-
-class Renderable (object) :
-    """
-        Structured data that's flattened into indented lines of text.
-    """
-
-    def flatten (self) :
-        """
-            Flatten this object into a series of (identlevel, line) tuples.
-        """
-        
-        raise NotImplementedError()
-
-    def iter (self, indent='\t') :
-        """
-            Yield a series of lines for this render.
-        """
-        
-        for indent_level, line in self.flatten() :
-            yield (indent * indent_level) + line
-
-    def unicode (self, newline=u'\n', **opts) :
-        """
-            Render as a single unicode string.
-
-            No newline is returned at the end of the string.
-
-            >>> Tag.build('a', 'b').unicode(newline='X', indent='Y')
-            u'<a>XYbX</a>'
-        """
-
-        return newline.join(self.iter(**opts))
-    
-    # required for print
-    def str (self, newline='\n', encoding='ascii', **opts) :
-        """
-            Render as a single string.
-        """
-        
-        # XXX: try and render as non-unicode, i.e. binary data in the tree?
-        return newline.join(line.encode(encoding) for line in self.iter(**opts))
-    
-    # formal interface using defaults
-    __iter__ = iter
-    __unicode__ = unicode
-    __str__ = str
-
-class Text (Renderable) :
-    """
-        Plain un-structured/un-processed HTML text for output
-        
-        >>> Text('foo')
-        Text(u'foo')
-        >>> list(Text('<foo>'))
-        [u'<foo>']
-        >>> list(tag('a', Text('<foo>')))
-        [u'<a>', u'\\t<foo>', u'</a>']
-    """
-
-    def __init__ (self, text) :
-        self.text = unicode(text)
-    
-    def flatten (self) :
-        yield (0, self.text)
-
-    def __repr__ (self) :
-        return "Text(%r)" % (self.text, )
-
-class Tag (Renderable) :
-    """
-        An immutable HTML tag structure, with the tag's name, attributes and contents.
-    """
-    
-    # types of nested items to flatten
-    CONTAINER_TYPES = (types.TupleType, types.ListType, types.GeneratorType)
-
-    # types of items to keep as-is
-    ITEM_TYPES = (Renderable, )
-
-    @classmethod
-    def process_contents (cls, *args) :
-        """
-            Yield the HTML tag's contents from the given sequence of positional arguments as a series of flattened
-            items, eagerly converting them to unicode.
-
-            If no arguments are given, we don't have any children:
-            
-            >>> bool(list(Tag.process_contents()))
-            False
-            
-            Items that are None will be ignored:
-
-            >>> list(Tag.process_contents(None))
-            []
-
-            Various Python container types are recursively flattened:
-
-            >>> list(Tag.process_contents([1, 2]))
-            [u'1', u'2']
-            >>> list(Tag.process_contents([1], [2]))
-            [u'1', u'2']
-            >>> list(Tag.process_contents([1, [2]]))
-            [u'1', u'2']
-            >>> list(Tag.process_contents(n + 1 for n in xrange(2)))
-            [u'1', u'2']
-            >>> list(Tag.process_contents((1, 2)))
-            [u'1', u'2']
-            >>> list(Tag.process_contents((1), (2, )))
-            [u'1', u'2']
-
-            Our own HTML-aware objects are returned as-is:
-            
-            >>> list(Tag.process_contents(Tag.build('foo')))
-            [tag('foo')]
-            >>> list(Tag.process_contents(Text('bar')))
-            [Text(u'bar')]
-            
-            All other objects are converted to unicode:
-            
-            >>> list(Tag.process_contents('foo', u'bar', 0.123, False))
-            [u'foo', u'bar', u'0.123', u'False']
-
-        """
-
-        for arg in args :
-            if arg is None :
-                # skip null: None
-                continue
-            
-            elif isinstance(arg, cls.CONTAINER_TYPES) :
-                # flatten nested container: tuple/list/generator
-                for node in arg :
-                    # recurse
-                    for item in cls.process_contents(node) :
-                        yield item
-
-            elif isinstance(arg, cls.ITEM_TYPES) :
-                # yield item: Renderable
-                yield arg
-
-            else :
-                # as unicode
-                yield unicode(arg)
-
-    @classmethod
-    def process_attrs (cls, **kwargs) :
-        """
-            Yield the HTML tag attributes from the given set of keyword arguments as a series of (name, value) tuples.
-            
-            Keyword-only options (`_key=value`) are filtered out:
-                
-            >>> dict(Tag.process_attrs(_opt=True))
-            {}
-
-            Attributes with a value of None/False are filtered out:
-
-            >>> dict(Tag.process_attrs(foo=None, bar=False))
-            {}
-            
-            A value given as True is returned as the key's value:
-
-            >>> dict(Tag.process_attrs(quux=True))
-            {'quux': u'quux'}
-
-            A (single) trailing underscore in the attribute name is removed:
-
-            >>> dict(Tag.process_attrs(class_='foo'))
-            {'class': u'foo'}
-            >>> dict(Tag.process_attrs(data__='foo'))
-            {'data_': u'foo'}
-        """
-
-        for key, value in kwargs.iteritems() :
-            # keyword arguments are always pure strings
-            assert type(key) is str
-
-            if value is None or value is False:
-                # omit
-                continue
-            
-            if key.startswith('_') :
-                # option
-                continue
-
-            if key.endswith('_') :
-                # strip underscore
-                key = key[:-1]
-            
-            if value is True :
-                # flag attr
-                value = key
-            
-            yield key, unicode(value)
-        
-    @classmethod
-    def process_opts (cls, **kwargs) :
-        """
-            Return a series of of the keyword-only _options, extracted from the given dict of keyword arguments, as 
-            (k, v) tuples.
-
-            >>> Tag.process_opts(foo='bar', _bar=False)
-            (('bar', False),)
-        """
-        
-        return tuple((k.lstrip('_'), v) for k, v in kwargs.iteritems() if k.startswith('_'))
-    
-    @classmethod
-    def build (cls, _name, *args, **kwargs) :
-        """
-            Factory function for constructing Tags by directly passing in contents/attributes/options as Python function
-            arguments/keyword arguments.
-
-            The first positional argument is the tag's name:
-            
-            >>> Tag.build('foo')
-            tag('foo')
-            
-            Further positional arguments are the tag's contents:
-
-            >>> Tag.build('foo', 'quux', 'bar')
-            tag('foo', u'quux', u'bar')
-
-            All the rules used by process_contents() are available:
-            
-            >>> Tag.build('foo', [1, None], None, (n for n in xrange(2)))
-            tag('foo', u'1', u'0', u'1')
-
-            The special-case for a genexp as the only argument works:
-            
-            >>> f = lambda *args: Tag.build('foo', *args)
-            >>> f('hi' for n in xrange(2))
-            tag('foo', u'hi', u'hi')
-            
-            Attributes are passed as keyword arguments, with or without contents:
-
-            >>> Tag.build('foo', id=1)
-            tag('foo', id=u'1')
-            >>> Tag.build('foo', 'quux', bar=5)
-            tag('foo', u'quux', bar=u'5')
-            >>> Tag.build('foo', class_='ten')
-            tag('foo', class=u'ten')
-            
-            The attribute names don't conflict with positional argument names:
-
-            >>> Tag.build('bar', name='foo')
-            tag('bar', name=u'foo')
-
-            Options are handled as the 'real' keyword arguments:
-
-            >>> print Tag.build('foo', _selfclosing=False)
-            <foo></foo>
-            >>> print Tag.build('foo', _foo='bar')
-            Traceback (most recent call last):
-                ...
-            TypeError: __init__() got an unexpected keyword argument 'foo'
-        """
-
-        # pre-process incoming user values
-        contents = list(cls.process_contents(*args))
-        attrs = dict(cls.process_attrs(**kwargs))
-
-        # XXX: use Python 2.6 keyword-only arguments instead?
-        options = dict(cls.process_opts(**kwargs))
-
-        return cls(_name, contents, attrs, **options)
-
-    def __init__ (self, name, contents, attrs, selfclosing=None, whitespace_sensitive=None) :
-        """
-            Initialize internal Tag state with the given tag identifier, flattened list of content items, dict of
-            attributes and dict of options.
-
-                selfclosing             - set to False to render empty tags as <foo></foo> instead of <foo /> 
-                                          (for XHTML -> HTML compatibility)
-
-                whitespace_sensitive    - do not indent tag content onto separate rows, render the full tag as a single
-                                          row
-
-            Use the build() factory function to build Tag objects using Python's function call argument semantics.
-            
-            The tag name is used a pure string identifier:
-
-            >>> Tag(u'foo', [], {})
-            tag('foo')
-            >>> Tag(u'\\xE4', [], {})
-            Traceback (most recent call last):
-                ...
-            UnicodeEncodeError: 'ascii' codec can't encode character u'\\xe4' in position 0: ordinal not in range(128)
-
-            Contents have their order preserved:
-
-            >>> Tag('foo', [1, 2], {})
-            tag('foo', 1, 2)
-            >>> Tag('foo', [2, 1], {})
-            tag('foo', 2, 1)
-
-            Attributes can be given:
-            
-            >>> Tag('foo', [], dict(foo='bar'))
-            tag('foo', foo='bar')
-
-            Options can be given:
-
-            >>> print Tag('foo', [], {}, selfclosing=False)
-            <foo></foo>
-        """
-        
-        self.name = str(name)
-        self.contents = contents
-        self.attrs = attrs
-
-        # options
-        self.selfclosing = selfclosing
-        self.whitespace_sensitive = whitespace_sensitive
-
-    def __call__ (self, *args, **kwargs) :
-        """
-            Return a new Tag as a copy of this tag, but with the given additional attributes/contents.
-
-            The same rules for function positional/keyword arguments apply as for build()
-
-            >>> Tag.build('foo')('bar')
-            tag('foo', u'bar')
-            >>> Tag.build('a', href='index.html')("Home")
-            tag('a', u'Home', href=u'index.html')
-
-            New contents and attributes can be given freely, using the same rules as for Tag.build:
-
-            >>> Tag.build('bar', None)(5, foo=None, class_='bar')
-            tag('bar', u'5', class=u'bar')
-
-            Tag contents accumulate in order:
-
-            >>> Tag.build('a')('b', ['c'])('d')
-            tag('a', u'b', u'c', u'd')
-
-            Each Tag is immutable, so the called Tag isn't changed, but rather a copy is returned:
-
-            >>> t1 = Tag.build('a'); t2 = t1('b'); t1
-            tag('a')
-
-            Attribute values are replaced:
-
-            >>> Tag.build('foo', a=2)(a=3)
-            tag('foo', a=u'3')
-
-            Options are also supported:
-
-            >>> list(Tag.build('foo')(bar='quux', _selfclosing=False))
-            [u'<foo bar="quux"></foo>']
-        """
-
-        # accumulate contents
-        contents = self.contents + list(self.process_contents(*args))
-
-        # merge attrs
-        attrs = dict(self.attrs)
-        attrs.update(self.process_attrs(**kwargs))
-
-        # options
-        opts = dict(
-            selfclosing = self.selfclosing,
-            whitespace_sensitive = self.whitespace_sensitive,
-        )
-        opts.update(self.process_opts(**kwargs))
-
-        # build updated tag
-        return Tag(self.name, contents, attrs, **opts)
-
-    def render_attrs (self) :
-        """
-            Return the HTML attributes string
-
-            >>> Tag.build('x', foo=5, bar='<', quux=None).render_attrs()
-            u'foo="5" bar="&lt;"'
-            >>> Tag.build('x', foo='a"b').render_attrs()
-            u'foo=\\'a"b\\''
-        """
-
-        return " ".join(
-            (
-                u'%s=%s' % (name, saxutils.quoteattr(value))
-            ) for name, value in self.attrs.iteritems()
-        )
-
-    def flatten_items (self, indent=1) :
-        """
-            Flatten our content into a series of indented lines.
-
-            >>> list(Tag.build('tag', 5).flatten_items())
-            [(1, u'5')]
-            >>> list(Tag.build('tag', 'line1', 'line2').flatten_items())
-            [(1, u'line1'), (1, u'line2')]
-
-            Nested :
-            >>> list(Tag.build('tag', 'a', Tag.build('b', 'bb'), 'c').flatten_items())
-            [(1, u'a'), (1, u'<b>'), (2, u'bb'), (1, u'</b>'), (1, u'c')]
-            >>> list(Tag.build('tag', Tag.build('hr'), Tag.build('foo')('bar')).flatten_items())
-            [(1, u'<hr />'), (1, u'<foo>'), (2, u'bar'), (1, u'</foo>')]
-        """
-
-        for item in self.contents :
-            if isinstance(item, Renderable) :
-                # recursively flatten items
-                for line_indent, line in item.flatten() :
-                    # indented
-                    yield indent + line_indent, line
-
-            else :
-                # render HTML-escaped raw value
-                # escape raw values
-                yield indent, saxutils.escape(item)
-   
-    def flatten (self) :
-        """
-            Render the tag and all content as a flattened series of indented lines.
-            
-            Empty tags collapse per default:
-
-            >>> list(Tag.build('foo').flatten())
-            [(0, u'<foo />')]
-            >>> list(Tag.build('bar', id=5).flatten())
-            [(0, u'<bar id="5" />')]
-
-            Values are indented inside the start tag:
-
-            >>> list(Tag.build('foo', 'bar', a=5).flatten())
-            [(0, u'<foo a="5">'), (1, u'bar'), (0, u'</foo>')]
-            
-            Nested tags are further indented:
-
-            >>> list(Tag.build('1', '1.1', Tag.build('1.2', '1.2.1'), '1.3', a=5).flatten())
-            [(0, u'<1 a="5">'), (1, u'1.1'), (1, u'<1.2>'), (2, u'1.2.1'), (1, u'</1.2>'), (1, u'1.3'), (0, u'</1>')]
-
-            Empty tags are rendered with a separate closing tag on the same line, if desired:
-
-            >>> list(Tag.build('foo', _selfclosing=False).flatten())
-            [(0, u'<foo></foo>')]
-            >>> list(Tag.build('foo', src='asdf', _selfclosing=False).flatten())
-            [(0, u'<foo src="asdf"></foo>')]
-
-            Tags that are declared as whitespace-sensitive are collapsed onto the same line:
-
-            >>> list(Tag.build('foo', _whitespace_sensitive=True).flatten())
-            [(0, u'<foo />')]
-            >>> list(Tag.build('foo', _whitespace_sensitive=True, _selfclosing=False).flatten())
-            [(0, u'<foo></foo>')]
-            >>> list(Tag.build('foo', 'bar', _whitespace_sensitive=True).flatten())
-            [(0, u'<foo>bar</foo>')]
-            >>> list(Tag.build('foo', 'bar\\nasdf\\tx', _whitespace_sensitive=True).flatten())
-            [(0, u'<foo>bar\\nasdf\\tx</foo>')]
-            >>> list(Tag.build('foo', 'bar', Tag.build('quux', 'asdf'), 'asdf', _whitespace_sensitive=True).flatten())
-            [(0, u'<foo>bar<quux>asdf</quux>asdf</foo>')]
-
-            Embedded HTML given as string values is escaped:
-
-            >>> list(Tag.build('foo', '<asdf>'))
-            [u'<foo>', u'\\t&lt;asdf&gt;', u'</foo>']
-
-            Embedded quotes in attribute values are esacaped:
-
-            >>> list(Tag.build('foo', style='ok;" onload="...'))
-            [u'<foo style=\\'ok;" onload="...\\' />']
-            >>> list(Tag.build('foo', style='ok;\\'" onload=..."\\''))
-            [u'<foo style="ok;\\'&quot; onload=...&quot;\\'" />']
-        """
-
-        # optional attr spec
-        if self.attrs :
-            attrs = " " + self.render_attrs()
-
-        else :
-            attrs = ""
-
-        if not self.contents and self.selfclosing is False :
-            # empty tag, but don't use the self-closing syntax..
-            yield 0, u"<%s%s></%s>" % (self.name, attrs, self.name)
-
-        elif not self.contents  :
-            # self-closing xml tag
-            # do note that this is invalid HTML, and the space before the / is relevant for parsing it as HTML
-            yield 0, u"<%s%s />" % (self.name, attrs)
-
-        elif self.whitespace_sensitive :
-            # join together each line for each child, discarding the indent
-            content = u''.join(line for indent, line in self.flatten_items())
-
-            # render full tag on a single line
-            yield 0, u"<%s%s>%s</%s>" % (self.name, attrs, content, self.name)
-
-        else :
-            # start tag
-            yield 0, u"<%s%s>" % (self.name, attrs)
-
-            # contents, indented one level below the start tag
-            for indent, line in self.flatten_items(indent=1) :
-                yield indent, line
-
-            # close tag
-            yield 0, u"</%s>" % (self.name, )
-
-    def __repr__ (self) :
-        return 'tag(%s)' % ', '.join(
-            [
-                repr(self.name)
-            ] + [
-                repr(c) for c in self.contents
-            ] + [
-                '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
-            ]
-        )
-
-# factory function for Tag
-tag = Tag.build
-
-
-class Document (Renderable) :
-    """
-        A full XHTML 1.0 document with optional XML header, doctype, html[@xmlns].
-
-        >>> list(Document(tags.html('...')))
-        [u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', u'<html xmlns="http://www.w3.org/1999/xhtml">', u'\\t...', u'</html>']
-    """
-
-    def __init__ (self, root,
-        doctype='html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"',
-        html_xmlns='http://www.w3.org/1999/xhtml',
-        xml_version=None, xml_encoding=None, 
-    ) :
-        # add xmlns attr to root node
-        self.root = root(xmlns=html_xmlns)
-
-        # store
-        self.doctype = doctype
-        self.xml_declaration = {}
-
-        if xml_version :
-            self.xml_declaration['version'] = xml_version
-
-        if xml_encoding :
-            self.xml_declaration['encoding'] = xml_encoding
-
-    def flatten (self) :
-        """
-            Return the header lines along with the normally formatted <html> tag
-        """
-        
-        if self.xml_declaration :
-            yield 0, u'<?xml %s ?>' % (' '.join('%s="%s"' % kv for kv in self.xml_declaration.iteritems()))
-
-        if self.doctype :
-            yield 0, u'<!DOCTYPE %s>' % (self.doctype)
-
-        # <html>
-        for indent, line in self.root.flatten() :
-            yield indent, line
-
-class TagFactory (object) :
-    """
-        Build Tags with names give as attribute names
-        
-        >>> list(TagFactory().a(href='#')('Yay'))
-        [u'<a href="#">', u'\\tYay', u'</a>']
-
-        >>> list(TagFactory()("><"))
-        [u'><']
-    """
-
-    # full XHTML document
-    document = Document
-    
-    def __getattr__ (self, name) :
-        """
-            Get a Tag object with the given name, but no contents
-
-            >>> TagFactory().a
-            tag('a')
-        """
-
-        return Tag(name, [], {})
-
-    def __call__ (self, value) :
-        """
-            Raw HTML.
-        """
-
-        return Text(value)
-
-# static instance
-tags = TagFactory()
-
-# testing
-if __name__ == '__main__' :
-    import doctest
-
-    doctest.testmod()
-
--- a/pvl/verkko/hosts.py	Sun Jan 20 18:19:03 2013 +0200
+++ b/pvl/verkko/hosts.py	Sun Jan 20 18:26:12 2013 +0200
@@ -1,7 +1,7 @@
 from pvl.verkko import db, web
 from pvl.verkko.utils import parse_timedelta, IPv4Network
 
-from pvl.html import tags as html
+from pvl.web import html
 
 import re
 import datetime
--- a/pvl/verkko/urls.py	Sun Jan 20 18:19:03 2013 +0200
+++ b/pvl/verkko/urls.py	Sun Jan 20 18:26:12 2013 +0200
@@ -8,6 +8,7 @@
 
 # index page here :)
 from pvl.verkko import web
+
 class Index (web.Handler) :
     def render (self) :
         html = web.html
--- a/pvl/verkko/web.py	Sun Jan 20 18:19:03 2013 +0200
+++ b/pvl/verkko/web.py	Sun Jan 20 18:26:12 2013 +0200
@@ -1,14 +1,9 @@
 # encoding: utf-8
-
-def merge (*dicts) :
-    dict = {}
+import pvl.web.application
 
-    for d in dicts :
-        dict.update(d)
+# view
+from pvl.web.html import tags as html
 
-    return dict
-
-# response types
 from werkzeug.wrappers import Response
 from werkzeug.exceptions import (
         HTTPException, 
@@ -17,15 +12,7 @@
 )
 from werkzeug.utils import redirect
 
-# view
-from pvl.html import tags as html
-
-class Handler (object) :
-    """
-        Per-Request controller/view, containing the request context and generating the response.
-    """
-
-    TITLE = None
+class Handler (pvl.web.application.Handler) :
     CSS = (
         #"/static/layout.css", 
         "/static/style.css", 
@@ -35,32 +22,11 @@
     )
 
     def __init__ (self, app, request, urls, params) :
-        """
-            app     - wsgi.Application
-            request - werkzeug.Request
-            urls    - werkzeug.routing.MapAdapter
-        """
+        super(Handler, self).__init__(app, request, urls, params)
 
-        self.app = app
-        self.request = request
-        self.urlmap = urls
-        self.params = params
-        
         # new ORM session per request
         self.db = app.db.session() 
 
-    def url (self, handler=None, **params) :
-        """
-            Return an URL for given endpoint, with parameters,
-        """
-
-        if not handler :
-            # XXX: just generate a plain-relative '?foo=...' url instead?
-            handler = self.__class__
-            params = merge(self.params, params)
-
-        return self.urlmap.build(handler, params)
-        
     def title (self) :
         """
             Render site/page title as text.
@@ -71,65 +37,9 @@
         else :
             return u"Päivölä Verkko"
 
-    def render (self) :
-        """
-            Render page content (as <body>...</body>).
-        """
-
-        raise NotImplementedError()
-
-    def render_html (self) :
-        """
-            Render page layout (as <html>).
-        """
-
-        title = self.title()
-
-        return html.html(
-            html.head(
-                html.title(title),
-                (
-                    html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS
-                ), 
-                (
-                    html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS
-                ),
-            ),
-            html.body(
-                html.h1(title),
-                self.render() 
-            )
-        )
-
-    def process (self, **params) :
-        """
-            Process request args to build internal request state.
-        """
-
-        pass
-
-    def respond (self) :
-        """
-            Generate a response, or raise an HTTPException
-
-            Does an HTML layout'd response per default.
-        """
-        
-        # returning e.g. redirect?
-        response = self.process(**self.params)
-
-        if response :
-            return response
-        
-        # render as html per default
-        render = self.render_html()
-        text = unicode(html.document(render))
-
-        return Response(text, mimetype='text/html')
-    
     def cleanup (self) :
         """
-            After request processing. Do not fail :)
+            After request processing.
         """
         
         # XXX: SQLAlchemy doesn't automatically close these...?
--- a/pvl/verkko/wsgi.py	Sun Jan 20 18:19:03 2013 +0200
+++ b/pvl/verkko/wsgi.py	Sun Jan 20 18:26:12 2013 +0200
@@ -1,51 +1,18 @@
-import werkzeug
-from werkzeug.wrappers import Request, Response
-from werkzeug.exceptions import HTTPException
+import pvl.web
 
 import logging; log = logging.getLogger('pvl.verkko.wsgi')
 
-from pvl.verkko import db as database, urls, web
+from pvl.verkko import urls, db as database
 
-class Application (object) :
-    urls = urls.urls
+class Application (pvl.web.Application) :
+    URLS  = urls.urls
 
     def __init__ (self, db) :
         """
             Initialize app with db.
         """
 
+        super(Application, self).__init__()
+
         self.db = database.Database(db)
 
-    def respond (self, request) :
-        """
-            Lookup Request -> web.Handler, params
-        """
-        
-        # bind to request
-        urls = self.urls.bind_to_environ(request)
-        
-        # lookup
-        handler, params = urls.match()
-
-        # handler instance
-        handler = handler(self, request, urls, params)
-
-        try :
-            # apply
-            return handler.respond()
-
-        finally :
-            handler.cleanup()
-
-    @Request.application
-    def __call__ (self, request) :
-        """
-            WSGI entry point, werkzeug Request -> Response
-        """
-
-        try :
-            return self.respond(request)
-        
-        except HTTPException as ex :
-            return ex
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/web/__init__.py	Sun Jan 20 18:26:12 2013 +0200
@@ -0,0 +1,15 @@
+"""
+    Werkzeug-based web application.
+"""
+
+from pvl.web.html import tags as html
+from pvl.web.application import Application, Handler
+
+# werkzeug
+from werkzeug.exceptions import (
+        HTTPException, 
+        BadRequest,         # 400
+        NotFound,           # 404
+)
+from werkzeug.utils import redirect
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/web/application.py	Sun Jan 20 18:26:12 2013 +0200
@@ -0,0 +1,172 @@
+"""
+    WSGI Application.
+"""
+
+import werkzeug
+from werkzeug.wrappers import Request, Response
+from werkzeug.exceptions import HTTPException
+
+import logging; log = logging.getLogger('pvl.web.application')
+
+class Application (object) :
+    """
+        Main state.
+    """
+
+    URLS = None
+
+    def __init__ (self, urls=None) :
+        """
+                urls        - werkzeug.routing.Map -> Handler
+        """
+
+        if not urls :
+            urls = self.URLS
+
+        if not urls :
+            raise ValueError("No URLS/urls=... given")
+
+        self.urls = urls
+
+    def respond (self, request) :
+        """
+            Lookup Request -> web.Handler, params
+        """
+        
+        # bind to request
+        urls = self.urls.bind_to_environ(request)
+        
+        # lookup
+        handler, params = urls.match()
+
+        # handler instance
+        handler = handler(self, request, urls, params)
+
+        try :
+            # apply
+            return handler.respond()
+
+        finally :
+            handler.cleanup()
+
+    @Request.application
+    def __call__ (self, request) :
+        """
+            WSGI entry point, werkzeug Request -> Response
+        """
+
+        try :
+            return self.respond(request)
+        
+        except HTTPException as ex :
+            return ex
+
+from pvl.invoke import merge # XXX
+from pvl.web.html import tags as html
+
+class Handler (object) :
+    """
+        Per-Request controller/view, containing the request context and generating the response.
+    """
+
+    TITLE = None
+    CSS = (
+        #"/static/...css", 
+    )
+    JS = (
+        #"/static/....js"
+    )
+
+    def __init__ (self, app, request, urls, params) :
+        """
+            app     - wsgi.Application
+            request - werkzeug.Request
+            urls    - werkzeug.routing.Map.bind_to_environ()
+        """
+
+        self.app = app
+        self.request = request
+        self.urls = urls
+        self.params = params
+        
+    def url (self, handler=None, **params) :
+        """
+            Return an URL for given endpoint, with parameters,
+        """
+
+        if not handler :
+            # XXX: just generate a plain-relative '?foo=...' url instead?
+            handler = self.__class__
+            params = merge(self.params, params)
+
+        return self.urls.build(handler, params)
+
+    def title (self) :
+        """
+            Render site/page title as text.
+        """
+
+        return self.TITLE
+ 
+    def render (self) :
+        """
+            Render page content (as <body>...</body>).
+        """
+
+        raise NotImplementedError()
+
+    def render_html (self) :
+        """
+            Render page layout (as <html>).
+        """
+
+        title = self.title()
+
+        return html.html(
+            html.head(
+                html.title(title),
+                (
+                    html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS
+                ), 
+                (
+                    html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS
+                ),
+            ),
+            html.body(
+                html.h1(title),
+                self.render() 
+            )
+        )
+
+    def process (self, **params) :
+        """
+            Process request args to build internal request state.
+        """
+
+        pass
+
+    def respond (self) :
+        """
+            Generate a response, or raise an HTTPException
+
+            Does an HTML layout'd response per default.
+        """
+        
+        # returning e.g. redirect?
+        response = self.process(**self.params)
+
+        if response :
+            return response
+        
+        # render as html per default
+        render = self.render_html()
+        text = unicode(html.document(render))
+
+        return Response(text, mimetype='text/html')
+    
+    def cleanup (self) :
+        """
+            After request processing. Do not fail :)
+        """
+        
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/web/html.py	Sun Jan 20 18:26:12 2013 +0200
@@ -0,0 +1,607 @@
+"""
+    Generate XHTML output from python code.
+
+    >>> from html import tags
+    >>> unicode(tags.a(href="http://www.google.com")("Google <this>!"))
+    u'<a href="http://www.google.com">\\n\\tGoogle &lt;this&gt;!\\n</a>'
+"""
+
+import itertools as itertools
+import types as types
+from xml.sax import saxutils
+
+class Renderable (object) :
+    """
+        Structured data that's flattened into indented lines of text.
+    """
+
+    def flatten (self) :
+        """
+            Flatten this object into a series of (identlevel, line) tuples.
+        """
+        
+        raise NotImplementedError()
+
+    def iter (self, indent='\t') :
+        """
+            Yield a series of lines for this render.
+        """
+        
+        for indent_level, line in self.flatten() :
+            yield (indent * indent_level) + line
+
+    def unicode (self, newline=u'\n', **opts) :
+        """
+            Render as a single unicode string.
+
+            No newline is returned at the end of the string.
+
+            >>> Tag.build('a', 'b').unicode(newline='X', indent='Y')
+            u'<a>XYbX</a>'
+        """
+
+        return newline.join(self.iter(**opts))
+    
+    # required for print
+    def str (self, newline='\n', encoding='ascii', **opts) :
+        """
+            Render as a single string.
+        """
+        
+        # XXX: try and render as non-unicode, i.e. binary data in the tree?
+        return newline.join(line.encode(encoding) for line in self.iter(**opts))
+    
+    # formal interface using defaults
+    __iter__ = iter
+    __unicode__ = unicode
+    __str__ = str
+
+class Text (Renderable) :
+    """
+        Plain un-structured/un-processed HTML text for output
+        
+        >>> Text('foo')
+        Text(u'foo')
+        >>> list(Text('<foo>'))
+        [u'<foo>']
+        >>> list(tag('a', Text('<foo>')))
+        [u'<a>', u'\\t<foo>', u'</a>']
+    """
+
+    def __init__ (self, text) :
+        self.text = unicode(text)
+    
+    def flatten (self) :
+        yield (0, self.text)
+
+    def __repr__ (self) :
+        return "Text(%r)" % (self.text, )
+
+class Tag (Renderable) :
+    """
+        An immutable HTML tag structure, with the tag's name, attributes and contents.
+    """
+    
+    # types of nested items to flatten
+    CONTAINER_TYPES = (types.TupleType, types.ListType, types.GeneratorType)
+
+    # types of items to keep as-is
+    ITEM_TYPES = (Renderable, )
+
+    @classmethod
+    def process_contents (cls, *args) :
+        """
+            Yield the HTML tag's contents from the given sequence of positional arguments as a series of flattened
+            items, eagerly converting them to unicode.
+
+            If no arguments are given, we don't have any children:
+            
+            >>> bool(list(Tag.process_contents()))
+            False
+            
+            Items that are None will be ignored:
+
+            >>> list(Tag.process_contents(None))
+            []
+
+            Various Python container types are recursively flattened:
+
+            >>> list(Tag.process_contents([1, 2]))
+            [u'1', u'2']
+            >>> list(Tag.process_contents([1], [2]))
+            [u'1', u'2']
+            >>> list(Tag.process_contents([1, [2]]))
+            [u'1', u'2']
+            >>> list(Tag.process_contents(n + 1 for n in xrange(2)))
+            [u'1', u'2']
+            >>> list(Tag.process_contents((1, 2)))
+            [u'1', u'2']
+            >>> list(Tag.process_contents((1), (2, )))
+            [u'1', u'2']
+
+            Our own HTML-aware objects are returned as-is:
+            
+            >>> list(Tag.process_contents(Tag.build('foo')))
+            [tag('foo')]
+            >>> list(Tag.process_contents(Text('bar')))
+            [Text(u'bar')]
+            
+            All other objects are converted to unicode:
+            
+            >>> list(Tag.process_contents('foo', u'bar', 0.123, False))
+            [u'foo', u'bar', u'0.123', u'False']
+
+        """
+
+        for arg in args :
+            if arg is None :
+                # skip null: None
+                continue
+            
+            elif isinstance(arg, cls.CONTAINER_TYPES) :
+                # flatten nested container: tuple/list/generator
+                for node in arg :
+                    # recurse
+                    for item in cls.process_contents(node) :
+                        yield item
+
+            elif isinstance(arg, cls.ITEM_TYPES) :
+                # yield item: Renderable
+                yield arg
+
+            else :
+                # as unicode
+                yield unicode(arg)
+
+    @classmethod
+    def process_attrs (cls, **kwargs) :
+        """
+            Yield the HTML tag attributes from the given set of keyword arguments as a series of (name, value) tuples.
+            
+            Keyword-only options (`_key=value`) are filtered out:
+                
+            >>> dict(Tag.process_attrs(_opt=True))
+            {}
+
+            Attributes with a value of None/False are filtered out:
+
+            >>> dict(Tag.process_attrs(foo=None, bar=False))
+            {}
+            
+            A value given as True is returned as the key's value:
+
+            >>> dict(Tag.process_attrs(quux=True))
+            {'quux': u'quux'}
+
+            A (single) trailing underscore in the attribute name is removed:
+
+            >>> dict(Tag.process_attrs(class_='foo'))
+            {'class': u'foo'}
+            >>> dict(Tag.process_attrs(data__='foo'))
+            {'data_': u'foo'}
+        """
+
+        for key, value in kwargs.iteritems() :
+            # keyword arguments are always pure strings
+            assert type(key) is str
+
+            if value is None or value is False:
+                # omit
+                continue
+            
+            if key.startswith('_') :
+                # option
+                continue
+
+            if key.endswith('_') :
+                # strip underscore
+                key = key[:-1]
+            
+            if value is True :
+                # flag attr
+                value = key
+            
+            yield key, unicode(value)
+        
+    @classmethod
+    def process_opts (cls, **kwargs) :
+        """
+            Return a series of of the keyword-only _options, extracted from the given dict of keyword arguments, as 
+            (k, v) tuples.
+
+            >>> Tag.process_opts(foo='bar', _bar=False)
+            (('bar', False),)
+        """
+        
+        return tuple((k.lstrip('_'), v) for k, v in kwargs.iteritems() if k.startswith('_'))
+    
+    @classmethod
+    def build (cls, _name, *args, **kwargs) :
+        """
+            Factory function for constructing Tags by directly passing in contents/attributes/options as Python function
+            arguments/keyword arguments.
+
+            The first positional argument is the tag's name:
+            
+            >>> Tag.build('foo')
+            tag('foo')
+            
+            Further positional arguments are the tag's contents:
+
+            >>> Tag.build('foo', 'quux', 'bar')
+            tag('foo', u'quux', u'bar')
+
+            All the rules used by process_contents() are available:
+            
+            >>> Tag.build('foo', [1, None], None, (n for n in xrange(2)))
+            tag('foo', u'1', u'0', u'1')
+
+            The special-case for a genexp as the only argument works:
+            
+            >>> f = lambda *args: Tag.build('foo', *args)
+            >>> f('hi' for n in xrange(2))
+            tag('foo', u'hi', u'hi')
+            
+            Attributes are passed as keyword arguments, with or without contents:
+
+            >>> Tag.build('foo', id=1)
+            tag('foo', id=u'1')
+            >>> Tag.build('foo', 'quux', bar=5)
+            tag('foo', u'quux', bar=u'5')
+            >>> Tag.build('foo', class_='ten')
+            tag('foo', class=u'ten')
+            
+            The attribute names don't conflict with positional argument names:
+
+            >>> Tag.build('bar', name='foo')
+            tag('bar', name=u'foo')
+
+            Options are handled as the 'real' keyword arguments:
+
+            >>> print Tag.build('foo', _selfclosing=False)
+            <foo></foo>
+            >>> print Tag.build('foo', _foo='bar')
+            Traceback (most recent call last):
+                ...
+            TypeError: __init__() got an unexpected keyword argument 'foo'
+        """
+
+        # pre-process incoming user values
+        contents = list(cls.process_contents(*args))
+        attrs = dict(cls.process_attrs(**kwargs))
+
+        # XXX: use Python 2.6 keyword-only arguments instead?
+        options = dict(cls.process_opts(**kwargs))
+
+        return cls(_name, contents, attrs, **options)
+
+    def __init__ (self, name, contents, attrs, selfclosing=None, whitespace_sensitive=None) :
+        """
+            Initialize internal Tag state with the given tag identifier, flattened list of content items, dict of
+            attributes and dict of options.
+
+                selfclosing             - set to False to render empty tags as <foo></foo> instead of <foo /> 
+                                          (for XHTML -> HTML compatibility)
+
+                whitespace_sensitive    - do not indent tag content onto separate rows, render the full tag as a single
+                                          row
+
+            Use the build() factory function to build Tag objects using Python's function call argument semantics.
+            
+            The tag name is used a pure string identifier:
+
+            >>> Tag(u'foo', [], {})
+            tag('foo')
+            >>> Tag(u'\\xE4', [], {})
+            Traceback (most recent call last):
+                ...
+            UnicodeEncodeError: 'ascii' codec can't encode character u'\\xe4' in position 0: ordinal not in range(128)
+
+            Contents have their order preserved:
+
+            >>> Tag('foo', [1, 2], {})
+            tag('foo', 1, 2)
+            >>> Tag('foo', [2, 1], {})
+            tag('foo', 2, 1)
+
+            Attributes can be given:
+            
+            >>> Tag('foo', [], dict(foo='bar'))
+            tag('foo', foo='bar')
+
+            Options can be given:
+
+            >>> print Tag('foo', [], {}, selfclosing=False)
+            <foo></foo>
+        """
+        
+        self.name = str(name)
+        self.contents = contents
+        self.attrs = attrs
+
+        # options
+        self.selfclosing = selfclosing
+        self.whitespace_sensitive = whitespace_sensitive
+
+    def __call__ (self, *args, **kwargs) :
+        """
+            Return a new Tag as a copy of this tag, but with the given additional attributes/contents.
+
+            The same rules for function positional/keyword arguments apply as for build()
+
+            >>> Tag.build('foo')('bar')
+            tag('foo', u'bar')
+            >>> Tag.build('a', href='index.html')("Home")
+            tag('a', u'Home', href=u'index.html')
+
+            New contents and attributes can be given freely, using the same rules as for Tag.build:
+
+            >>> Tag.build('bar', None)(5, foo=None, class_='bar')
+            tag('bar', u'5', class=u'bar')
+
+            Tag contents accumulate in order:
+
+            >>> Tag.build('a')('b', ['c'])('d')
+            tag('a', u'b', u'c', u'd')
+
+            Each Tag is immutable, so the called Tag isn't changed, but rather a copy is returned:
+
+            >>> t1 = Tag.build('a'); t2 = t1('b'); t1
+            tag('a')
+
+            Attribute values are replaced:
+
+            >>> Tag.build('foo', a=2)(a=3)
+            tag('foo', a=u'3')
+
+            Options are also supported:
+
+            >>> list(Tag.build('foo')(bar='quux', _selfclosing=False))
+            [u'<foo bar="quux"></foo>']
+        """
+
+        # accumulate contents
+        contents = self.contents + list(self.process_contents(*args))
+
+        # merge attrs
+        attrs = dict(self.attrs)
+        attrs.update(self.process_attrs(**kwargs))
+
+        # options
+        opts = dict(
+            selfclosing = self.selfclosing,
+            whitespace_sensitive = self.whitespace_sensitive,
+        )
+        opts.update(self.process_opts(**kwargs))
+
+        # build updated tag
+        return Tag(self.name, contents, attrs, **opts)
+
+    def render_attrs (self) :
+        """
+            Return the HTML attributes string
+
+            >>> Tag.build('x', foo=5, bar='<', quux=None).render_attrs()
+            u'foo="5" bar="&lt;"'
+            >>> Tag.build('x', foo='a"b').render_attrs()
+            u'foo=\\'a"b\\''
+        """
+
+        return " ".join(
+            (
+                u'%s=%s' % (name, saxutils.quoteattr(value))
+            ) for name, value in self.attrs.iteritems()
+        )
+
+    def flatten_items (self, indent=1) :
+        """
+            Flatten our content into a series of indented lines.
+
+            >>> list(Tag.build('tag', 5).flatten_items())
+            [(1, u'5')]
+            >>> list(Tag.build('tag', 'line1', 'line2').flatten_items())
+            [(1, u'line1'), (1, u'line2')]
+
+            Nested :
+            >>> list(Tag.build('tag', 'a', Tag.build('b', 'bb'), 'c').flatten_items())
+            [(1, u'a'), (1, u'<b>'), (2, u'bb'), (1, u'</b>'), (1, u'c')]
+            >>> list(Tag.build('tag', Tag.build('hr'), Tag.build('foo')('bar')).flatten_items())
+            [(1, u'<hr />'), (1, u'<foo>'), (2, u'bar'), (1, u'</foo>')]
+        """
+
+        for item in self.contents :
+            if isinstance(item, Renderable) :
+                # recursively flatten items
+                for line_indent, line in item.flatten() :
+                    # indented
+                    yield indent + line_indent, line
+
+            else :
+                # render HTML-escaped raw value
+                # escape raw values
+                yield indent, saxutils.escape(item)
+   
+    def flatten (self) :
+        """
+            Render the tag and all content as a flattened series of indented lines.
+            
+            Empty tags collapse per default:
+
+            >>> list(Tag.build('foo').flatten())
+            [(0, u'<foo />')]
+            >>> list(Tag.build('bar', id=5).flatten())
+            [(0, u'<bar id="5" />')]
+
+            Values are indented inside the start tag:
+
+            >>> list(Tag.build('foo', 'bar', a=5).flatten())
+            [(0, u'<foo a="5">'), (1, u'bar'), (0, u'</foo>')]
+            
+            Nested tags are further indented:
+
+            >>> list(Tag.build('1', '1.1', Tag.build('1.2', '1.2.1'), '1.3', a=5).flatten())
+            [(0, u'<1 a="5">'), (1, u'1.1'), (1, u'<1.2>'), (2, u'1.2.1'), (1, u'</1.2>'), (1, u'1.3'), (0, u'</1>')]
+
+            Empty tags are rendered with a separate closing tag on the same line, if desired:
+
+            >>> list(Tag.build('foo', _selfclosing=False).flatten())
+            [(0, u'<foo></foo>')]
+            >>> list(Tag.build('foo', src='asdf', _selfclosing=False).flatten())
+            [(0, u'<foo src="asdf"></foo>')]
+
+            Tags that are declared as whitespace-sensitive are collapsed onto the same line:
+
+            >>> list(Tag.build('foo', _whitespace_sensitive=True).flatten())
+            [(0, u'<foo />')]
+            >>> list(Tag.build('foo', _whitespace_sensitive=True, _selfclosing=False).flatten())
+            [(0, u'<foo></foo>')]
+            >>> list(Tag.build('foo', 'bar', _whitespace_sensitive=True).flatten())
+            [(0, u'<foo>bar</foo>')]
+            >>> list(Tag.build('foo', 'bar\\nasdf\\tx', _whitespace_sensitive=True).flatten())
+            [(0, u'<foo>bar\\nasdf\\tx</foo>')]
+            >>> list(Tag.build('foo', 'bar', Tag.build('quux', 'asdf'), 'asdf', _whitespace_sensitive=True).flatten())
+            [(0, u'<foo>bar<quux>asdf</quux>asdf</foo>')]
+
+            Embedded HTML given as string values is escaped:
+
+            >>> list(Tag.build('foo', '<asdf>'))
+            [u'<foo>', u'\\t&lt;asdf&gt;', u'</foo>']
+
+            Embedded quotes in attribute values are esacaped:
+
+            >>> list(Tag.build('foo', style='ok;" onload="...'))
+            [u'<foo style=\\'ok;" onload="...\\' />']
+            >>> list(Tag.build('foo', style='ok;\\'" onload=..."\\''))
+            [u'<foo style="ok;\\'&quot; onload=...&quot;\\'" />']
+        """
+
+        # optional attr spec
+        if self.attrs :
+            attrs = " " + self.render_attrs()
+
+        else :
+            attrs = ""
+
+        if not self.contents and self.selfclosing is False :
+            # empty tag, but don't use the self-closing syntax..
+            yield 0, u"<%s%s></%s>" % (self.name, attrs, self.name)
+
+        elif not self.contents  :
+            # self-closing xml tag
+            # do note that this is invalid HTML, and the space before the / is relevant for parsing it as HTML
+            yield 0, u"<%s%s />" % (self.name, attrs)
+
+        elif self.whitespace_sensitive :
+            # join together each line for each child, discarding the indent
+            content = u''.join(line for indent, line in self.flatten_items())
+
+            # render full tag on a single line
+            yield 0, u"<%s%s>%s</%s>" % (self.name, attrs, content, self.name)
+
+        else :
+            # start tag
+            yield 0, u"<%s%s>" % (self.name, attrs)
+
+            # contents, indented one level below the start tag
+            for indent, line in self.flatten_items(indent=1) :
+                yield indent, line
+
+            # close tag
+            yield 0, u"</%s>" % (self.name, )
+
+    def __repr__ (self) :
+        return 'tag(%s)' % ', '.join(
+            [
+                repr(self.name)
+            ] + [
+                repr(c) for c in self.contents
+            ] + [
+                '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
+            ]
+        )
+
+# factory function for Tag
+tag = Tag.build
+
+
+class Document (Renderable) :
+    """
+        A full XHTML 1.0 document with optional XML header, doctype, html[@xmlns].
+
+        >>> list(Document(tags.html('...')))
+        [u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', u'<html xmlns="http://www.w3.org/1999/xhtml">', u'\\t...', u'</html>']
+    """
+
+    def __init__ (self, root,
+        doctype='html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"',
+        html_xmlns='http://www.w3.org/1999/xhtml',
+        xml_version=None, xml_encoding=None, 
+    ) :
+        # add xmlns attr to root node
+        self.root = root(xmlns=html_xmlns)
+
+        # store
+        self.doctype = doctype
+        self.xml_declaration = {}
+
+        if xml_version :
+            self.xml_declaration['version'] = xml_version
+
+        if xml_encoding :
+            self.xml_declaration['encoding'] = xml_encoding
+
+    def flatten (self) :
+        """
+            Return the header lines along with the normally formatted <html> tag
+        """
+        
+        if self.xml_declaration :
+            yield 0, u'<?xml %s ?>' % (' '.join('%s="%s"' % kv for kv in self.xml_declaration.iteritems()))
+
+        if self.doctype :
+            yield 0, u'<!DOCTYPE %s>' % (self.doctype)
+
+        # <html>
+        for indent, line in self.root.flatten() :
+            yield indent, line
+
+class TagFactory (object) :
+    """
+        Build Tags with names give as attribute names
+        
+        >>> list(TagFactory().a(href='#')('Yay'))
+        [u'<a href="#">', u'\\tYay', u'</a>']
+
+        >>> list(TagFactory()("><"))
+        [u'><']
+    """
+
+    # full XHTML document
+    document = Document
+    
+    def __getattr__ (self, name) :
+        """
+            Get a Tag object with the given name, but no contents
+
+            >>> TagFactory().a
+            tag('a')
+        """
+
+        return Tag(name, [], {})
+
+    def __call__ (self, value) :
+        """
+            Raw HTML.
+        """
+
+        return Text(value)
+
+# static instance
+tags = TagFactory()
+
+# testing
+if __name__ == '__main__' :
+    import doctest
+
+    doctest.testmod()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/web/urls.py	Sun Jan 20 18:26:12 2013 +0200
@@ -0,0 +1,5 @@
+from werkzeug.routing import Map, Rule
+
+def rule (string, endpoint, **opts) :
+    return Rule(string, endpoint=endpoint, defaults=opts)
+
--- a/test.py	Sun Jan 20 18:19:03 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-#!/usr/bin/python
-
-from werkzeug.serving import run_simple
-
-from pvl import __version__
-import pvl.args
-import pvl.verkko.wsgi
-
-import optparse
-import logging; log = logging.getLogger('main')
-
-def parse_argv (argv, doc = __doc__) :
-    """
-        Parse command-line argv, returning (options, args).
-
-        Use pvl.ldap.args.apply() to get the LDAPClient
-    """
-
-    prog = argv.pop(0)
-    args = argv
-
-    # optparse
-    parser = optparse.OptionParser(
-        prog        = prog,
-        usage       = '%prog: [options] [<user> [...]]',
-        version     = __version__,
-        description = doc,
-    )
-
-    # common
-    parser.add_option_group(pvl.args.parser(parser))
-
-    parser.add_option('-d', '--database-read', metavar='URI', default='sqlite:///var/verkko.db',
-        help="Database to use (readonly)")
-
-    # parse
-    options, args = parser.parse_args(args)
-
-    # apply
-    pvl.args.apply(options)
-
-    return options, args
-
-def main (argv) :
-    """
-        Manage LDAP user dirs/symlinks/quotas on system.
-    """
-
-    # parse cmdline
-    options, args = parse_argv(argv, doc=__doc__)
-
-    # app
-    application = pvl.verkko.wsgi.Application(options.database_read)
-
-    # wsgi wrapper
-    run_simple('0.0.0.0', 8080, application,
-            #use_reloader    = True, 
-            use_debugger    = (options.loglevel == logging.DEBUG),
-            static_files    = {
-                '/static':      'static',
-            },
-    )
-
-if __name__ == '__main__' :
-    import sys
-
-    sys.exit(main(sys.argv))
-
-