Mercurial > hg > decoupage
view decoupage/web.py @ 115:a8613e4c51dc
call __init__ correctly
author | Jeff Hammel <k0scist@gmail.com> |
---|---|
date | Wed, 28 Aug 2024 08:30:14 -0700 |
parents | 5099d45c7730 |
children |
line wrap: on
line source
""" decoupage: a dynamic file server """ # TODO: # handle files with `#`s like like `#index.ini` # -> http://k0s.org/portfolio/ideas/#index.ini# # # oops. Handle it better # - either # is a magic hide character # - or you urlescape that guy import os import sys from collections import OrderedDict from contenttransformer.app import FileTypeTransformer from contenttransformer.app import transformers from datetime import datetime from genshi.builder import Markup from genshi.template import TemplateLoader from genshi.template.base import TemplateError from genshi.template.base import TemplateSyntaxError from genshi.template.loader import TemplateNotFound from martini.config import ConfigMunger from paste.fileapp import FileApp from pkg_resources import iter_entry_points from pkg_resources import load_entry_point from pkg_resources import resource_filename from webob import Request, Response, exc from .formatters import formatters transformers = transformers() try: # python2 string = (str, unicode) except NameError: # python3 string = (str,) class FileSorter(object): def __init__(self, *keys): self.keys = keys def __call__(self, item): try: index = self.keys.index(item['name']) except ValueError: index = len(self.keys) return (index, item['name'].lower(), item['name']) 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 'charset': 'utf-8', # content encoding for index.html files; -> `Content-Type: text/html; charset=ISO-8859-1` 'file_sorter': FileSorter } def __init__(self, **kw): # set defaults from app configuration for key, default_value in self.defaults.items(): value = kw.get(key, default_value) # handle non-string bools if isinstance(default_value, bool) and isinstance(value, string): value = {'true': True, 'false': False}[value.lower()] # TODO: error handling for bad strings setattr(self, key, value) # configure defaults assert self.directory, "Decoupage: directory not specified" self.directory = self.directory.rstrip(os.path.sep) assert os.path.isdir(self.directory), "'%s' is not a directory" % self.directory self.template_directories = self.template_directories.split() # no spaces in directory names, for now for directory in self.template_directories: assert os.path.isdir(directory), "Decoupage template directory %s does not exist!" % directory # static file server self.fileserver = FileApp # pluggable formats s = 'format.' _format_args = [ (i.split(s, 1)[-1], j) for i, j in kw.items() if i.startswith(s) ] format_args = {} for i, j in _format_args: assert i.count('.') == 1, 'Illegal string or something' format_name, var_name = i.split('.') format_args.setdefault(format_name, {})[var_name] = j self.formats = {} for _format in iter_entry_points('decoupage.formats'): try: _cls = _format.load() _instance = _cls(self, **format_args.get(_format.name, {})) except Exception as e: # record the error, but persist sys.stderr.write("Couldn't load format: {}\n{}\n".format(_format, e)) continue self.formats[_format.name] = _instance # 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 Exception as e: # record the error, but persist sys.stderr.write("Couldn't load formatter: {}\n{}\n".format(formatter, e)) continue self.formatters[formatter.name] = _formatter # template loader self.loader = TemplateLoader(self.template_directories, variable_lookup="lenient", auto_reload=self.auto_reload) ### methods dealing with HTTP def __call__(self, environ, start_response): """WSGI application""" # boilerplate: request and filename request = Request(environ) filename = request.path_info.strip('/') path = os.path.join(self.directory, filename) # check to see what we have to serve if os.path.exists(path): if os.path.isdir(path): # serve an index if request.path_info.endswith('/'): res = self.get(request) else: res = exc.HTTPMovedPermanently(add_slash=True) return res(environ, start_response) else: # serve a file conf = self.conf(request.path_info.rsplit('/',1)[0]) if '/transformer' in conf: args = [i.split('=', 1) for i in conf['/transformer'].split(',') if '=' in i] kwargs = {} for i in conf: if i.startswith('/'): name = i[1:] if name in transformers: kwargs[name] = dict([j.split('=', 1) for j in conf[i].split(',') if '=' in j]) fileserver = FileTypeTransformer(*args, **kwargs) else: fileserver = self.fileserver fileserver = fileserver(path) return fileserver(environ, start_response) else: # file does not exist conf = self.conf('/') data = dict(request=request, title="Not Found") template = self.loader.load('HTTPNotFound.html') body = template.generate(**data).render('html', doctype='html') response = Response(content_type='text/html', body=body, status=404) return response(environ, start_response) 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) ### build data dictionary # TODO: separate these out into several formatters files = self.filedata(path, directory, conf) data = {'path': path, 'files': files, 'request': request} # add a function to get the path to files data['filepath'] = lambda *segments: os.path.join(*([directory] + list(segments))) # defaults data['directory'] = directory data['css'] = () data['scripts'] = () # apply formatters formatters = self.get_formatters(path) for formatter in formatters: formatter(request, data) # return an alternate format if specified # decoupage.formats should return a 2-tuple: # (content_type, body) if 'format' in request.GET: format_name = request.GET['format'] if format_name in self.formats: _format = self.formats[format_name] content_type, body = _format(request, data) return Response(content_type=content_type, body=body) # render the template template = conf.get('/template') local_index = False if template is None: if 'index.html' in [ f['name'] for f in files ]: local_index = os.path.join(directory, 'index.html') template = local_index else: template = self.template else: if not os.path.isabs(template): _template = os.path.join(directory, template) if os.path.exists(_template): template = _template else: for _directory in self.template_directories: if template in os.listdir(_directory): break else: raise TemplateNotFound("template %s not found" % template, self.loader.search_path) # extend template `search_path` to include local directory self.loader.search_path.insert(0, directory) try: template = self.loader.load(template) res = template.generate(**data).render('html', doctype='html') except (TemplateError, TemplateSyntaxError, TemplateNotFound) as e: if local_index: print (repr(e)) return self.fileserver(local_index) raise finally: self.loader.search_path.pop(0) # set charset if given kw = {} if self.charset: kw['charset'] = self.charset # return response return Response(content_type='text/html', body=res, **kw) ### internal methods def file_sort_key(filename, keys): try: index = keys.index(filename) except ValueError: index = len(keys) return (index, filename.lower(), filename) def filedata(self, path, directory, conf=None): conf = conf or OrderedDict() files = [] # get data for files filenames = os.listdir(directory) for i in filenames: filepath = os.path.join(directory, i) filetype = 'file' if os.path.isdir(filepath): filetype = 'directory' try: modified = os.path.getmtime(filepath) except OSError: # the file (mysteriously) may not exist by this time(!) # File "/home/jhammel/web/src/decoupage/decoupage/web.py", line 114, in __call__ # res = self.get(request) # File "/home/jhammel/web/src/decoupage/decoupage/web.py", line 162, in get # files = self.filedata(path, directory, conf) # File "/home/jhammel/web/src/decoupage/decoupage/web.py", line 246, in filedata # modified = os.path.getmtime(filepath) # File "/home/jhammel/web/lib/python2.6/genericpath.py", line 54, in getmtime # return os.stat(filename).st_mtime # OSError: [Errno 2] No such file or directory: '/home/jhammel/web/site/portfolio/ideas/.#index.ini' continue # wt{h,f}??? modified = datetime.fromtimestamp(modified) data = {'path' : '%s/%s' % (path.rstrip('/'), i), 'name': i, 'modified': modified, 'type': filetype} if filetype == 'file': data['size'] = os.path.getsize(filepath) files.append(data) for i in conf: if i in filenames or i.startswith('/'): continue # TODO: deal with other links in conf; # this actually doesn't work because the ':' is magical to .ini files if i.startswith('http://') or i.startswith('https://'): files.append({'path': i, 'name': i, 'type': link}) # TODO: sort files files = sorted(files, key=self.file_sorter(*conf.keys())) # get the description for f in files: f['description'] = conf.get(f['name'], None) return files def conf(self, path, cascade=None): """returns configuration dictionary appropriate to a path""" if cascade is None: cascase = self.cascade 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 = OrderedDict() # local configuration ini_path = os.path.join(directory, 'index.ini') if os.path.exists(ini_path): try: _conf = ConfigMunger(ini_path).dict() except Exception as e: print(f"Error reading INI {ini_path}: {e}") raise if len(_conf) == 1: conf = _conf[list(_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('/'), {}) # inherit and cascade configuration inherit_directory = None if '/inherit' in conf: inherit_directory = conf['/inherit'] elif self.cascade and path_tuple: inherit_directory = '/%s' % '/'.join(path_tuple[:-1]) if inherit_directory: parent_configuration = self.conf(inherit_directory) 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 = OrderedDict() self._conf[path_tuple] = conf return conf def get_formatters(self, path): """return formatters for a path""" retval = [] conf = self.conf(path) # 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: retval.append(self.formatters[name](conf.get('/%s' % name, ''))) return retval