changeset 0:fa2005f769eb

adding decoupage from http://my-svn.assembla.com/svn/arbez/decoupage r355
author k0s <k0scist@gmail.com>
date Mon, 07 Sep 2009 13:40:22 -0400
parents
children 93ab6d1968bd
files README.txt decoupage.ini decoupage/__init__.py decoupage/decoupage.py decoupage/factory.py decoupage/formatters.py decoupage/templates/index.html example.ini example/bar.txt example/cats/bigcats/lion.txt example/cats/bigcats/tiger.txt example/cats/charlie.txt example/cats/hobbes.txt example/cats/lilly.txt example/fleem/fleem.txt example/fleem/index.html example/fleem/index.ini example/foo.html example/foo.txt setup.py
diffstat 20 files changed, 482 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,57 @@
+decoupage
+=========
+
+what is it?
+-----------
+
+decoupage is a static file server that allows for index pages
+configurable with genshi templates and .ini files.  I mainly wrote it
+because i was tired of using apache for serving my website and
+generating index.html files by hand.
+
+
+how do i use it?
+----------------
+
+Set up a `paste <http://pythonpaste.org>`_ .ini file that specifies the
+directory to serve (``decoupage.directory``) and, optionally, a
+configuration file .ini file (``decoupage.configuraton``) which
+specifies the labels for the files based on directory. An example of a
+`paste <http://pythonpaste.org>`_ .ini file is in
+``decoupage.ini``. Note the ``[app:decoupage]`` section::
+
+    [app:decoupage]
+    paste.app_factory = decoupage.factory:factory
+    decoupage.directory = %(here)s/example
+    decoupage.configuration = %(here)s/example.ini
+
+The labels for files are in ``example.ini``, specified by sections as
+directories::
+
+    [/]
+    foo.txt = a file about cats
+
+    [/cats]
+    lilly.txt = lilly
+    hobbes.txt = a file about Hobbes
+
+You can specify the entire layout from here.  Alternately, you can
+have an ``index.ini`` in a directory which, if present, overrides the
+default configuration.  Such a file is in the ``fleem`` subdirectory
+of ``example``::
+
+    /template = index.html
+    fleem.txt = some fleem for ya
+
+Try it out!  Install decoupage and run ``paster serve decoupage.ini``
+and point your browser to the URL it gives you.
+
+
+how do i do more with decoupage?
+--------------------------------
+
+Since filenames can't start with a ``/`` (just try it!), the
+functionality of decoupage may be extended with ``/`` commands in a
+section.  This is done by adding a setuptools ``entry_point`` to
+``[decoupage.formatters]``.  See the decoupage ``setup.py`` and
+``decoupage.formatters`` for examples.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage.ini	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,23 @@
+#!/usr/bin/env paster
+
+[DEFAULT]
+debug = true
+email_to = k0scist@gmail.com
+smtp_server = localhost
+error_email_from = paste@localhost
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5150
+
+[composite:main]
+use = egg:Paste#urlmap
+/ = decoupage
+
+set debug = false
+
+[app:decoupage]
+paste.app_factory = decoupage.factory:factory
+decoupage.directory = %(here)s/example
+decoupage.configuration = %(here)s/example.ini
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage/__init__.py	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage/decoupage.py	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,193 @@
+"""
+decoupage: a view with webob
+"""
+
+import os
+
+from genshi.builder import Markup
+from genshi.template import TemplateLoader
+from martini.config import ConfigMunger
+from paste.urlparser import StaticURLParser
+from pkg_resources import resource_filename
+from pkg_resources import iter_entry_points
+from webob import Request, Response, exc
+
+class Decoupage(object):
+
+    ### class level variables
+    defaults = { 'auto_reload': 'False',
+                 'configuration': None,
+                 'directory': None, # directory to serve
+                 'cascade': 'True', # whether to cascade configuration
+                 'template': 'index.html', # XXX see below
+                 'template_directories': '' # list of directories to look for templates
+                 }
+
+    def __init__(self, **app_conf):
+
+        # set defaults from app configuration
+        kw = self.app_conf('decoupage', app_conf)
+        for key in self.defaults:
+            setattr(self, key, kw.get(key, self.defaults[key]))
+
+        # configure defaults
+        self.auto_reload = self.auto_reload.lower() == 'true'
+        self.cascade = self.cascade.lower() == 'true'
+        self.directory = self.directory.rstrip(os.path.sep)
+        assert os.path.isdir(self.directory)
+        self.template_directories = self.template_directories.split() # no spaces in directory names, for now
+        assert sum([os.path.isdir(directory) for directory in self.template_directories]) == len(self.template_directories)
+
+        # static file server
+        self.fileserver = StaticURLParser(self.directory)
+        
+        # pluggable index data formatters
+        self.formatters = {}
+        for formatter in iter_entry_points('decoupage.formatters'):
+            try:
+                _formatter = formatter.load()
+                template_dir = resource_filename(formatter.module_name, 'templates')
+                if template_dir not in self.template_directories and os.path.isdir(template_dir):
+                    self.template_directories.append(template_dir)
+            except:
+                continue # XXX should probably raise
+            self.formatters[formatter.name] = _formatter
+        
+        # template loader
+        self.loader = TemplateLoader(self.template_directories, 
+                                     auto_reload=self.auto_reload)
+
+    ### methods dealing with HTTP
+    def __call__(self, environ, start_response):
+        request = Request(environ)
+        filename = request.path_info.strip('/')
+        path = os.path.join(self.directory, filename)
+        if os.path.exists(path):
+            if os.path.isdir(path):
+
+                if not request.path_info.endswith('/'):
+                    raise exc.HTTPMovedPermanently(add_slash=True)
+
+                res = self.get(request)
+                return res(environ, start_response)
+            else:
+                return self.fileserver(environ, start_response)
+        else:
+            raise exc.HTTPNotFound()
+
+    def get_response(self, text, content_type='text/html'):
+        """construct a response to a GET request"""
+        res = Response(content_type=content_type, body=text)
+        return res
+
+    def get(self, request):
+        """
+        return response to a GET requst
+        """
+        # ensure a sane path        
+        path = request.path_info.strip('/')
+        directory = os.path.join(self.directory, path)
+        path = '/%s' % path
+        
+        # get the configuraton
+        conf = self.conf(path)
+
+        # add data for the files
+        files = []
+        for i in os.listdir(directory):
+            files.append({'path' : '%s/%s' % (path.rstrip('/'), i),
+                          'name': i,
+                          'description': conf.get(i.lower(), None)})
+
+        # build data dictionary
+        data = {'path': path, 'files': files, 'request': request}
+
+        # apply formatters
+        # XXX this should be cached if not self.auto_reload
+        if '/formatters' in conf:
+            # ordered list of formatters to be applied first
+            formatters = [ i for i in conf['/formatters'].split()
+                           if i in self.formatters ]
+        else:
+            formatters = []
+        for key in conf:
+            if key.startswith('/'):
+                key = key[1:]
+                if key in self.formatters and key not in formatters:
+                    formatters.append(key)
+        for name in formatters:
+            formatter = self.formatters[name](conf.get('/%s' % name, ''))
+            formatter(request, data)
+        
+        # render the template
+        template = conf.get('/template')
+        if template is None:
+            if 'index.html' in [ f['name'] for f in files ]:
+                template = os.path.join(directory, 'index.html')
+            else:
+                template = self.template
+        if not os.path.isabs(template):
+            template = os.path.join(directory, template)
+        if not os.path.exists(template):
+            template = self.template
+        template = self.loader.load(template)
+        res = template.generate(**data).render('html', doctype='html')
+        return self.get_response(res)
+
+    ### internal methods
+
+    def conf(self, path):
+        """returns configuration dictionary appropriate to a path"""
+
+        directory = os.path.join(self.directory, path.strip('/'))
+        if path.strip('/'):
+            path_tuple = tuple(path.strip('/').split('/'))
+        else:
+            path_tuple = ()
+
+        # return cached configuration
+        if hasattr(self, '_conf') and path_tuple in self._conf:
+            return self._conf[path_tuple]
+
+        conf = {}
+
+        # local configuration
+        ini_path = os.path.join(directory, 'index.ini')
+        if os.path.exists(ini_path):
+            _conf = ConfigMunger(ini_path).dict()
+            if len(_conf) == 1:
+                conf = _conf[_conf.keys()[0]].copy()
+
+        # global configuration
+        if not conf and self.configuration and os.path.exists(self.configuration):
+            conf = ConfigMunger(self.configuration).dict().get('/%s' % path.rstrip('/'), {})
+
+        # cascade configuration
+        if self.cascade and path_tuple:
+            parent_configuration = self.conf('/%s' % '/'.join(path_tuple[:-1]))
+            for key, value in parent_configuration.items():
+                if key.startswith('/') and key not in conf:
+                    conf[key] = value
+
+        # cache configuration
+        if not self.auto_reload:
+            if not hasattr(self, '_conf'):
+                self._conf = {}
+            self._conf[path_tuple] = conf
+
+        return conf
+
+    def fmtrs(self, path):
+        formatters = []
+        for key, value in self.conf(path).items():
+            if key.startswith('/'):
+                key = key[1:]
+                if key in self.formatters:
+                    formatter = self.formatters[key](value)        
+
+
+    def app_conf(self, keystr, app_conf):
+        keystr += '.'
+        return dict([(key.split(keystr, 1)[-1], value)
+                     for key, value in app_conf.items()
+                     if key.startswith(keystr) ])        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage/factory.py	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,8 @@
+from decoupage import Decoupage
+from paste.httpexceptions import HTTPExceptionHandler
+
+def factory(global_conf, **app_conf):
+    """create a webob view and wrap it in middleware"""
+    app = Decoupage(**app_conf)
+    return HTTPExceptionHandler(app)
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage/formatters.py	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,95 @@
+from fnmatch import fnmatch
+
+class FormatterBase(object):
+    """
+    abstract base class if you want to use __init__ methods 
+    in the form of 
+    'arg1, arg2, arg3, kw1=foo, kw2=bar, kw3=baz
+    """
+
+    defaults = {}
+
+    def __init__(self, string):
+        args = [ i.strip() for i in string.split(',') ]
+        for index, arg in enumerate(args):
+            if '=' in arg:
+                break
+        else:
+            self.args = args
+            return
+        self.args = args[:index]
+        self.kw = dict([i.split('=', 1) for i in args[index:]])
+        for key, default in defaults.items():
+            if key not in self.kw:
+                self.kw[key] = default
+
+class Ignore(object):
+    """ignore files of a certain pattern"""
+    
+
+    def __init__(self, ignore):
+        self.match = [ i.strip() for i in ignore.split(',')
+                       if i.strip() ]
+
+    def __call__(self, request, data):
+        _files = []
+        for f in data['files']:
+            for pattern in self.match:
+                if fnmatch(f['name'], pattern):
+                    break
+            else:
+                _files.append(f)
+        data['files'] = _files
+
+
+class All(object):
+    """
+    only pass files of a certain pattern;
+    the inverse of ignore
+    """
+    
+    def __init__(self, pattern):
+        self.match = [ i.strip() for i in pattern.split(',')
+                       if i.strip() ]
+        
+
+    def __call__(self, request, data):
+        _files = []
+        for f in data['files']:
+            if self.match:
+                for pattern in self.match:
+                    if fnmatch(f['name'], pattern):
+                        _files.append(f)
+                        break
+            else:
+                # use only files where the description is not None
+                if f['description'] is not None:
+                    _files.append(f)
+        data['files'] = _files
+        
+
+class FilenameDescription(FormatterBase):
+
+    def __call__(self, request, data):
+        for f in data['files']:
+            if f['description'] is None:
+                description = f['name']
+                if 'strip' in self.args:
+                    description = description.rsplit('.', 1)[0]
+                f['description'] = description
+
+
+class TitleDescription(FormatterBase):
+    defaults = { 'separator': ':' }
+        
+    def __call__(self, request, data):
+        for f in data['files']:
+            if f['description'] and self.separator in f['description']:
+                title, description = f['description'].split(self.separator, 1)
+                f['title'] = title
+                f['description'] = description
+            else:
+                f['title'] = f['description']
+                f['description'] = None
+                
+        
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decoupage/templates/index.html	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,16 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/">
+  <head>
+    <title>${path}</title>
+  </head>
+  <body>
+    <ul>
+      <li py:if="request.path_info.strip('/')"><a href="..">..</a></li>
+      <li py:for="f in files"><a href="${f['path']}">${f['description'] or f['name']}</a></li>
+
+    </ul>
+   </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example.ini	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,7 @@
+[/]
+/ignore = .*
+foo.txt = a file about cats
+
+[/cats]
+lilly.txt = lilly
+hobbes.txt = a file about Hobbes
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/bar.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+i like dogs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/cats/bigcats/lion.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+lion
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/cats/bigcats/tiger.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+tiger
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/cats/charlie.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+i'm a fox-cat
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/cats/hobbes.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+i'm a kinda scary boy cat
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/cats/lilly.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+I'm a beautiful cat that is part siamese.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/fleem/fleem.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+i like fleem
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/fleem/index.html	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,17 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/">
+  <head>
+    <title>${path}</title>
+  </head>
+  <body>
+    <dl>
+      <py:for each="f in files">
+        <dt>${f['description']}</dt>
+        <dd><a href="${f['path']}">${f['path']}</a></dd>
+      </py:for>
+    </dl>
+   </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/fleem/index.ini	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,3 @@
+/template = index.html
+/ignore = .*
+fleem.txt = some fleem for ya
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/foo.html	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>dfoiadioafds</title>
+</head>
+<body>
+blah
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/foo.txt	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,1 @@
+i like cats
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Mon Sep 07 13:40:22 2009 -0400
@@ -0,0 +1,46 @@
+from setuptools import setup, find_packages
+import sys, os
+
+try:
+    description = file("README.txt").read()
+except IOError:
+    description = ''
+
+version = '0.1.4'
+
+setup(name='decoupage',
+      version=version,
+      description="Decoupage is the art of decorating an object by gluing colored paper cutouts onto it in combination with special paint effects ...",
+      long_description=description,
+      classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      author='Jeff Hammel',
+      author_email='k0scist@gmail.com',
+      url='http://explosivedecompression.net',
+      license="",
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+         'WebOb',	
+         'Paste',
+         'PasteScript',
+         'genshi',
+         'martINI',
+         ],
+      find_links=[
+        'https://svn.openplans.org/svn/standalone/martINI#egg=martINI',
+        ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [paste.app_factory]
+      main = decoupage.factory:factory
+
+      [decoupage.formatters]
+      ignore = decoupage.formatters:Ignore
+      all = decoupage.formatters:All
+      describe = decoupage.formatters:FilenameDescription
+      title = decoupage.formatters:TitleDescription
+      """,
+      )
+