reduce to irclogs.qmsk.net site
authorTero Marttila <terom@fixme.fi>
Sun, 08 Feb 2009 03:23:25 +0200
changeset 46 185504387370
parent 45 e94ab812c0c8
child 47 3d59c9eeffaa
reduce to irclogs.qmsk.net site
__init__.py
channels.py
handlers.py
index.cgi
lib/__init__.py
lib/config.py
lib/handler.py
lib/helpers.py
lib/http.py
lib/map.py
lib/site.py
lib/template.py
lib/tree_parse.py
lib/urltree.py
lib/wsgi.py
log_channel.py
log_event.py
log_source.py
logs/openttd
logs/tycoon
sites/irclogs.qmsk.net/__init__.py
sites/irclogs.qmsk.net/channels.py
sites/irclogs.qmsk.net/handlers.py
sites/irclogs.qmsk.net/log_channel.py
sites/irclogs.qmsk.net/log_event.py
sites/irclogs.qmsk.net/log_source.py
sites/irclogs.qmsk.net/logs/openttd
sites/irclogs.qmsk.net/logs/tycoon
sites/irclogs.qmsk.net/templates/channel.tmpl
sites/irclogs.qmsk.net/templates/index.tmpl
sites/irclogs.qmsk.net/templates/layout.tmpl
sites/irclogs.qmsk.net/urls.py
sites/www.qmsk.net/__init__.py
sites/www.qmsk.net/lookup.py
sites/www.qmsk.net/menu.py
sites/www.qmsk.net/page.py
sites/www.qmsk.net/page_tree.py
sites/www.qmsk.net/pages/about.html
sites/www.qmsk.net/pages/debug.tmpl
sites/www.qmsk.net/pages/index.tmpl
sites/www.qmsk.net/pages/list
sites/www.qmsk.net/pages/projects/evsql.html
sites/www.qmsk.net/pages/projects/foo.html
sites/www.qmsk.net/pages/projects/index.html
sites/www.qmsk.net/pages/projects/kg.html
sites/www.qmsk.net/pages/projects/nr-ttd.html
sites/www.qmsk.net/pages/projects/qmsk.net.html
sites/www.qmsk.net/templates/layout.tmpl
static/link.png
static/style.css
templates/channel.tmpl
templates/index.tmpl
templates/layout.tmpl
urls.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/__init__.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,10 @@
+"""
+    The irclogs.qmsk.net site is an IRC log browser
+"""
+
+# the URL mapper
+import urls
+
+# our RequestHandler
+handler = urls.mapper
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/channels.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,53 @@
+"""
+    Our list of LogChannels
+"""
+
+import pytz
+
+# for relpath
+import os.path
+
+from log_channel import LogChannel
+from log_source import LogDirectory
+
+relpath = lambda path : os.path.join(os.path.dirname(__file__), path)
+
+class ChannelList (object) :
+    """
+        The list of channels, and related methods
+    """
+    
+    # the statically defined channel list
+    CHANNELS = {
+        'tycoon':   LogChannel('tycoon', "OFTC", "#tycoon", 
+            LogDirectory(relpath('logs/tycoon'), pytz.timezone('Europe/Helsinki'))
+        ),
+        'openttd':   LogChannel('openttd', "OFTC", "#openttd", 
+            LogDirectory(relpath('logs/openttd'), pytz.timezone('Europe/Helsinki'))
+        ),
+    }
+
+    def __init__ (self, channels) :
+        """
+            Initialize with the given channel dict
+        """
+
+        self.channels = channels
+
+    def lookup (self, channel_name) :
+        """
+            Looks up the LogChannel for the given name
+        """
+
+        return self.channels[channel_name]
+
+    def __iter__ (self) :
+        """
+            Iterate over our defined LogChannel objects
+        """
+
+        return self.channels.itervalues()
+
+# the global singletone ChannelList...
+channel_list = ChannelList(ChannelList.CHANNELS)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/handlers.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,68 @@
+"""
+    Our URL action handlers
+"""
+
+from qmsk.web import http, template
+
+import urls, channels
+
+# load templates from here
+templates = template.TemplateLoader("sites/irclogs.qmsk.net/templates",
+    urls            = urls,
+    channel_list    = channels.channel_list,
+)
+
+def index (request) :
+    """
+        The topmost index page, display a list of available channels, perhaps some general stats
+    """
+    
+    return templates.render_to_response("index",
+        req             = request,
+    )
+
+def channel_select (request, channel) :
+    """
+        Redirect to the appropriate channel_view
+    """
+   
+    return http.Redirect(urls.channel_view.build(request, channel=channel.id))
+
+def channel_view (request, channel, count) :
+    """
+        The main channel view page, display the most important info, and all requisite links
+    """
+
+    if count == 'all' :
+        xxx
+
+    else :
+        count = int(count)
+
+    return templates.render_to_response("channel",
+        req             = request,
+        channel         = channel,
+        count           = count,
+        lines           = channel.source.get_latest(count),
+    )
+
+    pass
+
+def channel_last (request, channel, count, format) :
+    """
+        Display the last x lines of channel messages in various formats
+    """
+
+    if format == 'txt' :
+        return http.Response('\n'.join(channel.source.get_latest(count)), 'text/plain')
+    
+    else :
+        raise http.ResponseError("Unknown filetype %r" % format)
+
+def channel_search (request, channel) :
+    """
+        Display the search form for the channel for GET, or do the search for POST
+    """
+
+    pass
+
--- a/index.cgi	Sun Feb 08 03:13:11 2009 +0200
+++ b/index.cgi	Sun Feb 08 03:23:25 2009 +0200
@@ -27,13 +27,14 @@
     """
 
     try :
-        from lib import wsgi, site
+        from qmsk.web import wsgi
+        import urls
 
         # create handler
         cgi_handler = wsgiref.handlers.CGIHandler()
 
         # create app
-        app = wsgi.Application(site.SiteModuleCollection('sites'))
+        app = wsgi.Application(urls.mapper)
         
         # run once
         cgi_handler.run(app)
--- a/lib/config.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-"""
-    Configuration settings, preferences
-"""
-
-# datetime format for time.strftime
-DATETIME_FMT = "%Y/%m/%d %H:%M %Z"
-
--- a/lib/handler.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-"""
-    The actual application behaviour, i.e. generating a Response from a Request :)
-"""
-
-class RequestHandler (object) :
-    """
-        A handler handles a Request, returning a Response
-    """
-
-    def __init__ (self, func, *args, **kwargs) :
-        self.func = func
-        self.args = args
-        self.kwargs = kwargs
-    
-    def handle_request (self, request) :
-        """
-            Handle the request, returning a Response object
-
-            XXX: rename to __call__ kplzthx
-        """
-
-        return self.func(request, *self.args, **self.kwargs)
-
-
--- a/lib/helpers.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-"""
-    Helper functions for use in templates
-"""
-
-import time
-
-import config
-
-def now () :
-    """
-        Returns the current date/time
-    """
-
-    return time.strftime(config.DATETIME_FMT)
-
-def copyright_year () :
-    """
-        Returns the current year
-    """
-
-    return time.strftime("%Y")
-
-def validation_notice (site_host) :
-    """
-        Returns a short "Validated XHTML & CSS" link text for the given site hostname
-    """
-
-    return 'Validated <a href="http://validator.w3.org/check?uri=http://%(host)s">XHTML 1.0 Strict</a> &amp; <a href="http://jigsaw.w3.org/css-validator/validator?uri=http://%(host)s">CSS 2.1</a>' % dict(
-        host = site_host
-    )
-
-def breadcrumb (trail, links=True) :
-    """
-        Returns a nicely formatted breadcrumb tail, optinally with links
-    """
-
-    return ' &raquo; '.join(
-        (
-            '<a href="$site_url/%s">%s</a>' % (page.url, page.title) if links else page.title
-        ) for page in trail
-    )
-
--- a/lib/http.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-"""
-    WSGI HTTP utility code
-"""
-
-# for utility functions
-import cgi
-
-# for header handling
-import wsgiref.headers
-
-# for path handling
-import os.path
-
-class Request (object) :
-    """
-        HTTP Request with associated metadata
-    """
-
-    def __init__ (self, env) :
-        """
-            Parse env data
-        """
-
-        # store env
-        self.env = env
-
-        # get the querystring
-        self.arg_str = env.get('QUERY_STRING', '')
-
-        # parse query args
-        self.arg_dict = cgi.parse_qs(self.arg_str, True)
- 
-    @property
-    def site_host (self) :
-        """
-            Returns the site's hostname (DNS name)
-        """
-        
-        return self.env['HTTP_HOST']
-  
-    @property
-    def site_root (self) :
-        """
-            Returns the URL path to the requested script's directory with no trailing slash, i.e.
-
-            /               -> 
-            /foo.cgi        -> 
-            /foo/bar.cgi    -> /foo
-        """
-
-        return os.path.dirname(self.env['SCRIPT_NAME']).rstrip('/')
-    
-    @property
-    def page_prefix (self) :
-        """
-            Returns the URL path root for page URLs, based on REQUEST_URI with PATH_INFO removed
-
-            /                   -> 
-            /foo.cgi            -> /foo.cgi
-            /foo.cgi/index      -> /foo.cgi
-            /foo.cgi/quux/bar   -> /foo.cgi
-            /quux/foo.cgi/bar   -> /quux/foo.cgi
-            /bar                -> 
-        """
-        
-        # XXX: request uri path without the query string
-        request_path = self.env.get('REQUEST_URI', '').split('?', 1)[0].rstrip('/')
-
-        # path info
-        page_name = self.get_page_name()
-
-        # special-case for empty page_name
-        if not page_name :
-            return request_path
-        
-        # sanity-check
-        assert request_path.endswith(page_name)
-        
-        # trim
-        return request_path[:-len(page_name)].rstrip('/')
-    
-    def get_page_name (self) :
-        """
-            Returns the requested page path with no leading slash, i.e.
-
-            /foo.cgi        -> 
-            /foo.cgi/       -> 
-            /foo.cgi/bar    -> bar
-            /foo.cgi/quux/  -> quux/
-        """
-        
-        # the raw PATH_INFO
-        path_info = self.env.get('PATH_INFO')
-        
-        # avoid nasty '.' paths
-        if path_info :
-            return os.path.normpath(path_info).lstrip('/')
-
-        else :
-            return ''
-    
-    def get_args (self) :
-        """
-            Iterate over all available (key, value) pairs from the query string
-        """
-
-        return cgi.parse_qsl(self.arg_str)
-
-class Response (object) :
-    """
-        HTTP Response with headers and data
-    """
-
-    def __init__ (self, data, content_type='text/html', status='200 OK', charset='UTF-8') :
-        """
-            Create the response. The Content-type header is built from the given values. The given \a data must be
-            either a str (which is sent plain), an unicode object (which is encoded with the relevant charset), or
-            None, whereupon an empty response body is sent. The content_type argument can also be forced to None to
-            not send a Content-type header (e.g. for redirects)
-        """
-
-        # store info
-        self.status = status
-        self.data = data
-        self.charset = charset
-
-        # headers
-        self.headers = wsgiref.headers.Headers([])
-        
-        # add Content-type header?
-        if content_type :
-            self.add_header('Content-type', content_type, charset=charset)
-
-    def add_header (self, name, value, **params) :
-        """
-            Add response header with the given name/value, plus option params
-
-            XXX: uses the wsgiref.headers code, not sure how that behaves re multiple headers with the same name, etc
-        """
-        
-        self.headers.add_header(name, value, **params)
-    
-    def get_status (self) :
-        """
-            Returns response status string (XXX Foo)
-        """
-
-        return self.status
-    
-    def get_headers (self) :
-        """
-            Returns the list of header (name, value) pairs
-        """
-
-        return self.headers.items()
-
-    def get_data (self) :
-        """
-            Returns the response data - as an encoded string
-        """
-
-        if self.data :
-            return self.data.encode(self.charset)
-
-        else :
-            return ''
-
-class ErrorResponse (Response) :
-    """
-        A response with an error code / message
-    """
-
-    def __init__ (self, status, message, details=None) :
-        """
-            Build a plain error message response with the given status/message
-
-            @param status HTTP status code
-            @param message short message to describe errors
-            @param details optional details, plaintext
-        """
-
-        data = """\
-<html><head><title>%(title)s</title></head><body>
-<h1>%(title)s</h1>
-<p>%(message)s</p>
-%(details)s
-</body></html>
-""" % dict(
-            title       = status, 
-            message     = message,
-            details     = '<pre>%s</pre>' % details if details else ''
-        )
-            
-        super(ErrorResponse, self).__init__(data, status=status)
-
-class ResponseError (Exception) :
-    """
-        An exception that results in a specfic 4xx ErrorResponse message to the client
-    """
-
-    def __init__ (self, message, status='400 Bad Request', details=None) :
-        self.status = status
-        self.message = message
-        self.details = details
-
-        super(ResponseError, self).__init__(message)
-
-    def get_response (self) :
-        return ErrorResponse(self.status, self.message, self.details)
-
-class Redirect (Response) :
-    """
-        Redirect response
-    """
-
-    def __init__ (self, url) :
-        """
-            Redirect to given *absolute* URL
-        """
-        
-        # no content-type or data
-        super(Redirect, self).__init__(None, content_type=None, status='302 Found')
-
-        # add Location: header
-        self.add_header("Location", url)
-
-
--- a/lib/map.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-"""
-    Handles mapping URLs to request handlers
-"""
-
-import http
-import handler
-
-class MapperError (http.ResponseError) :
-    """
-        URL could not be mapped
-    """
-
-    def __init__ (self, url) :
-        super(MapperError, self).__init__("URL not found: %s" % (url, ), status='404 Not Found')
-
-class Mapper (handler.RequestHandler) :
-    """
-        Looks up the handler to use based on the URL
-    """
-
-    def handle_request (self, request) :
-        """
-            Map the given request
-        """
-
-        abstract
-
-class Mapping (object) :
-    """
-        A mapping object for StaticMapping
-    """
-
-    def test (self, request) :
-        """
-            Either return a handler, or None
-        """
-
-        abstract
-
-class RegexpMapping (object) :
-    """
-        A mapping object that uses regular expressions
-    """
-
-    def __init__ (self, regexp, handler) :
-        pass
-
-    def test (self, request) :
-        xxx
-
-class SimpleMapping (object) :
-    """
-        A mapping object that uses simple expressions
-    """
-
-    def __init__ (self, expression, handler) :
-        pass
-
-    def test (self, request) :
-        xxx
-
-class StaticMapper (Mapper) :
-    """
-        Translates requests to handlers using a list of pre-determined Mapping's
-    """
-
-    def __init__ (self, mappings) :
-        # store
-        self.mappings = mappings
-
-    def handle_request (self, request) :
-        """
-            Returns the appropriate handler
-        """
-
-        # find handler to use
-        handler = None
-
-        # just test each mapping in turn
-        for mapping in self.mappings :
-            handler = mapping.test(request)
-
-            if handler :
-                break
-        
-        if not handler :
-            # fail, not found
-            raise MapperError(request.get_page_name())
-        
-        # passthrough
-        return handler.handle_request(request)
-
-# "friendly" names
-map     = SimpleMapping
-mapre   = RegexpMapping
-
--- a/lib/site.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-"""
-    Per-site stuff
-"""
-
-import imp
-
-import handler
-
-class Site (handler.RequestHandler) :
-    """
-        A site is a website and its configuration
-
-        XXX: need to somehow communicate the site name to our downstream handler
-    """
-
-    def __init__ (self, name, handler) :
-        """
-            The given name must be like a valid hostname, e.g. 'www.qmsk.net'
-        """
-
-        # store
-        self.name = name
-        self.handler = handler
-
-    def handle_request (self, request) :
-        """
-            Map the request through our handler...
-        """
-
-        return self.handler.handle_request(request)
-
-class SiteModule (Site) :
-    """
-        A site, represented as python module/package, with the following module attributes:
-
-            handler     - the RequestHandler to use
-    """
-
-    def __init__ (self, name, module) :
-        """
-            Create the Site based on the given module
-        """
-
-        super(SiteModule, self).__init__(name,
-            module.handler
-        )
-
-class SiteModuleCollection (handler.RequestHandler) :
-    """
-        A collection of SiteModules, looking up the correct site to use based on the request hostname
-    """
-
-    def __init__ (self, path) :
-        """
-            Initialize to load site modules from the given path
-        """
-
-        self.path = path
-        self.site_cache = dict()
-
-    def handle_request (self, request) :
-        """
-            Lookup and return a Site object for the given request
-        """
-
-        # request hostnmae
-        name = request.env['HTTP_HOST']
-        
-        # already loaded?
-        if name in self.site_cache :
-            site = self.site_cache[name]
-
-        else :
-            # first, we need to find it
-            file, pathname, description = imp.find_module(name, [self.path])
-
-            # then, we can load the module
-            module = imp.load_module(name, file, pathname, description)
-            
-            # then build+cache the SiteModule
-            site = self.site_cache[name] = SiteModule(name, module)
-
-        # then execute the site's request handler
-        return site.handle_request(request)
-
--- a/lib/template.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-"""
-    Template handler
-"""
-
-# use Mako
-from mako import exceptions
-from mako.template import Template
-import mako.lookup
-
-# for http.ResponseError
-import http
-
-import helpers
-
-# path to template files
-TEMPLATE_DIR = "templates"
-
-# path to cached templates
-CACHE_DIR = "cache/templates"
-
-# template file extension
-TEMPLATE_EXT = "tmpl"
-
-class TemplateError (http.ResponseError) :
-    """
-        Raised by the template module functions
-    """
-
-    pass
-
-class TemplateLoader (mako.lookup.TemplateLookup) :
-    """
-        Our own specialization of mako's TemplateLookup
-    """
-
-    def __init__ (self, path, fileext=TEMPLATE_EXT, **env) :
-        """
-            Initialize to load templates located at path, with the given file extension.
-
-            The given **env list is supplied to every template render
-        """
-
-        # store
-        self.path = path
-        self.fileext = fileext
-        self.env = env
-            
-        # build the TemplateLookup
-        super(TemplateLoader, self).__init__(directories=[path], module_directory=CACHE_DIR)
-    
-    @staticmethod
-    def _render (tpl, env, params) :
-        """
-            Render the given template with given env/params, returning the output as a unicode string, or raising a TemplateError
-        """
-
-        # build the context from our superglobals, env, and params
-        ctx = dict(
-            h       = helpers,
-        )
-        ctx.update(env)
-        ctx.update(params)
-
-        try :
-            return tpl.render_unicode(**ctx)
-        
-        # a template may render other templates
-        except TemplateError :
-            raise
-
-        except :
-            details = exceptions.text_error_template().render()
-
-            raise TemplateError("Template render failed", status='500 Internal Server Error', details=details)
-
-
-    def lookup (self, name) :
-        """
-            Looks up a template based on the bare "name", which does not include the path or file extension
-        """
-        
-        try :
-            return self.get_template("%s.%s" % (name, self.fileext))
-
-        except :
-            raise TemplateError("Template broken: %r" % (name, ), status='500 Internal Server Error', details=exceptions.text_error_template().render())
-    
-    def render (self, name, **params) :
-        """
-            Render a template, using lookup() on the given name
-        """
-
-        return self._render(self.lookup(name), self.env, params)
-
-    def render_to_response (self, name, **params) :
-        """
-            Render a template, returning a http.Response object
-        """
-
-        return http.Response(self.render(name, **params))
-
-    @classmethod
-    def load (cls, path) :
-        """
-            Loads a template from a specific file
-        """
-
-        try :
-            return Template(filename=path, module_directory=CACHE_DIR)
-
-        except :
-            raise TemplateError("Template broken: %r" % (path, ), status='500 Internal Server Error', details=exceptions.text_error_template().render())
-    
-    @classmethod
-    def render_file (cls, path, **params) :
-        """
-            Render a template, using load() on the given path. No global environment vars are defined for the render.
-        """
-
-        return cls._render(cls.load(path), dict(), params)
-
-    @classmethod
-    def render_template (cls, template, **params) :
-        """
-            Render the given template object. No global environment vars are defined for the render.
-        """
-        
-        return cls._render(template, dict(), params)
--- a/lib/tree_parse.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-
-"""
-    Parsing trees of node stored using a python-like syntax.
-
-    A file consists of a number of lines, and each line consists of indenting whitespace and data. Each line has a parent
-"""
-
-class TreeParseError (Exception) :
-    """
-        Error parsing a tree file
-    """
-
-    pass
-
-def _read_lines (path, stop_tokens, charset) :
-    """
-        Reads lines from the given path, ignoring empty lines, and yielding (line_number, indent, line) tuples, where 
-        line_number is the line number, indent counts the amount of leading whitespace, and line is the actual line
-        data with whitespace stripped.
-
-        Stop tokens is a list of chars to stop counting indentation on - if such a line begins with such a char, its
-        indentation is taken as zero.
-    """
-
-    for line_number, line in enumerate(open(path, 'rb')) :
-        # decode to unicode
-        line = line.decode(charset)
-
-        indent = 0
-
-        # count indent
-        for char in line :
-            # tabs break things
-            assert char != '\t'
-            
-            # increment up to first non-space char
-            if char == ' ' :
-                indent += 1
-            
-            elif char in stop_tokens :
-                # consider line as not having any indentation at all
-                indent = 0
-                break
-
-            else :
-                break
-        
-        # strip whitespace
-        line = line.strip()
-
-        # ignore empty lines
-        if not line :
-            continue
-
-        # yield
-        yield line_number + 1, indent, line
-
-def parse (path, stop_tokens='', charset='utf8') :
-    """
-        Reads and parses the file at the given path, returning a list of (line_number, line, children) tuples.
-    """
-
-    # stack of (indent, PageInfo) items
-    stack = []
-
-    # the root item
-    root = None
-
-    # the previous item processed, None for first one
-    prev = None
-    
-    # read lines
-    for line_number, indent, line in _read_lines(path, stop_tokens, charset) :
-        # create item
-        item = (line_number, line, [])
-
-        # are we the first item?
-        if not prev :
-            # root node does not have a parent
-            parent = None
-            
-            # set root
-            root = item
-
-            # initialize stack
-            stack.append((0, root))
-            
-        else :
-            # peek stack
-            stack_indent, stack_parent = stack[-1]
-
-            # new indent level?
-            if indent > stack_indent :
-                # set parent to previous item, and push new indent level + parent to stack
-                parent = prev
-
-                # push new indent level + its parent
-                stack.append((indent, parent))
-
-            # same indent level as previous
-            elif indent == stack_indent :
-                # parent is the one of the current stack level, stack doesn't change
-                parent = stack_parent
-            
-            # unravel stack
-            elif indent < stack_indent :
-                while True :
-                    # remove current stack level
-                    stack.pop(-1)
-
-                    # peek next level
-                    stack_indent, stack_parent = stack[-1]
-                    
-                    # found the level to return to?
-                    if stack_indent == indent :
-                        # restore prev
-                        parent = stack_parent
-
-                        break
-
-                    elif stack_indent < indent :
-                        raise TreeParseError("Bad unindent on %s:%d, %d < %d" % (path, line_number, stack_indent, indent))
-        
-        # add to parent?
-        if parent :
-            parent[2].append(item)
-
-        # update prev
-        prev = item
-    
-    # return the root
-    return root
-
--- a/lib/urltree.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,670 +0,0 @@
-"""
-    Tree-based URL mapping
-"""
-
-import re
-import os.path
-
-# for Mapper
-from lib import map
-
-class URLError (Exception) :
-    """
-        Error with an URL definition
-    """
-
-    pass
-
-class LabelValue (object) :
-    """
-        Represents the value of a ValueLabel... love these names
-    """
-
-    def __init__ (self, label, value) :
-        """
-            Just store
-        """
-
-        self.label = label
-        self.value = value
-    
-    def __str__ (self) :
-        return "%s=%r" % (self.label.key, self.value)
-
-    def __repr__ (self) :
-        return "<%s>" % self
-
-class Label (object) :
-    """
-        Base class for URL labels (i.e. the segments of the URL between /s)
-    """
-
-    @staticmethod
-    def parse (mask, defaults, types) :
-        """
-            Parse the given label-segment, and return a *Label instance
-        """
-
-        # empty?
-        if not mask :
-            return EmptyLabel()
-
-        # simple value?
-        match = SimpleValueLabel.EXPR.match(mask)
-
-        if match :
-            # key
-            key = match.group('key')
-
-            # type
-            type = match.group("type")
-            
-            # lookup type, None for default
-            type = types[type]
-
-            # defaults?
-            default = defaults.get(key)
-
-            if not default :
-                default = match.group('default')
-
-                if default :
-                    # apply type to default
-                    default = type(default)
-
-            # build
-            return SimpleValueLabel(key, type, default)
-        
-        # static?
-        match = StaticLabel.EXPR.match(mask)
-
-        if match :
-            return StaticLabel(match.group('name'))
-
-        # invalid
-        raise URLError("Invalid label: %r" % (mask, ))
-    
-    def match (self, value=None) :
-        """
-            Match this label against the given value, returning either True to match without a value, a LabelValue
-            object, or boolean false to not match.
-
-            If value is None, this means that only a default value should be returned.
-        """
-
-        abstract
-    
-    def build (self, value_dict) :
-        """
-            Return a string representing this label, using the values in the given value_dict if needed
-        """
-
-        abstract
-
-class EmptyLabel (Label) :
-    """
-        An empty label, i.e. just a slash in the URL
-    """
-    
-    def __eq__ (self, other) :
-        """
-            Just compares type
-        """
-
-        return isinstance(other, EmptyLabel)
-    
-    def match (self, value=None) :
-        """
-            Match empty string -> True
-        """
-        
-        # no default
-        if value is None :
-            return False
-        
-        # only empty segments
-        if value == '' :
-            return True
-    
-    def build (self, values) :
-        return str(self)
-
-    def __str__ (self) :
-        return ''
-
-class StaticLabel (Label) :
-    """
-        A simple literal Label, used for fixed terms in the URL
-    """
-
-    EXPR = re.compile(r'^(?P<name>[a-zA-Z_.-]+)$')
-
-    def __init__ (self, name) :
-        """
-            The given name is the literal name of this label
-        """
-
-        self.name = name
-
-    def __eq__ (self, other) :
-        """
-            Compares names
-        """
-
-        return isinstance(other, StaticLabel) and self.name == other.name
-    
-    def match (self, value=None) :
-        """
-            Match exactly -> True
-        """
-
-        # no defaults
-        if value is None :
-            return False
-        
-        # match name
-        if value == self.name :
-            return True
-
-    def build (self, values) :
-        return str(self)
-
-    def __str__ (self) :
-        return self.name
-
-class ValueLabel (Label) :
-    """
-        A label with a key and a value
-
-        XXX: do we even need this?
-    """
-
-    def __init__ (self, key, default) :
-        """
-            Set the key and default value. Default value may be None if there is no default value defined
-        """
-
-        self.key = key
-        self.default = default
-
-    def __eq__ (self, other) :
-        """
-            Compares keys
-        """
-
-        return isinstance(other, ValueLabel) and self.key == other.key
-    
-    def build (self, values) :
-        """
-            Return either the assigned value from values, our default value, or raise an error
-        """
-
-        value = values.get(self.key)
-        
-        if not value and self.default :
-            value = self.default
-
-        elif not value :
-            raise URLError("No value given for label %r" % (self.key, ))
-
-        return value
-
-class SimpleValueLabel (ValueLabel) :
-    """
-        A label that has a name and a simple string value
-    """
-
-    EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
-
-    def __init__ (self, key, type=str, default=None) :
-        """
-            The given key is the name of this label's value
-        """
-
-        # type
-        self.type = type
-
-        # store
-        self.key = key
-        self.default = default
-        
-    def match (self, value=None) :
-        """
-            Match -> LabelValue
-        """
-        
-        # default?
-        if value is None and self.default :
-            return LabelValue(self, self.default)
-        
-        # only non-empty values!
-        elif value :
-            # convert with type
-            try :
-                value = self.type(value)
-
-            except Exception, e :
-                raise URLError("Bad value %r for type %s: %s: %s" % (value, self.type.__name__, type(e).__name__, e))
-
-            return LabelValue(self, value)
-
-    def __str__ (self) :
-        return '{%s%s%s}' % (
-            self.key, 
-            ':%s' % (self.type.__name__ ) if self.type != str else '',
-            '=%s' % (self.default, ) if self.default else '',
-        )
-
-class URLConfig (object) :
-    """
-        Global configuration relevant to all URLs
-    """
-
-    # built-in type codes
-    BUILTIN_TYPES = {
-        # default
-        None    : str,
-
-        # string
-        'str'   : str,
-
-        # integer
-        'int'   : int,
-    }
-
-    def __init__ (self, type_dict=None) :
-        """
-            Create an URLConfig for use with URL
-
-            If type_dict is given, it should be a mapping of type names -> callables, and they will be available for
-            type specifications in addition to the defaults.
-        """
-
-        # build our type_dict
-        self.type_dict = self.BUILTIN_TYPES.copy()
-        
-        # apply the given type_dict
-        if type_dict :
-            self.type_dict.update(type_dict)
-
-class URL (object) :
-    """
-        Represents a specific URL
-    """
-
-
-    def __init__ (self, config, url_mask, handler, type_dict=None, **defaults) :
-        """
-            Create an URL using the given URLConfig, with the given url mask, handler, and default values.
-        """
-
-        # store
-        self.config = config
-        self.url_mask = url_mask
-        self.handler = handler
-        self.defaults = defaults
-
-        # query string
-        self.query_args = dict()
-        
-        # parse any query string
-        # XXX: conflicts with regexp syntax
-        if '/?' in url_mask :
-            url_mask, query_mask = url_mask.split('/?')
-        
-        else :
-            query_mask = None
-
-        # build our label path
-        self.label_path = [Label.parse(mask, defaults, config.type_dict) for mask in url_mask.split('/')]
-
-        # build our query args list
-        if query_mask :
-            # split into items
-            for query_item in query_mask.split('&') :
-                # parse default
-                if '=' in query_item :
-                    query_item, default = query_item.split('=')
-
-                else :
-                    default = None
-                
-                # parse type
-                if ':' in query_item :
-                    query_item, type = query_item.split(':')
-                else :
-                    type = None
-                
-                # parse key
-                key = query_item
-
-                # type
-                type = self.config.type_dict[type]
-
-                # add to query_args as (type, default) tuple
-                self.query_args[key] = (type, type(default) if default else default)
-         
-    def get_label_path (self) :
-        """
-            Returns a list containing the labels in this url
-        """
-        
-        # copy self.label_path
-        return list(self.label_path)
-
-    def execute (self, request, label_values) :
-        """
-            Invoke the handler, using the given label values
-        """
-        
-        # start with the defaults
-        kwargs = self.defaults.copy()
-
-        # then add all the values
-        for label_value in label_values :
-            kwargs[label_value.label.key] = label_value.value
-       
-        # then parse all query args
-        for key, value in request.get_args() :
-            # lookup spec
-            type, default = self.query_args[key]
-
-            # normalize empty value to None
-            if not value :
-                value = None
-
-            else :
-                # process value
-                value = type(value)
-
-            # set default?
-            if not value :
-                if default :
-                    value = default
-
-                if default == '' :
-                    # do not pass key at all
-                    continue
-
-                # otherwise, fail
-                raise URLError("No value given for required argument: %r" % (key, ))
-            
-            # set key
-            kwargs[key] = value
-        
-        # then check all query args
-        for key, (type, default) in self.query_args.iteritems() :
-            # skip those already present
-            if key in kwargs :
-                continue
-
-            # apply default?
-            if default is None :
-                raise URLError("Missing required argument: %r" % (key, ))
-            
-            elif default == '' :
-                # skip empty default
-                continue
-
-            else :
-                # set default
-                kwargs[key] = default
-
-        # execute the handler
-        return self.handler(request, **kwargs)
-    
-    def build (self, request, **values) :
-        """
-            Build an absolute URL pointing to this target, with the given values
-        """
-
-        # build URL from request page prefix and our labels
-        return request.page_prefix + '/'.join(label.build(values) for label in self.label_path)
-
-    def __str__ (self) :
-        return '/'.join(str(label) for label in self.label_path)
-    
-    def __repr__ (self) :
-        return "URL(%r, %r)" % (str(self), self.handler)
-
-class URLNode (object) :
-    """
-        Represents a node in the URLTree
-    """
-
-    def __init__ (self, parent, label) :
-        """
-            Initialize with the given parent and label, empty children dict
-        """
-        
-        # the parent URLNode
-        self.parent = parent
-
-        # this node's Label
-        self.label = label
-
-        # list of child URLNodes
-        self.children = []
-
-        # this node's URL, set by add_url for an empty label_path
-        self.url = None
-
-    def _build_child (self, label) :
-        """
-            Build, insert and return a new child Node
-        """
-        
-        # build new child
-        child = URLNode(self, label)
-        
-        # add to children
-        self.children.append(child)
-
-        # return
-        return child
-
-    def add_url (self, url, label_path) :
-        """
-            Add a URL object to this node under the given path. Uses recursion to process the path.
-
-            The label_path argument is a (partial) label path as returned by URL.get_label_path.
-
-            If label_path is empty (len zero, or begins with EmptyLabel), then the given url is assigned to this node, if no
-            url was assigned before.
-        """
-        
-        # matches this node?
-        if not label_path or isinstance(label_path[0], EmptyLabel) :
-            if self.url :
-                raise URLError(url, "node already defined")
-
-            else :
-                # set
-                self.url = url
-
-        else :
-            # pop child label from label_path
-            child_label = label_path.pop(0)
-
-            # look for the child to recurse into
-            child = None
-
-            # look for an existing child with that label
-            for child in self.children :
-                if child.label == child_label :
-                    # found, use this
-                    break
-
-            else :
-                # build a new child
-                child = self._build_child(child_label)
-
-            # recurse to handle the rest of the label_path
-            child.add_url(url, label_path)
-    
-    def match (self, label_path) :
-        """
-            Locate the URL object corresponding to the given label_path value under this node.
-
-            Returns a (url, label_values) tuple
-        """
-
-        # determine value to use
-        value = None
-
-        # empty label_path?
-        if not label_path or label_path[0] == '' :
-            # the search ends at this node
-            if self.url :
-                # this URL is the best match
-                return (self.url, [])
-            
-            elif not self.children :
-                # incomplete URL
-                raise URLError("no URL handler defined for this Node")
-            
-            else :
-                # use default value, i.e. Label.match(None)
-                label = None
-
-        else :
-            # pop the next label from the label path
-            label = label_path.pop(0)
-
-        # return one match...
-        match = value = None
-
-        # recurse through our children, DFS
-        for child in self.children :
-            # match value
-            value = child.label.match(label)
-
-            # skip those that don't match at all
-            if not value :
-                continue;
-            
-            # already found a match? :/
-            if match :
-                raise URLError("Ambiguous URL")
-
-            # ok, but continue looking to make sure there's no ambiguous URLs
-            match = child
-        
-        # found something?
-        if not match :
-            raise URLError("No child found for label: %s + %s + %s" % (self.get_url(), label, '/'.join(str(l) for l in label_path)))
-
-        # ok, recurse into the match
-        url, label_value = match.match(label_path)
-
-        # add our value?
-        if isinstance(value, LabelValue) :
-            label_value.append(value)
-
-        # return the match
-        return url, label_value
-
-    def get_url (self) :
-        """
-            Returns the URL for this node, by iterating over our parents
-        """
-        
-        # URL segments in reverse order
-        segments = ['']
-        
-        # start with ourself
-        node = self
-        
-        # iterate up to root
-        while node :
-            segments.append(str(node.label))
-
-            node = node.parent
-
-        # reverse
-        segments.reverse()
-
-        # return
-        return '/'.join(segments)
-
-    def dump (self, indent=0) :
-        """
-            Returns a multi-line string representation of this Node
-        """
-
-        return '\n'.join([
-            "%-45s%s" % (
-                ' '*indent + str(self.label) + ('/' if self.children else ''), 
-                (' -> %r' % self.url) if self.url else ''
-            )
-        ] + [
-            child.dump(indent + 4) for child in self.children
-        ])
-
-    def __str__ (self) :
-        return "%s/[%s]" % (self.label, ','.join(str(child) for child in self.children))
-
-class URLTree (map.Mapper) :
-    """
-        Map requests to handlers, using a defined tree of URLs
-    """
-
-    def __init__ (self, url_list) :
-        """
-            Initialize the tree using the given list of URLs
-        """
-
-        # root node
-        self.root = URLNode(None, EmptyLabel())
-        
-        # just add each URL
-        for url in url_list :
-            self.add_url(url)
-
-    def add_url (self, url) :
-        """
-            Adds the given URL to the tree. The URL must begin with a root slash.
-        """
-        # get url's label path
-        path = url.get_label_path()
-
-        # should begin with root
-        root_label = path.pop(0)
-        assert root_label == self.root.label, "URL must begin with root"
-
-        # add to root
-        self.root.add_url(url, path)
-        
-    def match (self, url) :
-        """
-            Find the URL object best corresponding to the given url, matching any ValueLabels.
-
-            Returns an (URL, [LabelValue]) tuple.
-        """
-
-        # split it into labels
-        path = url.split('/')
-        
-        # empty URL is empty
-        if url :
-            # ensure that it doesn't start with a /
-            assert not self.root.label.match(path[0]), "URL must not begin with root"
-
-        # just match starting at root
-        return self.root.match(path)
-
-    def handle_request (self, request) :
-        """
-            Looks up the request's URL, and invokes its handler
-        """
-        
-        # get the requested URL
-        request_url = request.get_page_name()
-
-        # find the URL+values to use
-        url, label_values = self.match(request_url)
-
-        # let the URL handle it
-        return url.execute(request, label_values)
-
-
--- a/lib/wsgi.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-
-"""
-    WSGI application implementation
-"""
-
-# for error reporting
-import sys, traceback
-
-# for Request/Response
-import http
-
-class Application (object) :
-    """
-        Our WSGI application, implements the wsgi __call__ interface
-    """
-
-    def __init__ (self, handler) :
-        """
-            Initialize to use the given handler for requests
-        """
-
-        self.handler = handler
-    
-    def handle_request (self, env, start_response) :
-        """
-            The actual request handling code
-        """
-
-        # build Request object
-        request = http.Request(env)
-
-        try :
-            # request -> response using our handler
-            response = self.handler.handle_request(request)
-
-        except http.ResponseError, err :
-            # just use the generated response
-            response = err.get_response()
-
-        # send response
-        assert response, "No response"
-        
-        # send response status/headers
-        start_response(response.get_status(), response.get_headers())
-
-        # send respones data
-        yield response.get_data()
-
-    def __call__ (self, env, start_response) :
-        """
-            Wraps handle_request to trap errors
-        """
-
-        try :
-            # passthrough request_handler
-            for chunk in self.handle_request(env, start_response) :
-                yield chunk
-
-        except :
-            # execption info
-            info = sys.exc_info()
-
-            # try and send 500 ISE to browser, if no headers yet...
-            start_response("500 Internal Server Error", [('Content-type', "text/plain; charset=utf8")], info)
-
-            # send traceback
-            yield traceback.format_exc()
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/log_channel.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,27 @@
+"""
+    A channel represents a series of log events, stored in some log source
+"""
+
+class LogChannel (object) :
+    """
+        A single IRC channel, logged to some specific place
+    """
+
+    def __init__ (self, id, network, name, source) :
+        """
+            Initialize this channel from the given identifier key, network name, channel name, and LogSource
+        """
+        
+        self.id = id
+        self.network = network
+        self.name = name
+        self.source = source
+    
+    @property
+    def title (self) :
+        """
+            Title is 'Network - #channel'
+        """
+
+        return "%s - %s" % (self.network, self.name)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/log_event.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,21 @@
+"""
+    An IRC logfile consists of a series of events, a.k.a. "lines"
+"""
+
+class LogEvent (object) :
+    """
+        An event on some specific channel
+    """
+
+    # the event ype
+    type = None
+
+    # the UTC timestamp of the event
+    timestamp = None
+
+    # the event source
+    source = None
+
+    # associated data (message, etc)
+    data = None
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/log_source.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,231 @@
+"""
+    A source of IRC log files
+"""
+
+import codecs
+from datetime import date, datetime, timedelta
+import pytz
+
+# for SEEK_*, errno
+import os, errno
+
+class LogSource (object) :
+    """
+        A collection of IRC logs for a specific target in some format. Provides the possibility to read specific events
+    """
+    
+    def get_latest (self, count) :
+        """
+            Yield the latest events, up to `count` of them.
+        """
+
+        abstract
+
+class LogFile (LogSource) :
+    """
+        A file containing LogEvents
+    """
+
+    def __init__ (self, path, charset='utf-8', sep='\n') :
+        """
+            Open the file at the given path, which contains data of the given codec, as lines separated by the given separator
+        """
+        
+        # store
+        self.path = path
+        self.charset = charset
+        self.sep = sep
+
+        # open
+        self.file = codecs.open(path, 'r', charset)
+    
+    def __iter__ (self) :
+        """
+            Yields a series of lines, as read from the top of the file
+        """
+        
+        # seek to beginning
+        self.file.seek(0)
+
+        # iterate over lines
+        return iter(self.file)
+    
+    def get_latest (self, count) :
+        """
+            Returns up to <count> lines from the end of the file, or less, if the file doesn't contain that many lines
+        """
+
+        # the list of lines
+        lines = []
+
+        # seek to end of file
+        self.file.seek(0, os.SEEK_END)
+
+        # read offset
+        # XXX; why -2 ?
+        size = offset = self.file.tell() - 2
+
+        # use this blocksize
+        BLOCKSIZE = 1024
+
+        # trailing data
+        buf = ''
+
+        # read a block at a time, backwards
+        while  count > 0 and offset >= 0:
+            # update offset back one block
+            offset -= BLOCKSIZE
+
+            # normalize to zero
+            if offset < 0 :
+                offset = 0
+
+            # seek to offset
+            self.file.seek(offset)
+
+            # add the new block to our buffer
+            read_buf = self.file.read(BLOCKSIZE)
+
+            # XXX: trim off extra...
+            if len(read_buf) > BLOCKSIZE :
+                read_buf = read_buf[:BLOCKSIZE]
+
+            # make sure we got the right amount of data
+            assert len(read_buf) == BLOCKSIZE, "read(%d) @ %d/%d -> %d" % (BLOCKSIZE, offset, size, len(read_buf))
+
+            # add in our previous buf
+            buf = read_buf + buf
+            
+            # split out lines
+            buf_lines = buf.split(self.sep)
+
+            # keep the first one as our buffer, as it's incomplete
+            buf = buf_lines[0]
+
+            # add up to count lines to our lines buffer
+            lines = buf_lines[-min(count, len(buf_lines) - 1):] + lines
+
+            # update count
+            count -= (len(buf_lines) - 1)
+
+        # return the line list
+        return lines
+
+class LogDirectory (LogSource) :
+    """
+        A directory containing a series of timestamped LogFiles
+    """
+
+    def __init__ (self, path, tz, charset='utf-8', filename_fmt='%Y-%m-%d') :
+        """
+            Load the logfiles at the given path.
+            
+            The files contain data in the given charset, and are named according the the date in the given timezone and
+            date format.
+        """
+
+        # store
+        self.path = path
+        self.tz = tz
+        self.charset = charset
+        self.filename_fmt = filename_fmt
+
+    def _get_logfile_datetime (self, dt) :
+        """
+            Get the logfile corresponding to the given datetime
+        """
+
+        # convert to target timezone
+        dtz = dt.astimezone(self.tz)
+        
+        # convert to date and use that
+        return self._get_logfile_date(dtz.date())
+
+    def _get_logfile_date (self, d) :
+        """
+            Get the logfile corresponding to the given naive date in our timezone
+        """
+
+        # format filename
+        filename = d.strftime(self.filename_fmt)
+
+        # build path
+        path = os.path.join(self.path, filename)
+
+        # return the LogFile
+        return LogFile(path, self.charset)
+    
+    def _iter_backwards (self, dt=None) :
+        """
+            Yields an infinite series of naive date objects in our timezone, iterating backwards in time starting at the
+            given *datetime*, or the the current date, if none given
+        """
+        
+        # default to now
+        if not dt :
+            dt = datetime.now(pytz.utc)
+        
+        # convert to target timezone
+        dtz = dt.astimezone(self.tz)
+
+        # our timedelta
+        ONE_DAY = timedelta(1)
+        
+        # iterate unto infinity
+        while True :
+            # yield
+            yield dtz.date()
+            
+            # one day sdrawkcab
+            dtz -= ONE_DAY
+    
+    def get_latest (self, count) :
+        """
+            Uses _iter_backwards + _get_logfile_date to read the yield the given lines from as many logfiles as needed
+        """
+        
+        # iterate backwards from now
+        day_iter = self._iter_backwards()
+
+        # number of files read
+        files = 0
+
+        # only read up to 100 files or so
+        MAX_FILES = 100
+        
+        # loop until done
+        while count > 0 :
+            logfile = None
+
+            try :
+                # get next logfile
+                files += 1
+                
+                # open
+                logfile = self._get_logfile_date(day_iter.next())
+            
+            except IOError, e :
+                # skip nonexistant days if we haven't found any logs yet
+                if e.errno != errno.ENOENT :
+                    raise
+
+                if files > MAX_FILES :
+                    raise Exception("No recent logfiles found")
+                
+                else :
+                    # skip to next day
+                    continue
+
+            # yield lines
+            for line in logfile.get_latest(count) :
+                # yield while we still need to, otherwise, stop
+                if count > 0 :
+                    # decrement
+                    count -= 1
+ 
+                    yield line
+            
+                else :
+                    break
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/logs/openttd	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,1 @@
+/home/terom/backups/zapotek-irclogs/#openttd
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/logs/tycoon	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,1 @@
+/home/terom/backups/zapotek-irclogs/#tycoon
\ No newline at end of file
--- a/sites/irclogs.qmsk.net/__init__.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-"""
-    The irclogs.qmsk.net site is an IRC log browser
-"""
-
-# the URL mapper
-import urls
-
-# our RequestHandler
-handler = urls.mapper
-
--- a/sites/irclogs.qmsk.net/channels.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-"""
-    Our list of LogChannels
-"""
-
-import pytz
-
-# for relpath
-import os.path
-
-from log_channel import LogChannel
-from log_source import LogDirectory
-
-relpath = lambda path : os.path.join(os.path.dirname(__file__), path)
-
-class ChannelList (object) :
-    """
-        The list of channels, and related methods
-    """
-    
-    # the statically defined channel list
-    CHANNELS = {
-        'tycoon':   LogChannel('tycoon', "OFTC", "#tycoon", 
-            LogDirectory(relpath('logs/tycoon'), pytz.timezone('Europe/Helsinki'))
-        ),
-        'openttd':   LogChannel('openttd', "OFTC", "#openttd", 
-            LogDirectory(relpath('logs/openttd'), pytz.timezone('Europe/Helsinki'))
-        ),
-    }
-
-    def __init__ (self, channels) :
-        """
-            Initialize with the given channel dict
-        """
-
-        self.channels = channels
-
-    def lookup (self, channel_name) :
-        """
-            Looks up the LogChannel for the given name
-        """
-
-        return self.channels[channel_name]
-
-    def __iter__ (self) :
-        """
-            Iterate over our defined LogChannel objects
-        """
-
-        return self.channels.itervalues()
-
-# the global singletone ChannelList...
-channel_list = ChannelList(ChannelList.CHANNELS)
-
--- a/sites/irclogs.qmsk.net/handlers.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-"""
-    Our URL action handlers
-"""
-
-from lib import http, template
-
-import urls, channels
-
-# load templates from here
-templates = template.TemplateLoader("sites/irclogs.qmsk.net/templates",
-    urls            = urls,
-    channel_list    = channels.channel_list,
-)
-
-def index (request) :
-    """
-        The topmost index page, display a list of available channels, perhaps some general stats
-    """
-    
-    return templates.render_to_response("index",
-        req             = request,
-    )
-
-def channel_select (request, channel) :
-    """
-        Redirect to the appropriate channel_view
-    """
-   
-    return http.Redirect(urls.channel_view.build(request, channel=channel.id))
-
-def channel_view (request, channel, count) :
-    """
-        The main channel view page, display the most important info, and all requisite links
-    """
-
-    if count == 'all' :
-        xxx
-
-    else :
-        count = int(count)
-
-    return templates.render_to_response("channel",
-        req             = request,
-        channel         = channel,
-        count           = count,
-        lines           = channel.source.get_latest(count),
-    )
-
-    pass
-
-def channel_last (request, channel, count, format) :
-    """
-        Display the last x lines of channel messages in various formats
-    """
-
-    if format == 'txt' :
-        return http.Response('\n'.join(channel.source.get_latest(count)), 'text/plain')
-    
-    else :
-        raise http.ResponseError("Unknown filetype %r" % format)
-
-def channel_search (request, channel) :
-    """
-        Display the search form for the channel for GET, or do the search for POST
-    """
-
-    pass
-
--- a/sites/irclogs.qmsk.net/log_channel.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-"""
-    A channel represents a series of log events, stored in some log source
-"""
-
-class LogChannel (object) :
-    """
-        A single IRC channel, logged to some specific place
-    """
-
-    def __init__ (self, id, network, name, source) :
-        """
-            Initialize this channel from the given identifier key, network name, channel name, and LogSource
-        """
-        
-        self.id = id
-        self.network = network
-        self.name = name
-        self.source = source
-    
-    @property
-    def title (self) :
-        """
-            Title is 'Network - #channel'
-        """
-
-        return "%s - %s" % (self.network, self.name)
-
--- a/sites/irclogs.qmsk.net/log_event.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-"""
-    An IRC logfile consists of a series of events, a.k.a. "lines"
-"""
-
-class LogEvent (object) :
-    """
-        An event on some specific channel
-    """
-
-    # the event ype
-    type = None
-
-    # the UTC timestamp of the event
-    timestamp = None
-
-    # the event source
-    source = None
-
-    # associated data (message, etc)
-    data = None
-
--- a/sites/irclogs.qmsk.net/log_source.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,231 +0,0 @@
-"""
-    A source of IRC log files
-"""
-
-import codecs
-from datetime import date, datetime, timedelta
-import pytz
-
-# for SEEK_*, errno
-import os, errno
-
-class LogSource (object) :
-    """
-        A collection of IRC logs for a specific target in some format. Provides the possibility to read specific events
-    """
-    
-    def get_latest (self, count) :
-        """
-            Yield the latest events, up to `count` of them.
-        """
-
-        abstract
-
-class LogFile (LogSource) :
-    """
-        A file containing LogEvents
-    """
-
-    def __init__ (self, path, charset='utf-8', sep='\n') :
-        """
-            Open the file at the given path, which contains data of the given codec, as lines separated by the given separator
-        """
-        
-        # store
-        self.path = path
-        self.charset = charset
-        self.sep = sep
-
-        # open
-        self.file = codecs.open(path, 'r', charset)
-    
-    def __iter__ (self) :
-        """
-            Yields a series of lines, as read from the top of the file
-        """
-        
-        # seek to beginning
-        self.file.seek(0)
-
-        # iterate over lines
-        return iter(self.file)
-    
-    def get_latest (self, count) :
-        """
-            Returns up to <count> lines from the end of the file, or less, if the file doesn't contain that many lines
-        """
-
-        # the list of lines
-        lines = []
-
-        # seek to end of file
-        self.file.seek(0, os.SEEK_END)
-
-        # read offset
-        # XXX; why -2 ?
-        size = offset = self.file.tell() - 2
-
-        # use this blocksize
-        BLOCKSIZE = 1024
-
-        # trailing data
-        buf = ''
-
-        # read a block at a time, backwards
-        while  count > 0 and offset >= 0:
-            # update offset back one block
-            offset -= BLOCKSIZE
-
-            # normalize to zero
-            if offset < 0 :
-                offset = 0
-
-            # seek to offset
-            self.file.seek(offset)
-
-            # add the new block to our buffer
-            read_buf = self.file.read(BLOCKSIZE)
-
-            # XXX: trim off extra...
-            if len(read_buf) > BLOCKSIZE :
-                read_buf = read_buf[:BLOCKSIZE]
-
-            # make sure we got the right amount of data
-            assert len(read_buf) == BLOCKSIZE, "read(%d) @ %d/%d -> %d" % (BLOCKSIZE, offset, size, len(read_buf))
-
-            # add in our previous buf
-            buf = read_buf + buf
-            
-            # split out lines
-            buf_lines = buf.split(self.sep)
-
-            # keep the first one as our buffer, as it's incomplete
-            buf = buf_lines[0]
-
-            # add up to count lines to our lines buffer
-            lines = buf_lines[-min(count, len(buf_lines) - 1):] + lines
-
-            # update count
-            count -= (len(buf_lines) - 1)
-
-        # return the line list
-        return lines
-
-class LogDirectory (LogSource) :
-    """
-        A directory containing a series of timestamped LogFiles
-    """
-
-    def __init__ (self, path, tz, charset='utf-8', filename_fmt='%Y-%m-%d') :
-        """
-            Load the logfiles at the given path.
-            
-            The files contain data in the given charset, and are named according the the date in the given timezone and
-            date format.
-        """
-
-        # store
-        self.path = path
-        self.tz = tz
-        self.charset = charset
-        self.filename_fmt = filename_fmt
-
-    def _get_logfile_datetime (self, dt) :
-        """
-            Get the logfile corresponding to the given datetime
-        """
-
-        # convert to target timezone
-        dtz = dt.astimezone(self.tz)
-        
-        # convert to date and use that
-        return self._get_logfile_date(dtz.date())
-
-    def _get_logfile_date (self, d) :
-        """
-            Get the logfile corresponding to the given naive date in our timezone
-        """
-
-        # format filename
-        filename = d.strftime(self.filename_fmt)
-
-        # build path
-        path = os.path.join(self.path, filename)
-
-        # return the LogFile
-        return LogFile(path, self.charset)
-    
-    def _iter_backwards (self, dt=None) :
-        """
-            Yields an infinite series of naive date objects in our timezone, iterating backwards in time starting at the
-            given *datetime*, or the the current date, if none given
-        """
-        
-        # default to now
-        if not dt :
-            dt = datetime.now(pytz.utc)
-        
-        # convert to target timezone
-        dtz = dt.astimezone(self.tz)
-
-        # our timedelta
-        ONE_DAY = timedelta(1)
-        
-        # iterate unto infinity
-        while True :
-            # yield
-            yield dtz.date()
-            
-            # one day sdrawkcab
-            dtz -= ONE_DAY
-    
-    def get_latest (self, count) :
-        """
-            Uses _iter_backwards + _get_logfile_date to read the yield the given lines from as many logfiles as needed
-        """
-        
-        # iterate backwards from now
-        day_iter = self._iter_backwards()
-
-        # number of files read
-        files = 0
-
-        # only read up to 100 files or so
-        MAX_FILES = 100
-        
-        # loop until done
-        while count > 0 :
-            logfile = None
-
-            try :
-                # get next logfile
-                files += 1
-                
-                # open
-                logfile = self._get_logfile_date(day_iter.next())
-            
-            except IOError, e :
-                # skip nonexistant days if we haven't found any logs yet
-                if e.errno != errno.ENOENT :
-                    raise
-
-                if files > MAX_FILES :
-                    raise Exception("No recent logfiles found")
-                
-                else :
-                    # skip to next day
-                    continue
-
-            # yield lines
-            for line in logfile.get_latest(count) :
-                # yield while we still need to, otherwise, stop
-                if count > 0 :
-                    # decrement
-                    count -= 1
- 
-                    yield line
-            
-                else :
-                    break
-
-
--- a/sites/irclogs.qmsk.net/logs/openttd	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-/home/terom/backups/zapotek-irclogs/#openttd
\ No newline at end of file
--- a/sites/irclogs.qmsk.net/logs/tycoon	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-/home/terom/backups/zapotek-irclogs/#tycoon
\ No newline at end of file
--- a/sites/irclogs.qmsk.net/templates/channel.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-<%inherit file="layout.tmpl" />
-
-<%def name="menu()">
-<ul>
-    <li><a href="${urls.index.build(req)}">Home</a></li>
-
-    <li>
-        <form action="${urls.channel_select.build(req)}" method="GET">
-            <label for="channel">Channel:</label>
-
-            <select name="channel">
-            % for ch in channel_list :
-                <option value="${ch.id}"${' selected="selected"' if ch == channel else ''}>${ch.title}</option>
-            % endfor
-            </select><input type="submit" value="Go &raquo;" />
-        </form>
-    </li>
-
-    <li>
-        <form action="" method="GET">
-            View last
-
-            <select name="count">
-            % for cc in (10, 20, 50, 100, 'all') :
-                <option${' selected="selected"' if cc == count else ''}>${cc}</option>
-            % endfor
-            </select>
-
-            lines: <input type="submit" value="Go &raquo;" />
-        </form>
-    </li>
-
-    <li><a href="#">Browse by Date</a></li>
-
-    <li><a href="#">Search</a></li>
-
-    <li><a href="#">[RSS]</a></li>
-</ul>
-</%def>
-
-<h1>${channel.title} &raquo; Last ${count} lines</h1>
-
-<pre>
-% for line in lines :
-${line}
-% endfor
-</pre>
-
--- a/sites/irclogs.qmsk.net/templates/index.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<%inherit file="layout.tmpl" />
-
-<h1>Available Channels</h1>
-<ul>
-% for channel in channel_list :
-    <li><a href="${urls.channel_view.build(req, channel=channel.id)}">${channel.title}</a></li>
-% endfor
-</ul>
-
--- a/sites/irclogs.qmsk.net/templates/layout.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<%def name="menu()">
-
-</%def>
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>irclogs.qmsk.net ${('::' + channel.title) if channel else ''}</title>
-        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/irclogs.css" />
-    </head>
-    <body>
-        <div id="menu">
-            ${self.menu()}
-        </div>
-
-        <div id="content">
-            ${next.body()}
-        </div>
-
-        <div id="footer">
-            <div id="footer-left">
-            </div>
-            
-            <div id="footer-center">
-                <!-- ${h.validation_notice(req.site_host)} -->
-            </div>
-        </div>
-    </body>
-</html>
-
--- a/sites/irclogs.qmsk.net/urls.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-
-"""
-    URL mapping for the irclogs.qmsk.net site
-"""
-
-# urltree stuff
-from lib.urltree import URLConfig, URL, URLTree
-
-# our own handlers
-import handlers
-
-# for types
-import channels 
-
-# our URLConfig
-url_config = URLConfig(
-    type_dict   = { 
-        # lookup LogChannel
-        'cid': channels.channel_list.lookup 
-    }
-)
-
-# shortcut for building an URL with our url_config
-def url (*args, **kwargs) :
-    return URL(url_config, *args, **kwargs)
-
-# urls
-index           = url('/',                                                              handlers.index                  )
-channel_select  = url('/channel_select/?channel:cid',                                   handlers.channel_select         )
-channel_view    = url('/channels/{channel:cid}/?count:str=10',                          handlers.channel_view           )
-channel_last    = url('/channels/{channel:cid}/last/{count:int=100}/{format=html}',     handlers.channel_last           )
-channel_search  = url('/channels/{channel:cid}/search',                                 handlers.channel_search         )
-
-# mapper
-mapper = URLTree(
-    [index, channel_select, channel_view, channel_last, channel_search]
-)
-
--- a/sites/www.qmsk.net/__init__.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-"""
-    The www.qmsk.net site is just a simple site with a filesystem-based URL mapping
-"""
-
-from lib import template
-import lookup
-
-# global mapper attribute
-handler = lookup.PageMapper("sites/www.qmsk.net/pages", template=template.TemplateLoader.load("sites/www.qmsk.net/templates/layout.tmpl"))
-
--- a/sites/www.qmsk.net/lookup.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-
-import os, os.path
-
-from lib import http, template, map
-
-import page, page_tree
-
-class PageMapper (map.Mapper) :
-    """
-        Translates requests to handlers based on a filesystem directory containing various kinds of files
-    """
-
-    # list of page handlers, by type
-    PAGE_TYPES = [
-        ('html',                    page.HTMLPage           ),
-        (template.TEMPLATE_EXT,     page.TemplatePage       ),
-    ]
-
-    def __init__ (self, path, template) :
-        """
-            Create, path is where the pages are stored. The list of pages is loaded from $path/list
-        """
-        
-        # store
-        self.path = path 
-        self.template = template
-
-        # load the page tree
-        self.tree = page_tree.PageTree(path + '/list')
-    
-    def _lookup_page_type (self, url, path, filename, basename, extension, tail) :
-        """
-            We found the file that we looked for, now get the correct type
-        """
-
-        # find appropriate handler
-        for handler_ext, type in self.PAGE_TYPES :
-            # match against file extension?
-            if handler_ext == extension :
-                # found handler, return instance
-                return type(self, url, path, basename, tail)
-
-        # no handler found
-        raise PageError("No handler found for page %r of type %r" % (url, extension))
-
-    def _lookup_page (self, name) :
-        """
-            Look up and return a Page object for the given page, or raise an error
-        """
-
-        # inital path
-        path = self.path
-        url_segments = []
-
-        # name segments
-        segments = name.split('/')
-
-        # iterate through the parts of the page segments
-        while True :
-            segment = None
-
-            # pop segment
-            if segments :
-                segment = segments.pop(0)
-
-                url_segments.append(segment)
-
-            # translate empty -> index
-            if not segment :
-                segment = 'index'
-
-            # look for it in the dir
-            for filename in os.listdir(path) :
-                # build full file path
-                file_path = os.path.join(path, filename)
-
-                # stat, recurse into subdirectory?
-                if os.path.isdir(file_path) and filename == segment :
-                    # use new dir
-                    path = file_path
-
-                    # break for-loop to look at next segment
-                    break
-     
-                # split into basename + extension
-                basename, extension = os.path.splitext(filename)
-
-                # ...remove that dot
-                extension = extension.lstrip('.')
-                
-                # match against requested page name?
-                if basename == segment :
-                    # found the file we wanted
-                    return self._lookup_page_type('/'.join(url_segments), file_path, filename, basename, extension, '/'.join(segments))
-                
-                else :
-                    # inspect next file in dir
-                    continue
-
-            else :
-                # did not find any dir or file, break out of while loop
-                break
-
-        # did not find the filename we were looking for in os.listdir
-        raise map.MapperError(name)
-
-    def handle_request (self, request) :
-        """
-            Looks up the appropriate Page, and then renders it
-        """
-
-        # determine the page name
-        page_name = request.get_page_name()
-
-        # get the page handler
-        page = self._lookup_page(page_name)
-        
-        # pass on
-        return page.handle_request(request)
-
--- a/sites/www.qmsk.net/menu.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-"""
-    Handling the list of available pages
-"""
-
-class Menu (object) :
-    """
-        Contains info needed to render the menu
-    """
-
-    def __init__ (self, fs, page) :
-        """
-            Gather the menu information for the given page, as part of the given FilesystemMapper
-        """
-
-        # the selected page
-        self.page = fs.tree.get_page(page.url)
-
-        # the selected pagen's inheritance
-        self.ancestry = self.page.get_ancestry() if self.page else []
-        
-        # list of menu items == root children, since we always show the full menu...
-        self.items = fs.tree.root.children
-    
--- a/sites/www.qmsk.net/page.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,138 +0,0 @@
-
-"""
-    Handling page requests
-"""
-
-# for filesystem ops
-import os, os.path
-import time
-
-from lib import http, handler, template, config
-
-import menu
-
-class PageError (http.ResponseError) :
-    """
-        Error looking up/handling a page
-    """
-
-    pass
-
-# XXX: should inherit from PageInfo
-class Page (handler.RequestHandler) :
-    """
-        This object represents the information about our attempt to render some specific page
-    """
-
-    def __init__ (self, fs, url, path, basename, url_tail, charset='utf8') :
-        """
-            Initialize the page at the given location
-            
-            @param fs the FilesysteMapper
-            @param url the URL leading to this page
-            @param path the filesystem path to this page's file
-            @param basename the filesystem name of this page's file, without the file extension
-            @param url_trail trailing URL for this page
-            @param charset file charset
-        """
-        
-        # store
-        self.fs = fs
-        self.url = url
-        self.path = path
-        self.basename = basename
-        self.url_tail = url_tail
-        self.charset = charset
-
-        # sub-init
-        self._init()
-
-    def _init (self) :
-        """
-            Do initial data loading, etc
-        """
-        
-        pass
-
-    @property
-    def title (self) :
-        """
-            Return the page's title
-
-            Defaults to the retreiving the page title from page_list, or basename in Titlecase.
-        """
-        
-        # lookup in PageTree
-        page_info = self.fs.tree.get_page(self.url)
-        
-        # fallback to titlecase
-        if page_info :
-            title = page_info.title
-
-        else :
-            title = self.basename.title()
-
-        return title
-    
-    @property
-    def content (self) :
-        """
-            Return the page content as a string
-        """
-
-        abstract
-    
-    @property
-    def modified (self) :
-        """
-            Returns the page modification timestamp
-        """
-        
-        # stat
-        timestamp = os.stat(self.path).st_mtime
-
-        return time.strftime(config.DATETIME_FMT, time.gmtime(timestamp))
-    
-    def handle_request (self, request) :
-        """
-            Renders the fs's layout template with this page + menu
-        """
-
-        # render the template
-        response_data = template.TemplateLoader.render_template(self.fs.template,
-            req             = request,
-            page            = self,
-            menu            = menu.Menu(self.fs, self),
-        )
-        
-        # return the response
-        return http.Response(response_data)
-
-class HTMLPage (Page) :
-    """
-        A simple .html page that's just passed through directly
-    """
-
-    @property
-    def content (self) :
-        """
-            Opens the .html file, reads and returns contents
-        """
-
-        return open(self.path, 'rb').read().decode(self.charset)
-
-class TemplatePage (Page) :
-    """
-        A template that's rendered using our template library
-    """
-    
-    @property
-    def content (self) :
-        """
-            Loads the .tmpl file, and renders it
-        """
-
-        return template.TemplateLoader.render_file(self.path,
-            page_tree   = self.fs.tree,
-        )
-
--- a/sites/www.qmsk.net/page_tree.py	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,218 +0,0 @@
-"""
-    Implements the tree containing pages and their metadata
-"""
-
-from lib import tree_parse
-
-class PageTreeError (Exception) :
-    """
-        Error parsing/loading the page tree
-    """
-
-    pass
-
-class PageInfo (object) :
-    """
-        Contains metainformation about a page
-    """
-
-    def __init__ (self, parent, name, title, children=None) :
-        """
-            Initialize, children defaults to empty list
-        """
-
-        # store
-        self.parent = parent
-        self.name = name
-        self.title = title
-        self.children = children if children else []
-
-        # no url get
-        self._url = None
-    
-    def set_parent (self, parent) :
-        """
-            Set a parent where non was set before
-        """
-
-        assert self.parent is None
-
-        self.parent = parent
-
-    def add_child (self, child) :
-        """
-            Add a PageInfo child
-        """
-
-        self.children.append(child)
-    
-    def get_child (self, name) :
-        """
-            Look up a child by name, returning None if not found
-        """
-
-        return dict((c.name, c) for c in self.children).get(name)
-
-    def get_ancestry (self) :
-        """
-            Returns a list of this page's parents and the page itself, but not root
-        """
-        
-        # collect in reverse order
-        ancestry = []
-        
-        # starting from self
-        item = self
-        
-        # add all items, but not root
-        while item and item.parent :
-            ancestry.append(item)
-
-            item = item.parent
-
-        # reverse
-        ancestry.reverse()
-        
-        # done
-        return ancestry
-
-    @property
-    def url (self) :
-        """
-            Build this page's URL
-        """
-
-        # cached?
-        if self._url :
-            return self._url
-
-        segments = [item.name for item in self.get_ancestry()]
-        
-        # add empty segment if dir
-        if self.children :
-            segments.append('')
-        
-        # join
-        url = '/'.join(segments)
-        
-        # cache
-        self._url = url
-        
-        # done
-        return url
-
-class PageTree (object) :
-    """
-        The tree of pages, rooted at .root.
-
-        Use load_page_tree to initialize the global page_tree instance, and then use that
-    """
-
-    def __init__ (self, path) :
-        """
-            Loads the PageTree root from the given file
-        """
-        
-        # store
-        self.path = path
-        
-        # load
-        self._load(path)
-
-    def _load (self, path) :
-        """
-            Processes the lines in the given file
-        """
-        
-        # parse tree
-        tree = tree_parse.parse(path, ':')
-
-        if not tree :
-            raise PageTreeError("No root node found")
-
-        def _create_node (parent, item) :
-            """
-                Creates and returns a PageInfo from the given parent node and (line_number, line, children) tuple item
-            """
-
-            # unpack
-            line_number, line, children = item
-            
-            # parse line
-            url = title = None
-            
-            try :
-                url, title = line.split(':')
-
-            except :
-                raise PageTreeError("Invalid line: %s:%d: %r" % (path, line_number, line))
-
-            # remove whitespace
-            url = url.strip()
-            title = title.strip()
-            
-            # create PageInfo
-            node = PageInfo(parent, url, title)
-            
-            # set node children
-            node.children = [
-                _create_node(node, child_item) for child_item in children
-            ]
-
-            # return
-            return node
-        
-        # translate
-        self.root = _create_node(None, tree)
-            
-        # *evil cackle*
-        self.root.children.insert(0, self.root)
-        
-    def get_page (self, url) :
-        """
-            Lookup the given page URL, and return the matching PageInfo object, or None, if not found
-        """
-        
-        # start from root
-        node = self.root
-        
-        # traverse the object tree
-        for segment in url.split('/') :
-            if segment :
-                node = node.get_child(segment)
-
-            if not node :
-                return None
-        
-        # return
-        return node
-    
-    def get_siblings (self, url) :
-        """
-            Get the list of siblings for the given url, including the given page itself
-        """
-        
-        # look up the page itself
-        page = self.get_page(url)
-        
-        # specialcase root/unknown node
-        if page and page.parent :
-            return page.parent.children
-
-        else :
-            return self.root.children
-    
-    def dump (self) :
-        """
-            Returns a string representation of the tree
-        """
-        
-        def _print_node (indent, node) :
-            return '\n'.join('%s%s' % (' '*indent, line) for line in [
-                "%-15s : %s" % (node.name, node.title)
-            ] + [
-                _print_node(indent + 4, child) for child in node.children if child != node
-            ])
-
-        return _print_node(0, self.root)
-
--- a/sites/www.qmsk.net/pages/about.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-<h1>About me</h1>
-
-<p>
-    My name is Tero Marttila. I'm a student at <a href="http://www.tkk.fi/">Helsinki University of Techonology</a>, living in Otaniemi.
-</p>
-
-<h1>Contact</h1>
-
-<p>
-    I am best reached via IRC, under the nickname of <em>SpComb</em> on any of IRCNet, freenode, OFTC.
-</p>
-
-<p>
-    Alternatively, I'm also reached via email: <em>terom@fixme.fi</em>
-</p>
--- a/sites/www.qmsk.net/pages/debug.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-
-<pre>
-${page_tree.dump() | h}
-</pre>
-
-<dl>
-    <lh>List of request env variables</lh>
-
-% for key, val in request.env.iteritems() :
-    <dt>${key}</dt> <dd>${val}</dd>
-% endfor
-</dl>
--- a/sites/www.qmsk.net/pages/index.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-<h1></h1>
-
-<div style="font-size: small">
-    <h2>What's <q>qmsk</q>?</h2>
-
-    <p>
-        I'm not sure, but it doesn't have any vowels.
-    </p>
-</div>    
--- a/sites/www.qmsk.net/pages/list	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-
-            : Index
-projects        : Projects
-    qmsk.net        : qmsk.net
-    evsql           : evsql
-    nr-ttd          : Nr-TTD
-    kg              : Kišna Glista
-about           : About
-
--- a/sites/www.qmsk.net/pages/projects/evsql.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-<h1>evsql</h1>
-    <p>Evsql is a C-language SQL library designed for use with libevent, and primarily PostgreSQL's libpq.</p>
-
-    <p>For more information, read the <a href="http://qmsk.net/static/evsql-doxygen/">Doxygen documentation</a>, or look at the <a href="http://hg.qmsk.net/evfuse.hg/file/tip/src/">Mercurial repo</a> (currently part of evfuse).</p>
-
--- a/sites/www.qmsk.net/pages/projects/foo.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-The foo project
--- a/sites/www.qmsk.net/pages/projects/index.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-<h1>Projects</h1>
-
-    <p>I've worked on a variety of projects over the years, leaving them in various stages of completion.</p>
-
-    <p>Some of the perhaps more noteworthy ones have their own project pages here (see menu to the left).</p>
-
-    <h2>Nr-TTD</h2>
-        
-        <p>A bunch of Microsoft batch-file code that functions as an installer for <a href="http://www.ttdpatch.net/">TTD Patch</a>, a third-party binary patch for an <a href="http://en.wikipedia.org/wiki/Transport_Tycoon">old game</a>.</p>
-
-    <h2>Kišna Glista</h2>
-
-        <p>An open-source Liero clone (the 985th one).</p>
-
--- a/sites/www.qmsk.net/pages/projects/kg.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<h1>Kišna Glista</h1>
-
-    <p><em>Kišna Glista</em> is a (yet another) Liero clone written in C++, with GPL source code, running on Linux platforms.</p>
-
-    <p>Visit the <a href="http://kg.qmsk.net/">project website</a> for more information</p>
-
--- a/sites/www.qmsk.net/pages/projects/nr-ttd.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-<h1>Summary</h1>
-
-    <p><em>nr-TTD</em> (originally <q>No Registry - TTD</q>) is a batch file installer for TTD. It can download and install <a href="http://en.wikipedia.org/wiki/Transport_Tycoon#Transport_Tycoon_Deluxe">TTD</a>, <a href="http://ttdpatch.net/">TTD Patch</a>, <a href="http://www.tt-forums.net/viewtopic.php?t=26352">TTD Patch Configurator</a> as well as a (poor) selection of newgrf files.</p>
-
-    <p>Please note that the project is not under any form of active development anymore.</p>
-
-<h1>Install</h1>
-
-<ol>
-    <li>Download <a href="http://zapotek.paivola.fi/~terom/nr-ttd/get/nr-ttd_latest.zip">the latest version</a></li>
-    <li>Extract/unzip the file into the folder you want to install TTD into</li>
-    <li>Double-click <tt>install.bat</tt></li>
-    <li>Answer the questions, make the choices</li>
-    <li>Done</li>
-</ol>
-
-<h1>Playing</h1>
-
-    <p>Usually, you can play TTD by simply running <tt>ttdpatchw.exe</tt> in the folder that you unzipped Nr-TTD to. For added convenience you can create a shortcut to the <tt>ttdpatchw.exe</tt> file:</p>
-
-    <ol>
-        <li>Right-click <tt>ttdpatchw.exe</tt></li>
-        <li>From the <q>Send To</q> menu, select <q>Send to Desktop (create shortcut)</q></li>
-        <li>Rename the resulting shortcut on your Desktop (right click -&gt; Rename) to, say, <q>TTD Patch</q></li>
-    </ol>
-
-    <p>Then simply run this shortcut to play TTD.</p>
-
-<h2>Registry errors</h2>
-
-    <p>In case you get registry errors when running <tt>ttdpatchw.exe</tt> (as may be the case when installing on top of previous installs of TTD), you should simply use <tt>run-forcenoreg.bat</tt> instead of <tt>ttdpatchw.exe</tt>. This instructs TTD Patch to ignore the windows registry, which should solve all registry problems.</p>
-
-<h1>Configuring</h1>
-
-    <p>Nr-TTD also installs the <a href="http://www.tt-forums.net/viewtopic.php?t=26352">TTD Patch Configurator</a>, which can be used to change your TTD Patch configuration. This enables you to tweak your gameplay in many different ways, and it is highly recommended that you look through the available settings. Refer to the <a href="http://wiki.ttdpatch.net/tiki-index.php?page=Manual">TTD Patch Manual</a> for further information on the induvidual switches.</p>
-
-    <p>Simply run <tt>ttdpc.exe</tt> from the TTD folder.</p>
-
-<h1>Requirements</h1>
-    <p>Nr-TTD uses fairly new batch file syntax, and hence probably only works on Windows XP SP2 or later. Space requirements are about 20MB or so. Total download size is some 9 MB without newgrfs, and then a couple hundred KB per newgrf.</p>
-
-    <p>TTD Patch should work on Windows Vista as well, provided you have a <a href="http://nightly.ttdpatch.net/latest/">recent enough version</a> (grab the file called TTDPatch-nightly-rXXXX-win32.zip), which you may need to download yourself.</p>
-
-<h1>Problems? Suggestions?</h1>
-
-    <p>Not working? Cryptic error messages? Turned your default language into swahili? Unfortunately, there is nobody actively fixing bugs, but you can try asking for help.</p>
-
-    <p>Discuss Nr-TTD using the appropriate <a href="http://www.tt-forums.net/viewtopic.php?t=26742">TT-Forums topic</a>.</p>
-
--- a/sites/www.qmsk.net/pages/projects/qmsk.net.html	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<h1>qmsk.net</h1>
-    <p>This is the code that runs this website itself.</p>
-
-    <p>It's a relatively simple WSGI-based CGI script that renders various kinds of pages from the filesystem, along with a nice menu.</p>
-
-    <p><a href="http://hg.qmsk.net/qmsk.net">Mercurial repo</a></p>
--- a/sites/www.qmsk.net/templates/layout.tmpl	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-
-<%def name="render_menu(open_page, page, items, ancestry)">
-<ul>
-% for pi in items :
-    <li>
-        <a href="${req.page_prefix}/${pi.url}"${' class="selected-page"' if pi == open_page else ''}>${pi.title} ${'&raquo;' if pi.children and pi.parent else ''}</a>
-    % if pi in ancestry and pi.children and pi.parent :
-        ${render_menu(page, pi, pi.children, ancestry)}
-    % endif
-    </li>
-% endfor
-</ul>
-</%def>
-
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-    <head>
-        <title>qmsk.net ${' :: ' + h.breadcrumb(menu.ancestry, links=False) if menu.ancestry else ''}</title>
-        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/style.css" />
-    </head>
-    <body>
-            <div id="header">
-                <a href="${req.page_prefix}/">QMSK.NET</a>
-            </div>
-            
-            <div id="container">
-                <div id="nav">
-                    ${render_menu(menu.page, menu.page, menu.items, menu.ancestry)}
-                </div>
-
-                <div id="content">
-                    <div id="breadcrumb">
-                        <!-- ${h.breadcrumb(menu.ancestry)} -->
-                    </div>
-                    ${page.content}
-                </div>
-                
-            </div>
-
-            <div id="footer">
-                <div id="footer-right">
-                    Page Modified ${page.modified} <br/>
-                    Current time ${h.now()}
-                </div>
-               
-                <div id="footer-left">
-                    &copy; ${h.copyright_year()} Tero Marttila
-                </div>
-                
-                <div id="footer-center">
-                    ${h.validation_notice(req.site_host)}
-                </div>
-            </div>
-    </body>
-</html>
-
Binary file static/link.png has changed
--- a/static/style.css	Sun Feb 08 03:13:11 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +0,0 @@
-
-/*
- * Three main areas:
- *  header
- *  nav
- *  content
- *
- * Border layout:
- *      Header (bottom)
- *      Content (left)
- */
-
-/*
- * Global styles
- */
-body {
-    padding: 0px;
-    margin: 0px;
-
-    background-color: #ffffff;
-    color: #000000;
-}
-
-/*
- * Top header
- */
-div#header {
-    padding: 30px;
-    
-    font-size: 48pt;
-    font-weight: bold;
-
-    border-bottom: 1px dashed #a5a5a5;
-}
-
-div#header a:hover {
-    text-decoration: none;
-}
-
-/*
- * Container for layout items
- */
- #container {
-    overflow: hidden;
-}
-
-/*
- * Main navigation menu
- */
-#nav { 
-    float: left;
-    
-    padding-bottom: 2000px;
-    margin-bottom: -2000px;
-    
-    border-right: 1px dashed #a5a5a5;
-}
-
-#nav ul {
-    margin: 0px;
-    padding: 0px;
-
-    list-style: none;
-
-    width: 180px;
-
-    padding-top: 25px;
-}
-
-#nav ul ul {
-    padding-top: 0px;
-    border-left: 15px solid #d0d0d0;
-}
-
-#nav li a {
-    display: block;
-    
-    padding: 10px;
-    padding-left: 20px;
-}
-
-#nav li a:hover {
-    background-color: #d0d0d0;
-    text-decoration: none;
-}
-
-#nav li a.selected-page {
-    border-left: 5px solid black;
-    padding-left: 15px;
-}
-
-/*
- * Content
- */
-div#content {
-    margin-left: 180px;
-    padding: 25px;
-
-    padding-right: 50px;
-}
-
-div#breadcrumb {
-    font-size: x-small;
-}
-
-/*
- * Footer
- */
-div#footer {
-    padding: 10px;
-
-    border-top: 1px dashed #a5a5a5;
-
-    font-size: x-small;
-    font-style: italic;
-}
-
-div#footer-left {
-    float: left;
-}
-
-div#footer-right {
-    float: right;
-    text-align: right;
-}
-
-div#footer-center {
-    text-align: center;
-}
-
-/*
- * General styles
- */
-a {
-    color: black;
-    text-decoration: none;
-    font-weight: bold;
-}
-
-a:hover {
-    text-decoration: underline;
-}
-
-h1 {
-    font-size: x-large;
-}
-
-h2 {
-    font-size: large;
-}
-
-div#content p {
-    margin-left: 0.5em;
-}
-
-#content li {
-    padding: 2px;
-}
-
-#content a {
-    padding-right: 13px;
-    
-    background: transparent url(/static/link.png) no-repeat center right;
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/channel.tmpl	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,48 @@
+<%inherit file="layout.tmpl" />
+
+<%def name="menu()">
+<ul>
+    <li><a href="${urls.index.build(req)}">Home</a></li>
+
+    <li>
+        <form action="${urls.channel_select.build(req)}" method="GET">
+            <label for="channel">Channel:</label>
+
+            <select name="channel">
+            % for ch in channel_list :
+                <option value="${ch.id}"${' selected="selected"' if ch == channel else ''}>${ch.title}</option>
+            % endfor
+            </select><input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+
+    <li>
+        <form action="" method="GET">
+            View last
+
+            <select name="count">
+            % for cc in (10, 20, 50, 100, 'all') :
+                <option${' selected="selected"' if cc == count else ''}>${cc}</option>
+            % endfor
+            </select>
+
+            lines: <input type="submit" value="Go &raquo;" />
+        </form>
+    </li>
+
+    <li><a href="#">Browse by Date</a></li>
+
+    <li><a href="#">Search</a></li>
+
+    <li><a href="#">[RSS]</a></li>
+</ul>
+</%def>
+
+<h1>${channel.title} &raquo; Last ${count} lines</h1>
+
+<pre>
+% for line in lines :
+${line}
+% endfor
+</pre>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/index.tmpl	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,9 @@
+<%inherit file="layout.tmpl" />
+
+<h1>Available Channels</h1>
+<ul>
+% for channel in channel_list :
+    <li><a href="${urls.channel_view.build(req, channel=channel.id)}">${channel.title}</a></li>
+% endfor
+</ul>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/layout.tmpl	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<%def name="menu()">
+
+</%def>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+    <head>
+        <title>irclogs.qmsk.net ${('::' + channel.title) if channel else ''}</title>
+        <link rel="Stylesheet" type="text/css" href="${req.site_root}/static/irclogs.css" />
+    </head>
+    <body>
+        <div id="menu">
+            ${self.menu()}
+        </div>
+
+        <div id="content">
+            ${next.body()}
+        </div>
+
+        <div id="footer">
+            <div id="footer-left">
+            </div>
+            
+            <div id="footer-center">
+                <!-- ${h.validation_notice(req.site_host)} -->
+            </div>
+        </div>
+    </body>
+</html>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/urls.py	Sun Feb 08 03:23:25 2009 +0200
@@ -0,0 +1,38 @@
+
+"""
+    URL mapping for the irclogs.qmsk.net site
+"""
+
+# urltree stuff
+from qmsk.web.urltree import URLConfig, URL, URLTree
+
+# our own handlers
+import handlers
+
+# for types
+import channels 
+
+# our URLConfig
+url_config = URLConfig(
+    type_dict   = { 
+        # lookup LogChannel
+        'cid': channels.channel_list.lookup 
+    }
+)
+
+# shortcut for building an URL with our url_config
+def url (*args, **kwargs) :
+    return URL(url_config, *args, **kwargs)
+
+# urls
+index           = url('/',                                                              handlers.index                  )
+channel_select  = url('/channel_select/?channel:cid',                                   handlers.channel_select         )
+channel_view    = url('/channels/{channel:cid}/?count:str=10',                          handlers.channel_view           )
+channel_last    = url('/channels/{channel:cid}/last/{count:int=100}/{format=html}',     handlers.channel_last           )
+channel_search  = url('/channels/{channel:cid}/search',                                 handlers.channel_search         )
+
+# mapper
+mapper = URLTree(
+    [index, channel_select, channel_view, channel_last, channel_search]
+)
+