# HG changeset patch # User k0s # Date 1252345222 14400 # Node ID fa2005f769ebbe9cb89cca0483ccc360cf2edb97 adding decoupage from http://my-svn.assembla.com/svn/arbez/decoupage r355 diff -r 000000000000 -r fa2005f769eb README.txt --- /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 `_ .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 `_ .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. diff -r 000000000000 -r fa2005f769eb decoupage.ini --- /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 diff -r 000000000000 -r fa2005f769eb decoupage/__init__.py --- /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 @@ +# diff -r 000000000000 -r fa2005f769eb decoupage/decoupage.py --- /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) ]) diff -r 000000000000 -r fa2005f769eb decoupage/factory.py --- /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) + diff -r 000000000000 -r fa2005f769eb decoupage/formatters.py --- /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 + + diff -r 000000000000 -r fa2005f769eb decoupage/templates/index.html --- /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 @@ + + + + ${path} + + + + + diff -r 000000000000 -r fa2005f769eb example.ini --- /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 diff -r 000000000000 -r fa2005f769eb example/bar.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/cats/bigcats/lion.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/cats/bigcats/tiger.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/cats/charlie.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/cats/hobbes.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/cats/lilly.txt --- /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. diff -r 000000000000 -r fa2005f769eb example/fleem/fleem.txt --- /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 diff -r 000000000000 -r fa2005f769eb example/fleem/index.html --- /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 @@ + + + + ${path} + + +
+ +
${f['description']}
+
${f['path']}
+
+
+ + diff -r 000000000000 -r fa2005f769eb example/fleem/index.ini --- /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 diff -r 000000000000 -r fa2005f769eb example/foo.html --- /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 @@ + + +dfoiadioafds + + +blah + + diff -r 000000000000 -r fa2005f769eb example/foo.txt --- /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 diff -r 000000000000 -r fa2005f769eb setup.py --- /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 + """, + ) +