diff options
author | Martin Vilcans <martin@librador.com> | 2011-09-05 23:09:05 +0200 |
---|---|---|
committer | Martin Vilcans <martin@librador.com> | 2011-09-10 20:45:56 +0200 |
commit | 7d3877f94a8de98a11bcea631b79b6db9b7f47fe (patch) | |
tree | 637d0b0c2c32494d9426fca9bda20270ff164f6b | |
parent | 56551b10935c9027d8e1178430f7d8e82e078150 (diff) | |
download | screenplain-7d3877f94a8de98a11bcea631b79b6db9b7f47fe.tar.gz |
Removed server code from Screenplain library project.
-rw-r--r-- | screenplain/export/annotated_html.py | 33 | ||||
-rw-r--r-- | server/app.yaml | 23 | ||||
-rw-r--r-- | server/bottle.py | 2526 | ||||
-rw-r--r-- | server/favicon.ico | bin | 626 -> 0 bytes | |||
-rw-r--r-- | server/index.html | 15 | ||||
-rw-r--r-- | server/main.py | 34 | ||||
-rw-r--r-- | server/style.css | 64 |
7 files changed, 12 insertions, 2683 deletions
diff --git a/screenplain/export/annotated_html.py b/screenplain/export/annotated_html.py index 1603e9a..2bcf643 100644 --- a/screenplain/export/annotated_html.py +++ b/screenplain/export/annotated_html.py @@ -9,28 +9,14 @@ def unspace(text): text = re.sub(r'>\s+<', '><', text) return text.strip() -head = unspace(''' -<!doctype html> -<html> - <head> - <link rel="stylesheet" href="/style.css"> - </head> - <body> -''') - paragraph_html = unspace(""" <div class="block"> - <p>%(margin)s</p> + %(margin)s <div class="type type%(type)s">%(type)s</div> - <p>%(text)s</p> + %(text)s </div> """) -foot = unspace(''' - </body> -</html> -''') - types = { screenplain.parse.Slug: 'Slug', screenplain.parse.Dialog: 'Dialog', @@ -44,11 +30,16 @@ def to_html(text): def to_annotated_html(input, out): paragraphs = parse(input) - out.write(head) for para in paragraphs: lines = para.format() - margin = '<br>' * para.top_margin - html_text = '<br>'.join(to_html(line) for line in lines) - data = { 'type': types.get(type(para), '?'), 'text': html_text, 'margin': margin } + margin = '<p> </p>' * para.top_margin + html_text = ''.join( + '<p>%s</p>' % to_html(line) for line in lines + ) + data = { + 'type': types.get(type(para), '?'), + 'text': html_text, + 'margin': margin + } out.write(paragraph_html % data) - out.write(foot) + diff --git a/server/app.yaml b/server/app.yaml deleted file mode 100644 index 0b46191..0000000 --- a/server/app.yaml +++ /dev/null @@ -1,23 +0,0 @@ -application: screenplain -version: 1 -runtime: python -api_version: 1 - -handlers: -- url: /styles - static_dir: styles - -- url: /convert - script: main.py - -- url: / - static_files: index.html - upload: index.html - -- url: /style.css - static_files: style.css - upload: style.css - -- url: /favicon.ico - static_files: favicon.ico - upload: favicon.ico diff --git a/server/bottle.py b/server/bottle.py deleted file mode 100644 index 8f6e7b6..0000000 --- a/server/bottle.py +++ /dev/null @@ -1,2526 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Bottle is a fast and simple micro-framework for small web applications. It -offers request dispatching (Routes) with url parameter support, templates, -a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and -template engines - all in a single file and with no dependencies other than the -Python Standard Library. - -Homepage and documentation: http://bottlepy.org/ - -Copyright (c) 2011, Marcel Hellkamp. -License: MIT (see LICENSE.txt for details) -""" - -from __future__ import with_statement - -__author__ = 'Marcel Hellkamp' -__version__ = '0.9.6' -__license__ = 'MIT' - -import base64 -import cgi -import email.utils -import functools -import hmac -import httplib -import imp -import itertools -import mimetypes -import os -import re -import subprocess -import sys -import tempfile -import thread -import threading -import time -import warnings - -from Cookie import SimpleCookie -from tempfile import TemporaryFile -from traceback import format_exc -from urllib import urlencode, quote as urlquote, unquote as urlunquote -from urlparse import urlunsplit, urljoin, SplitResult as UrlSplitResult - -try: from collections import MutableMapping as DictMixin -except ImportError: # pragma: no cover - from UserDict import DictMixin - -try: from urlparse import parse_qs -except ImportError: # pragma: no cover - from cgi import parse_qs - -try: import cPickle as pickle -except ImportError: # pragma: no cover - import pickle - -try: from json import dumps as json_dumps -except ImportError: # pragma: no cover - try: from simplejson import dumps as json_dumps - except ImportError: # pragma: no cover - try: from django.utils.simplejson import dumps as json_dumps - except ImportError: # pragma: no cover - json_dumps = None - -NCTextIOWrapper = None -if sys.version_info >= (3,0,0): # pragma: no cover - # See Request.POST - from io import BytesIO - def touni(x, enc='utf8', err='strict'): - """ Convert anything to unicode """ - return str(x, enc, err) if isinstance(x, bytes) else str(x) - if sys.version_info < (3,2,0): - from io import TextIOWrapper - class NCTextIOWrapper(TextIOWrapper): - ''' Garbage collecting an io.TextIOWrapper(buffer) instance closes - the wrapped buffer. This subclass keeps it open. ''' - def close(self): pass -else: - from StringIO import StringIO as BytesIO - bytes = str - def touni(x, enc='utf8', err='strict'): - """ Convert anything to unicode """ - return x if isinstance(x, unicode) else unicode(str(x), enc, err) - -def tob(data, enc='utf8'): - """ Convert anything to bytes """ - return data.encode(enc) if isinstance(data, unicode) else bytes(data) - -# Convert strings and unicode to native strings -if sys.version_info >= (3,0,0): - tonat = touni -else: - tonat = tob -tonat.__doc__ = """ Convert anything to native strings """ - - -# Backward compatibility -def depr(message, critical=False): - if critical: raise DeprecationWarning(message) - warnings.warn(message, DeprecationWarning, stacklevel=3) - - -# Small helpers -def makelist(data): - if isinstance(data, (tuple, list, set, dict)): return list(data) - elif data: return [data] - else: return [] - - -class DictProperty(object): - ''' Property that maps to a key in a local dict-like attribute. ''' - def __init__(self, attr, key=None, read_only=False): - self.attr, self.key, self.read_only = attr, key, read_only - - def __call__(self, func): - functools.update_wrapper(self, func, updated=[]) - self.getter, self.key = func, self.key or func.__name__ - return self - - def __get__(self, obj, cls): - if obj is None: return self - key, storage = self.key, getattr(obj, self.attr) - if key not in storage: storage[key] = self.getter(obj) - return storage[key] - - def __set__(self, obj, value): - if self.read_only: raise AttributeError("Read-Only property.") - getattr(obj, self.attr)[self.key] = value - - def __delete__(self, obj): - if self.read_only: raise AttributeError("Read-Only property.") - del getattr(obj, self.attr)[self.key] - -def cached_property(func): - ''' A property that, if accessed, replaces itself with the computed - value. Subsequent accesses won't call the getter again. ''' - return DictProperty('__dict__')(func) - -class lazy_attribute(object): # Does not need configuration -> lower-case name - ''' A property that caches itself to the class object. ''' - def __init__(self, func): - functools.update_wrapper(self, func, updated=[]) - self.getter = func - - def __get__(self, obj, cls): - value = self.getter(cls) - setattr(cls, self.__name__, value) - return value - - - - - - -############################################################################### -# Exceptions and Events ######################################################## -############################################################################### - - -class BottleException(Exception): - """ A base class for exceptions used by bottle. """ - pass - - -class HTTPResponse(BottleException): - """ Used to break execution and immediately finish the response """ - def __init__(self, output='', status=200, header=None): - super(BottleException, self).__init__("HTTP Response %d" % status) - self.status = int(status) - self.output = output - self.headers = HeaderDict(header) if header else None - - def apply(self, response): - if self.headers: - for key, value in self.headers.iterallitems(): - response.headers[key] = value - response.status = self.status - - -class HTTPError(HTTPResponse): - """ Used to generate an error page """ - def __init__(self, code=500, output='Unknown Error', exception=None, - traceback=None, header=None): - super(HTTPError, self).__init__(output, code, header) - self.exception = exception - self.traceback = traceback - - def __repr__(self): - return template(ERROR_PAGE_TEMPLATE, e=self) - - - - - - -############################################################################### -# Routing ###################################################################### -############################################################################### - - -class RouteError(BottleException): - """ This is a base class for all routing related exceptions """ - - -class RouteReset(BottleException): - """ If raised by a plugin or request handler, the route is reset and all - plugins are re-applied. """ - - -class RouteSyntaxError(RouteError): - """ The route parser found something not supported by this router """ - - -class RouteBuildError(RouteError): - """ The route could not been built """ - - -class Router(object): - ''' A Router is an ordered collection of route->target pairs. It is used to - efficiently match WSGI requests against a number of routes and return - the first target that satisfies the request. The target may be anything, - usually a string, ID or callable object. A route consists of a path-rule - and a HTTP method. - - The path-rule is either a static path (e.g. `/contact`) or a dynamic - path that contains wildcards (e.g. `/wiki/:page`). By default, wildcards - consume characters up to the next slash (`/`). To change that, you may - add a regular expression pattern (e.g. `/wiki/:page#[a-z]+#`). - - For performance reasons, static routes (rules without wildcards) are - checked first. Dynamic routes are searched in order. Try to avoid - ambiguous or overlapping rules. - - The HTTP method string matches only on equality, with two exceptions: - * ´GET´ routes also match ´HEAD´ requests if there is no appropriate - ´HEAD´ route installed. - * ´ANY´ routes do match if there is no other suitable route installed. - - An optional ``name`` parameter is used by :meth:`build` to identify - routes. - ''' - - default = '[^/]+' - - @lazy_attribute - def syntax(cls): - return re.compile(r'(?<!\\):([a-zA-Z_][a-zA-Z_0-9]*)?(?:#(.*?)#)?') - - def __init__(self): - self.routes = {} # A {rule: {method: target}} mapping - self.rules = [] # An ordered list of rules - self.named = {} # A name->(rule, build_info) mapping - self.static = {} # Cache for static routes: {path: {method: target}} - self.dynamic = [] # Cache for dynamic routes. See _compile() - - def add(self, rule, method, target, name=None, static=False): - ''' Add a new route or replace the target for an existing route. ''' - if static: - depr("Use a backslash to escape ':' in routes.") # 0.9 - rule = rule.replace(':','\\:') - - if rule in self.routes: - self.routes[rule][method.upper()] = target - else: - self.routes[rule] = {method.upper(): target} - self.rules.append(rule) - if self.static or self.dynamic: # Clear precompiler cache. - self.static, self.dynamic = {}, {} - if name: - self.named[name] = (rule, None) - - def build(self, _name, *anon, **args): - ''' Return a string that matches a named route. Use keyword arguments - to fill out named wildcards. Remaining arguments are appended as a - query string. Raises RouteBuildError or KeyError.''' - if _name not in self.named: - raise RouteBuildError("No route with that name.", _name) - rule, pairs = self.named[_name] - if not pairs: - token = self.syntax.split(rule) - parts = [p.replace('\\:',':') for p in token[::3]] - names = token[1::3] - if len(parts) > len(names): names.append(None) - pairs = zip(parts, names) - self.named[_name] = (rule, pairs) - try: - anon = list(anon) - url = [s if k is None - else s+str(args.pop(k)) if k else s+str(anon.pop()) - for s, k in pairs] - except IndexError: - msg = "Not enough arguments to fill out anonymous wildcards." - raise RouteBuildError(msg) - except KeyError, e: - raise RouteBuildError(*e.args) - - if args: url += ['?', urlencode(args)] - return ''.join(url) - - def match(self, environ): - ''' Return a (target, url_agrs) tuple or raise HTTPError(404/405). ''' - targets, urlargs = self._match_path(environ) - if not targets: - raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO'])) - method = environ['REQUEST_METHOD'].upper() - if method in targets: - return targets[method], urlargs - if method == 'HEAD' and 'GET' in targets: - return targets['GET'], urlargs - if 'ANY' in targets: - return targets['ANY'], urlargs - allowed = [verb for verb in targets if verb != 'ANY'] - if 'GET' in allowed and 'HEAD' not in allowed: - allowed.append('HEAD') - raise HTTPError(405, "Method not allowed.", - header=[('Allow',",".join(allowed))]) - - def _match_path(self, environ): - ''' Optimized PATH_INFO matcher. ''' - path = environ['PATH_INFO'] or '/' - # Assume we are in a warm state. Search compiled rules first. - match = self.static.get(path) - if match: return match, {} - for combined, rules in self.dynamic: - match = combined.match(path) - if not match: continue - gpat, match = rules[match.lastindex - 1] - return match, gpat.match(path).groupdict() if gpat else {} - # Lazy-check if we are really in a warm state. If yes, stop here. - if self.static or self.dynamic or not self.routes: return None, {} - # Cold state: We have not compiled any rules yet. Do so and try again. - if not environ.get('wsgi.run_once'): - self._compile() - return self._match_path(environ) - # For run_once (CGI) environments, don't compile. Just check one by one. - epath = path.replace(':','\\:') # Turn path into its own static rule. - match = self.routes.get(epath) # This returns static rule only. - if match: return match, {} - for rule in self.rules: - #: Skip static routes to reduce re.compile() calls. - if rule.count(':') < rule.count('\\:'): continue - match = self._compile_pattern(rule).match(path) - if match: return self.routes[rule], match.groupdict() - return None, {} - - def _compile(self): - ''' Prepare static and dynamic search structures. ''' - self.static = {} - self.dynamic = [] - def fpat_sub(m): - return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' - for rule in self.rules: - target = self.routes[rule] - if not self.syntax.search(rule): - self.static[rule.replace('\\:',':')] = target - continue - gpat = self._compile_pattern(rule) - fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern) - gpat = gpat if gpat.groupindex else None - try: - combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat) - self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) - self.dynamic[-1][1].append((gpat, target)) - except (AssertionError, IndexError), e: # AssertionError: Too many groups - self.dynamic.append((re.compile('(^%s$)'%fpat), - [(gpat, target)])) - except re.error, e: - raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) - - def _compile_pattern(self, rule): - ''' Return a regular expression with named groups for each wildcard. ''' - out = '' - for i, part in enumerate(self.syntax.split(rule)): - if i%3 == 0: out += re.escape(part.replace('\\:',':')) - elif i%3 == 1: out += '(?P<%s>' % part if part else '(?:' - else: out += '%s)' % (part or '[^/]+') - return re.compile('^%s$'%out) - - - - - - -############################################################################### -# Application Object ########################################################### -############################################################################### - - -class Bottle(object): - """ WSGI application """ - - def __init__(self, catchall=True, autojson=True, config=None): - """ Create a new bottle instance. - You usually don't do that. Use `bottle.app.push()` instead. - """ - self.routes = [] # List of installed routes including metadata. - self.router = Router() # Maps requests to self.route indices. - self.ccache = {} # Cache for callbacks with plugins applied. - - self.plugins = [] # List of installed plugins. - - self.mounts = {} - self.error_handler = {} - #: If true, most exceptions are catched and returned as :exc:`HTTPError` - self.catchall = catchall - self.config = config or {} - self.serve = True - # Default plugins - self.hooks = self.install(HooksPlugin()) - self.typefilter = self.install(TypeFilterPlugin()) - if autojson: - self.install(JSONPlugin()) - self.install(TemplatePlugin()) - - def optimize(self, *a, **ka): - depr("Bottle.optimize() is obsolete.") - - def mount(self, app, prefix, **options): - ''' Mount an application to a specific URL prefix. The prefix is added - to SCIPT_PATH and removed from PATH_INFO before the sub-application - is called. - - :param app: an instance of :class:`Bottle`. - :param prefix: path prefix used as a mount-point. - - All other parameters are passed to the underlying :meth:`route` call. - ''' - if not isinstance(app, Bottle): - raise TypeError('Only Bottle instances are supported for now.') - prefix = '/'.join(filter(None, prefix.split('/'))) - if not prefix: - raise TypeError('Empty prefix. Perhaps you want a merge()?') - for other in self.mounts: - if other.startswith(prefix): - raise TypeError('Conflict with existing mount: %s' % other) - path_depth = prefix.count('/') + 1 - options.setdefault('method', 'ANY') - options.setdefault('skip', True) - self.mounts[prefix] = app - @self.route('/%s/:#.*#' % prefix, **options) - def mountpoint(): - request.path_shift(path_depth) - return app._handle(request.environ) - - def add_filter(self, ftype, func): - depr("Filters are deprecated and can be replaced with plugins.") #0.9 - self.typefilter.add(ftype, func) - - def install(self, plugin): - ''' Add a plugin to the list of plugins and prepare it for beeing - applied to all routes of this application. A plugin may be a simple - decorator or an object that implements the :class:`Plugin` API. - ''' - if hasattr(plugin, 'setup'): plugin.setup(self) - if not callable(plugin) and not hasattr(plugin, 'apply'): - raise TypeError("Plugins must be callable or implement .apply()") - self.plugins.append(plugin) - self.reset() - return plugin - - def uninstall(self, plugin): - ''' Uninstall plugins. Pass an instance to remove a specific plugin. - Pass a type object to remove all plugins that match that type. - Subclasses are not removed. Pass a string to remove all plugins with - a matching ``name`` attribute. Pass ``True`` to remove all plugins. - The list of affected plugins is returned. ''' - removed, remove = [], plugin - for i, plugin in list(enumerate(self.plugins))[::-1]: - if remove is True or remove is plugin or remove is type(plugin) \ - or getattr(plugin, 'name', True) == remove: - removed.append(plugin) - del self.plugins[i] - if hasattr(plugin, 'close'): plugin.close() - if removed: self.reset() - return removed - - def reset(self, id=None): - ''' Reset all routes (force plugins to be re-applied) and clear all - caches. If an ID is given, only that specific route is affected. ''' - if id is None: self.ccache.clear() - else: self.ccache.pop(id, None) - if DEBUG: - for route in self.routes: - if route['id'] not in self.ccache: - self.ccache[route['id']] = self._build_callback(route) - - def close(self): - ''' Close the application and all installed plugins. ''' - for plugin in self.plugins: - if hasattr(plugin, 'close'): plugin.close() - self.stopped = True - - def match(self, environ): - """ (deprecated) Search for a matching route and return a - (callback, urlargs) tuple. - The first element is the associated route callback with plugins - applied. The second value is a dictionary with parameters extracted - from the URL. The :class:`Router` raises :exc:`HTTPError` (404/405) - on a non-match.""" - depr("This method will change semantics in 0.10.") - return self._match(environ) - - def _match(self, environ): - handle, args = self.router.match(environ) - environ['route.handle'] = handle # TODO move to router? - environ['route.url_args'] = args - try: - return self.ccache[handle], args - except KeyError: - config = self.routes[handle] - callback = self.ccache[handle] = self._build_callback(config) - return callback, args - - def _build_callback(self, config): - ''' Apply plugins to a route and return a new callable. ''' - wrapped = config['callback'] - plugins = self.plugins + config['apply'] - skip = config['skip'] - try: - for plugin in reversed(plugins): - if True in skip: break - if plugin in skip or type(plugin) in skip: continue - if getattr(plugin, 'name', True) in skip: continue - if hasattr(plugin, 'apply'): - wrapped = plugin.apply(wrapped, config) - else: - wrapped = plugin(wrapped) - if not wrapped: break - functools.update_wrapper(wrapped, config['callback']) - return wrapped - except RouteReset: # A plugin may have changed the config dict inplace. - return self._build_callback(config) # Apply all plugins again. - - def get_url(self, routename, **kargs): - """ Return a string that matches a named route """ - scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' - location = self.router.build(routename, **kargs).lstrip('/') - return urljoin(urljoin('/', scriptname), location) - - def route(self, path=None, method='GET', callback=None, name=None, - apply=None, skip=None, **config): - """ A decorator to bind a function to a request URL. Example:: - - @app.route('/hello/:name') - def hello(name): - return 'Hello %s' % name - - The ``:name`` part is a wildcard. See :class:`Router` for syntax - details. - - :param path: Request path or a list of paths to listen to. If no - path is specified, it is automatically generated from the - signature of the function. - :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of - methods to listen to. (default: `GET`) - :param callback: An optional shortcut to avoid the decorator - syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` - :param name: The name for this route. (default: None) - :param apply: A decorator or plugin or a list of plugins. These are - applied to the route callback in addition to installed plugins. - :param skip: A list of plugins, plugin classes or names. Matching - plugins are not installed to this route. ``True`` skips all. - - Any additional keyword arguments are stored as route-specific - configuration and passed to plugins (see :meth:`Plugin.apply`). - """ - if callable(path): path, callback = None, path - - plugins = makelist(apply) - skiplist = makelist(skip) - if 'decorate' in config: - depr("The 'decorate' parameter was renamed to 'apply'") # 0.9 - plugins += makelist(config.pop('decorate')) - if config.pop('no_hooks', False): - depr("The no_hooks parameter is no longer used. Add 'hooks' to the"\ - " list of skipped plugins instead.") # 0.9 - skiplist.append('hooks') - static = config.get('static', False) # depr 0.9 - - def decorator(callback): - for rule in makelist(path) or yieldroutes(callback): - for verb in makelist(method): - verb = verb.upper() - cfg = dict(rule=rule, method=verb, callback=callback, - name=name, app=self, config=config, - apply=plugins, skip=skiplist) - self.routes.append(cfg) - cfg['id'] = self.routes.index(cfg) - self.router.add(rule, verb, cfg['id'], name=name, static=static) - if DEBUG: self.ccache[cfg['id']] = self._build_callback(cfg) - return callback - - return decorator(callback) if callback else decorator - - def get(self, path=None, method='GET', **options): - """ Equals :meth:`route`. """ - return self.route(path, method, **options) - - def post(self, path=None, method='POST', **options): - """ Equals :meth:`route` with a ``POST`` method parameter. """ - return self.route(path, method, **options) - - def put(self, path=None, method='PUT', **options): - """ Equals :meth:`route` with a ``PUT`` method parameter. """ - return self.route(path, method, **options) - - def delete(self, path=None, method='DELETE', **options): - """ Equals :meth:`route` with a ``DELETE`` method parameter. """ - return self.route(path, method, **options) - - def error(self, code=500): - """ Decorator: Register an output handler for a HTTP error code""" - def wrapper(handler): - self.error_handler[int(code)] = handler - return handler - return wrapper - - def hook(self, name): - """ Return a decorator that attaches a callback to a hook. """ - def wrapper(func): - self.hooks.add(name, func) - return func - return wrapper - - def add_hook(self, name, func): - depr("Call Bottle.hooks.add() instead.") #0.9 - self.hooks.add(name, func) - - def remove_hook(self, name, func): - depr("Call Bottle.hooks.remove() instead.") #0.9 - self.hooks.remove(name, func) - - def handle(self, path, method='GET'): - """ (deprecated) Execute the first matching route callback and return - the result. :exc:`HTTPResponse` exceptions are catched and returned. - If :attr:`Bottle.catchall` is true, other exceptions are catched as - well and returned as :exc:`HTTPError` instances (500). - """ - depr("This method will change semantics in 0.10. Try to avoid it.") - if isinstance(path, dict): - return self._handle(path) - return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) - - def _handle(self, environ): - if not self.serve: - depr("Bottle.serve will be removed in 0.10.") - return HTTPError(503, "Server stopped") - try: - callback, args = self._match(environ) - return callback(**args) - except HTTPResponse, r: - return r - except RouteReset: # Route reset requested by the callback or a plugin. - del self.ccache[handle] - return self._handle(environ) # Try again. - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception, e: - if not self.catchall: raise - return HTTPError(500, "Internal Server Error", e, format_exc(10)) - - def _cast(self, out, request, response, peek=None): - """ Try to convert the parameter into something WSGI compatible and set - correct HTTP headers when possible. - Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, - iterable of strings and iterable of unicodes - """ - - # Empty output is done here - if not out: - response.headers['Content-Length'] = 0 - return [] - # Join lists of byte or unicode strings. Mixed lists are NOT supported - if isinstance(out, (tuple, list))\ - and isinstance(out[0], (bytes, unicode)): - out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' - # Encode unicode strings - if isinstance(out, unicode): - out = out.encode(response.charset) - # Byte Strings are just returned - if isinstance(out, bytes): - response.headers['Content-Length'] = str(len(out)) - return [out] - # HTTPError or HTTPException (recursive, because they may wrap anything) - if isinstance(out, HTTPError): - out.apply(response) - out = self.error_handler.get(out.status, repr)(out) - if isinstance(out, HTTPResponse): - depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 - return self._cast(out, request, response) - if isinstance(out, HTTPResponse): - out.apply(response) - return self._cast(out.output, request, response) - - # File-like objects. - if hasattr(out, 'read'): - if 'wsgi.file_wrapper' in request.environ: - return request.environ['wsgi.file_wrapper'](out) - elif hasattr(out, 'close') or not hasattr(out, '__iter__'): - return WSGIFileWrapper(out) - - # Handle Iterables. We peek into them to detect their inner type. - try: - out = iter(out) - first = out.next() - while not first: - first = out.next() - except StopIteration: - return self._cast('', request, response) - except HTTPResponse, e: - first = e - except Exception, e: - first = HTTPError(500, 'Unhandled exception', e, format_exc(10)) - if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\ - or not self.catchall: - raise - # These are the inner types allowed in iterator or generator objects. - if isinstance(first, HTTPResponse): - return self._cast(first, request, response) - if isinstance(first, bytes): - return itertools.chain([first], out) - if isinstance(first, unicode): - return itertools.imap(lambda x: x.encode(response.charset), - itertools.chain([first], out)) - return self._cast(HTTPError(500, 'Unsupported response type: %s'\ - % type(first)), request, response) - - def wsgi(self, environ, start_response): - """ The bottle WSGI-interface. """ - try: - environ['bottle.app'] = self - request.bind(environ) - response.bind() - out = self._handle(environ) - out = self._cast(out, request, response) - # rfc2616 section 4.3 - if response.status in (100, 101, 204, 304) or request.method == 'HEAD': - if hasattr(out, 'close'): out.close() - out = [] - status = '%d %s' % (response.status, HTTP_CODES[response.status]) - start_response(status, response.headerlist) - return out - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception, e: - if not self.catchall: raise - err = '<h1>Critical error while processing request: %s</h1>' \ - % environ.get('PATH_INFO', '/') - if DEBUG: - err += '<h2>Error:</h2>\n<pre>%s</pre>\n' % repr(e) - err += '<h2>Traceback:</h2>\n<pre>%s</pre>\n' % format_exc(10) - environ['wsgi.errors'].write(err) #TODO: wsgi.error should not get html - start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html')]) - return [tob(err)] - - def __call__(self, environ, start_response): - return self.wsgi(environ, start_response) - - - - - - -############################################################################### -# HTTP and WSGI Tools ########################################################## -############################################################################### - - -class Request(threading.local, DictMixin): - """ Represents a single HTTP request using thread-local attributes. - The Request object wraps a WSGI environment and can be used as such. - """ - def __init__(self, environ=None): - """ Create a new Request instance. - - You usually don't do this but use the global `bottle.request` - instance instead. - """ - self.bind(environ or {},) - - def bind(self, environ): - """ Bind a new WSGI environment. - - This is done automatically for the global `bottle.request` - instance on every request. - """ - self.environ = environ - # These attributes are used anyway, so it is ok to compute them here - self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/') - self.method = environ.get('REQUEST_METHOD', 'GET').upper() - - @property - def _environ(self): - depr("Request._environ renamed to Request.environ") - return self.environ - - def copy(self): - ''' Returns a copy of self ''' - return Request(self.environ.copy()) - - def path_shift(self, shift=1): - ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - - :param shift: The number of path fragments to shift. May be negative - to change the shift direction. (default: 1) - ''' - script_name = self.environ.get('SCRIPT_NAME','/') - self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift) - self['PATH_INFO'] = self.path - - def __getitem__(self, key): return self.environ[key] - def __delitem__(self, key): self[key] = ""; del(self.environ[key]) - def __iter__(self): return iter(self.environ) - def __len__(self): return len(self.environ) - def keys(self): return self.environ.keys() - def __setitem__(self, key, value): - """ Shortcut for Request.environ.__setitem__ """ - self.environ[key] = value - todelete = [] - if key in ('PATH_INFO','REQUEST_METHOD'): - self.bind(self.environ) - elif key == 'wsgi.input': todelete = ('body','forms','files','params') - elif key == 'QUERY_STRING': todelete = ('get','params') - elif key.startswith('HTTP_'): todelete = ('headers', 'cookies') - for key in todelete: - if 'bottle.' + key in self.environ: - del self.environ['bottle.' + key] - - @DictProperty('environ', 'bottle.urlparts', read_only=True) - def urlparts(self): - ''' Return a :class:`urlparse.SplitResult` tuple that can be used - to reconstruct the full URL as requested by the client. - The tuple contains: (scheme, host, path, query_string, fragment). - The fragment is always empty because it is not visible to the server. - ''' - env = self.environ - http = env.get('wsgi.url_scheme', 'http') - host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') - if not host: - # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. - host = env.get('SERVER_NAME', '127.0.0.1') - port = env.get('SERVER_PORT') - if port and port != ('80' if http == 'http' else '443'): - host += ':' + port - spath = self.environ.get('SCRIPT_NAME','').rstrip('/') + '/' - rpath = self.path.lstrip('/') - path = urlquote(urljoin(spath, rpath)) - return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') - - @property - def url(self): - """ Full URL as requested by the client. """ - return self.urlparts.geturl() - - @property - def fullpath(self): - """ Request path including SCRIPT_NAME (if present). """ - return urlunquote(self.urlparts[2]) - - @property - def query_string(self): - """ The part of the URL following the '?'. """ - return self.environ.get('QUERY_STRING', '') - - @property - def content_length(self): - """ Content-Length header as an integer, -1 if not specified """ - return int(self.environ.get('CONTENT_LENGTH', '') or -1) - - @property - def header(self): - depr("The Request.header property was renamed to Request.headers") - return self.headers - - @DictProperty('environ', 'bottle.headers', read_only=True) - def headers(self): - ''' Request HTTP Headers stored in a :class:`HeaderDict`. ''' - return WSGIHeaderDict(self.environ) - - @DictProperty('environ', 'bottle.get', read_only=True) - def GET(self): - """ The QUERY_STRING parsed into an instance of :class:`MultiDict`. """ - data = parse_qs(self.query_string, keep_blank_values=True) - get = self.environ['bottle.get'] = MultiDict() - for key, values in data.iteritems(): - for value in values: - get[key] = value - return get - - @DictProperty('environ', 'bottle.post', read_only=True) - def POST(self): - """ The combined values from :attr:`forms` and :attr:`files`. Values are - either strings (form values) or instances of - :class:`cgi.FieldStorage` (file uploads). - """ - post = MultiDict() - safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi - for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): - if key in self.environ: safe_env[key] = self.environ[key] - if NCTextIOWrapper: - fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') - else: - fb = self.body - data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) - for item in data.list or []: - post[item.name] = item if item.filename else item.value - return post - - @DictProperty('environ', 'bottle.forms', read_only=True) - def forms(self): - """ POST form values parsed into an instance of :class:`MultiDict`. - - This property contains form values parsed from an `url-encoded` - or `multipart/form-data` encoded POST request bidy. The values are - native strings. - """ - forms = MultiDict() - for name, item in self.POST.iterallitems(): - if not hasattr(item, 'filename'): - forms[name] = item - return forms - - @DictProperty('environ', 'bottle.files', read_only=True) - def files(self): - """ File uploads parsed into an instance of :class:`MultiDict`. - - This property contains file uploads parsed from an - `multipart/form-data` encoded POST request body. The values are - instances of :class:`cgi.FieldStorage`. - """ - files = MultiDict() - for name, item in self.POST.iterallitems(): - if hasattr(item, 'filename'): - files[name] = item - return files - - @DictProperty('environ', 'bottle.params', read_only=True) - def params(self): - """ A combined :class:`MultiDict` with values from :attr:`forms` and - :attr:`GET`. File-uploads are not included. """ - params = MultiDict(self.GET) - for key, value in self.forms.iterallitems(): - params[key] = value - return params - - @DictProperty('environ', 'bottle.body', read_only=True) - def _body(self): - """ The HTTP request body as a seekable file-like object. - - This property returns a copy of the `wsgi.input` stream and should - be used instead of `environ['wsgi.input']`. - """ - maxread = max(0, self.content_length) - stream = self.environ['wsgi.input'] - body = BytesIO() if maxread < MEMFILE_MAX else TemporaryFile(mode='w+b') - while maxread > 0: - part = stream.read(min(maxread, MEMFILE_MAX)) - if not part: break - body.write(part) - maxread -= len(part) - self.environ['wsgi.input'] = body - body.seek(0) - return body - - @property - def body(self): - self._body.seek(0) - return self._body - - @property - def auth(self): #TODO: Tests and docs. Add support for digest. namedtuple? - """ HTTP authorization data as a (user, passwd) tuple. (experimental) - - This implementation currently only supports basic auth and returns - None on errors. - """ - return parse_auth(self.headers.get('Authorization','')) - - @DictProperty('environ', 'bottle.cookies', read_only=True) - def COOKIES(self): - """ Cookies parsed into a dictionary. Signed cookies are NOT decoded - automatically. See :meth:`get_cookie` for details. - """ - raw_dict = SimpleCookie(self.headers.get('Cookie','')) - cookies = {} - for cookie in raw_dict.itervalues(): - cookies[cookie.key] = cookie.value - return cookies - - def get_cookie(self, key, secret=None): - """ Return the content of a cookie. To read a `Signed Cookies`, use the - same `secret` as used to create the cookie (see - :meth:`Response.set_cookie`). If anything goes wrong, None is - returned. - """ - value = self.COOKIES.get(key) - if secret and value: - dec = cookie_decode(value, secret) # (key, value) tuple or None - return dec[1] if dec and dec[0] == key else None - return value or None - - @property - def is_ajax(self): - ''' True if the request was generated using XMLHttpRequest ''' - #TODO: write tests - return self.headers.get('X-Requested-With') == 'XMLHttpRequest' - - -class Response(threading.local): - """ Represents a single HTTP response using thread-local attributes. - """ - - def __init__(self): - self.bind() - - def bind(self): - """ Resets the Response object to its factory defaults. """ - self._COOKIES = None - self.status = 200 - self.headers = HeaderDict() - self.content_type = 'text/html; charset=UTF-8' - - @property - def header(self): - depr("Response.header renamed to Response.headers") - return self.headers - - def copy(self): - ''' Returns a copy of self. ''' - copy = Response() - copy.status = self.status - copy.headers = self.headers.copy() - copy.content_type = self.content_type - return copy - - def wsgiheader(self): - ''' Returns a wsgi conform list of header/value pairs. ''' - for c in self.COOKIES.values(): - if c.OutputString() not in self.headers.getall('Set-Cookie'): - self.headers.append('Set-Cookie', c.OutputString()) - # rfc2616 section 10.2.3, 10.3.5 - if self.status in (204, 304) and 'content-type' in self.headers: - del self.headers['content-type'] - if self.status == 304: - for h in ('allow', 'content-encoding', 'content-language', - 'content-length', 'content-md5', 'content-range', - 'content-type', 'last-modified'): # + c-location, expires? - if h in self.headers: - del self.headers[h] - return list(self.headers.iterallitems()) - headerlist = property(wsgiheader) - - @property - def charset(self): - """ Return the charset specified in the content-type header. - - This defaults to `UTF-8`. - """ - if 'charset=' in self.content_type: - return self.content_type.split('charset=')[-1].split(';')[0].strip() - return 'UTF-8' - - @property - def COOKIES(self): - """ A dict-like SimpleCookie instance. Use :meth:`set_cookie` instead. """ - if not self._COOKIES: - self._COOKIES = SimpleCookie() - return self._COOKIES - - def set_cookie(self, key, value, secret=None, **kargs): - ''' Add a cookie or overwrite an old one. If the `secret` parameter is - set, create a `Signed Cookie` (described below). - - :param key: the name of the cookie. - :param value: the value of the cookie. - :param secret: required for signed cookies. (default: None) - :param max_age: maximum age in seconds. (default: None) - :param expires: a datetime object or UNIX timestamp. (defaut: None) - :param domain: the domain that is allowed to read the cookie. - (default: current domain) - :param path: limits the cookie to a given path (default: /) - - If neither `expires` nor `max_age` are set (default), the cookie - lasts only as long as the browser is not closed. - - Signed cookies may store any pickle-able object and are - cryptographically signed to prevent manipulation. Keep in mind that - cookies are limited to 4kb in most browsers. - - Warning: Signed cookies are not encrypted (the client can still see - the content) and not copy-protected (the client can restore an old - cookie). The main intention is to make pickling and unpickling - save, not to store secret information at client side. - ''' - if secret: - value = touni(cookie_encode((key, value), secret)) - elif not isinstance(value, basestring): - raise TypeError('Secret missing for non-string Cookie.') - - self.COOKIES[key] = value - for k, v in kargs.iteritems(): - self.COOKIES[key][k.replace('_', '-')] = v - - def delete_cookie(self, key, **kwargs): - ''' Delete a cookie. Be sure to use the same `domain` and `path` - parameters as used to create the cookie. ''' - kwargs['max_age'] = -1 - kwargs['expires'] = 0 - self.set_cookie(key, '', **kwargs) - - def get_content_type(self): - """ Current 'Content-Type' header. """ - return self.headers['Content-Type'] - - def set_content_type(self, value): - self.headers['Content-Type'] = value - - content_type = property(get_content_type, set_content_type, None, - get_content_type.__doc__) - - - - - - -############################################################################### -# Plugins ###################################################################### -############################################################################### - - - -class JSONPlugin(object): - name = 'json' - - def __init__(self, json_dumps=json_dumps): - self.json_dumps = json_dumps - - def apply(self, callback, context): - dumps = self.json_dumps - if not dumps: return callback - def wrapper(*a, **ka): - rv = callback(*a, **ka) - if isinstance(rv, dict): - response.content_type = 'application/json' - return dumps(rv) - return rv - return wrapper - - - -class HooksPlugin(object): - name = 'hooks' - - def __init__(self): - self.hooks = {'before_request': [], 'after_request': []} - self.app = None - - def _empty(self): - return not (self.hooks['before_request'] or self.hooks['after_request']) - - def setup(self, app): - self.app = app - - def add(self, name, func): - ''' Attach a callback to a hook. ''' - if name not in self.hooks: - raise ValueError("Unknown hook name %s" % name) - was_empty = self._empty() - self.hooks[name].append(func) - if self.app and was_empty and not self._empty(): self.app.reset() - - def remove(self, name, func): - ''' Remove a callback from a hook. ''' - if name not in self.hooks: - raise ValueError("Unknown hook name %s" % name) - was_empty = self._empty() - self.hooks[name].remove(func) - if self.app and not was_empty and self._empty(): self.app.reset() - - def apply(self, callback, context): - if self._empty(): return callback - before_request = self.hooks['before_request'] - after_request = self.hooks['after_request'] - def wrapper(*a, **ka): - for hook in before_request: hook() - rv = callback(*a, **ka) - for hook in after_request[::-1]: hook() - return rv - return wrapper - - - -class TypeFilterPlugin(object): - def __init__(self): - self.filter = [] - self.app = None - - def setup(self, app): - self.app = app - - def add(self, ftype, func): - if not isinstance(ftype, type): - raise TypeError("Expected type object, got %s" % type(ftype)) - self.filter = [(t, f) for (t, f) in self.filter if t != ftype] - self.filter.append((ftype, func)) - if len(self.filter) == 1 and self.app: self.app.reset() - - def apply(self, callback, context): - filter = self.filter - if not filter: return callback - def wrapper(*a, **ka): - rv = callback(*a, **ka) - for testtype, filterfunc in filter: - if isinstance(rv, testtype): - rv = filterfunc(rv) - return rv - return wrapper - - -class TemplatePlugin(object): - ''' This plugin applies the :func:`view` decorator to all routes with a - `template` config parameter. If the parameter is a tuple, the second - element must be a dict with additional options (e.g. `template_engine`) - or default variables for the template. ''' - name = 'template' - - def apply(self, callback, context): - conf = context['config'].get('template') - if isinstance(conf, (tuple, list)) and len(conf) == 2: - return view(conf[0], **conf[1])(callback) - elif isinstance(conf, str) and 'template_opts' in context['config']: - depr('The `template_opts` parameter is deprecated.') #0.9 - return view(conf, **context['config']['template_opts'])(callback) - elif isinstance(conf, str): - return view(conf)(callback) - else: - return callback - - -#: Not a plugin, but part of the plugin API. TODO: Find a better place. -class _ImportRedirect(object): - def __init__(self, name, impmask): - ''' Create a virtual package that redirects imports (see PEP 302). ''' - self.name = name - self.impmask = impmask - self.module = sys.modules.setdefault(name, imp.new_module(name)) - self.module.__dict__.update({'__file__': __file__, '__path__': [], - '__all__': [], '__loader__': self}) - sys.meta_path.append(self) - - def find_module(self, fullname, path=None): - if '.' not in fullname: return - packname, modname = fullname.rsplit('.', 1) - if packname != self.name: return - return self - - def load_module(self, fullname): - if fullname in sys.modules: return sys.modules[fullname] - packname, modname = fullname.rsplit('.', 1) - realname = self.impmask % modname - __import__(realname) - module = sys.modules[fullname] = sys.modules[realname] - setattr(self.module, modname, module) - module.__loader__ = self - return module - - - - - - -############################################################################### -# Common Utilities ############################################################# -############################################################################### - - -class MultiDict(DictMixin): - """ A dict that remembers old values for each key """ - # collections.MutableMapping would be better for Python >= 2.6 - def __init__(self, *a, **k): - self.dict = dict() - for k, v in dict(*a, **k).iteritems(): - self[k] = v - - def __len__(self): return len(self.dict) - def __iter__(self): return iter(self.dict) - def __contains__(self, key): return key in self.dict - def __delitem__(self, key): del self.dict[key] - def keys(self): return self.dict.keys() - def __getitem__(self, key): return self.get(key, KeyError, -1) - def __setitem__(self, key, value): self.append(key, value) - - def append(self, key, value): self.dict.setdefault(key, []).append(value) - def replace(self, key, value): self.dict[key] = [value] - def getall(self, key): return self.dict.get(key) or [] - - def get(self, key, default=None, index=-1): - if key not in self.dict and default != KeyError: - return [default][index] - return self.dict[key][index] - - def iterallitems(self): - for key, values in self.dict.iteritems(): - for value in values: - yield key, value - - -class HeaderDict(MultiDict): - """ Same as :class:`MultiDict`, but title()s the keys and overwrites. """ - def __contains__(self, key): - return MultiDict.__contains__(self, self.httpkey(key)) - def __getitem__(self, key): - return MultiDict.__getitem__(self, self.httpkey(key)) - def __delitem__(self, key): - return MultiDict.__delitem__(self, self.httpkey(key)) - def __setitem__(self, key, value): self.replace(key, value) - def get(self, key, default=None, index=-1): - return MultiDict.get(self, self.httpkey(key), default, index) - def append(self, key, value): - return MultiDict.append(self, self.httpkey(key), str(value)) - def replace(self, key, value): - return MultiDict.replace(self, self.httpkey(key), str(value)) - def getall(self, key): return MultiDict.getall(self, self.httpkey(key)) - def httpkey(self, key): return str(key).replace('_','-').title() - - -class WSGIHeaderDict(DictMixin): - ''' This dict-like class wraps a WSGI environ dict and provides convenient - access to HTTP_* fields. Keys and values are native strings - (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI - environment contains non-native string values, these are de- or encoded - using a lossless 'latin1' character set. - - The API will remain stable even on changes to the relevant PEPs. - Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one - that uses non-native strings.) - ''' - #: List of keys that do not have a 'HTTP_' prefix. - cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') - - def __init__(self, environ): - self.environ = environ - - def _ekey(self, key): - ''' Translate header field name to CGI/WSGI environ key. ''' - key = key.replace('-','_').upper() - if key in self.cgikeys: - return key - return 'HTTP_' + key - - def raw(self, key, default=None): - ''' Return the header value as is (may be bytes or unicode). ''' - return self.environ.get(self._ekey(key), default) - - def __getitem__(self, key): - return tonat(self.environ[self._ekey(key)], 'latin1') - - def __setitem__(self, key, value): - raise TypeError("%s is read-only." % self.__class__) - - def __delitem__(self, key): - raise TypeError("%s is read-only." % self.__class__) - - def __iter__(self): - for key in self.environ: - if key[:5] == 'HTTP_': - yield key[5:].replace('_', '-').title() - elif key in self.cgikeys: - yield key.replace('_', '-').title() - - def keys(self): return list(self) - def __len__(self): return len(list(self)) - def __contains__(self, key): return self._ekey(key) in self.environ - - -class AppStack(list): - """ A stack-like list. Calling it returns the head of the stack. """ - - def __call__(self): - """ Return the current default application. """ - return self[-1] - - def push(self, value=None): - """ Add a new :class:`Bottle` instance to the stack """ - if not isinstance(value, Bottle): - value = Bottle() - self.append(value) - return value - - -class WSGIFileWrapper(object): - - def __init__(self, fp, buffer_size=1024*64): - self.fp, self.buffer_size = fp, buffer_size - for attr in ('fileno', 'close', 'read', 'readlines'): - if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) - - def __iter__(self): - read, buff = self.fp.read, self.buffer_size - while True: - part = read(buff) - if not part: break - yield part - - - - - - -############################################################################### -# Application Helper ########################################################### -############################################################################### - - -def dict2json(d): - depr('JSONPlugin is the preferred way to return JSON.') #0.9 - response.content_type = 'application/json' - return json_dumps(d) - - -def abort(code=500, text='Unknown Error: Application stopped.'): - """ Aborts execution and causes a HTTP error. """ - raise HTTPError(code, text) - - -def redirect(url, code=303): - """ Aborts execution and causes a 303 redirect. """ - location = urljoin(request.url, url) - raise HTTPResponse("", status=code, header=dict(Location=location)) - - -def send_file(*a, **k): #BC 0.6.4 - """ Raises the output of static_file(). (deprecated) """ - depr("Use 'raise static_file()' instead of 'send_file()'.") - raise static_file(*a, **k) - - -def static_file(filename, root, mimetype='auto', guessmime=True, download=False): - """ Open a file in a safe way and return :exc:`HTTPResponse` with status - code 200, 305, 401 or 404. Set Content-Type, Content-Encoding, - Content-Length and Last-Modified header. Obey If-Modified-Since header - and HEAD requests. - """ - root = os.path.abspath(root) + os.sep - filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) - header = dict() - - if not filename.startswith(root): - return HTTPError(403, "Access denied.") - if not os.path.exists(filename) or not os.path.isfile(filename): - return HTTPError(404, "File does not exist.") - if not os.access(filename, os.R_OK): - return HTTPError(403, "You do not have permission to access this file.") - - if not guessmime: #0.9 - if mimetype == 'auto': mimetype = 'text/plain' - depr("To disable mime-type guessing, specify a type explicitly.") - if mimetype == 'auto': - mimetype, encoding = mimetypes.guess_type(filename) - if mimetype: header['Content-Type'] = mimetype - if encoding: header['Content-Encoding'] = encoding - elif mimetype: - header['Content-Type'] = mimetype - - if download: - download = os.path.basename(filename if download == True else download) - header['Content-Disposition'] = 'attachment; filename="%s"' % download - - stats = os.stat(filename) - header['Content-Length'] = stats.st_size - lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) - header['Last-Modified'] = lm - - ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') - if ims: - ims = parse_date(ims.split(";")[0].strip()) - if ims is not None and ims >= int(stats.st_mtime): - header['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) - return HTTPResponse(status=304, header=header) - - body = '' if request.method == 'HEAD' else open(filename, 'rb') - return HTTPResponse(body, header=header) - - - - - - -############################################################################### -# HTTP Utilities and MISC (TODO) ############################################### -############################################################################### - - -def debug(mode=True): - """ Change the debug level. - There is only one debug level supported at the moment.""" - global DEBUG - DEBUG = bool(mode) - - -def parse_date(ims): - """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ - try: - ts = email.utils.parsedate_tz(ims) - return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone - except (TypeError, ValueError, IndexError, OverflowError): - return None - - -def parse_auth(header): - """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" - try: - method, data = header.split(None, 1) - if method.lower() == 'basic': - name, pwd = base64.b64decode(data).split(':', 1) - return name, pwd - except (KeyError, ValueError, TypeError): - return None - - -def _lscmp(a, b): - ''' Compares two strings in a cryptographically save way: - Runtime is not affected by length of common prefix. ''' - return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) - - -def cookie_encode(data, key): - ''' Encode and sign a pickle-able object. Return a (byte) string ''' - msg = base64.b64encode(pickle.dumps(data, -1)) - sig = base64.b64encode(hmac.new(key, msg).digest()) - return tob('!') + sig + tob('?') + msg - - -def cookie_decode(data, key): - ''' Verify and decode an encoded string. Return an object or None.''' - data = tob(data) - if cookie_is_encoded(data): - sig, msg = data.split(tob('?'), 1) - if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())): - return pickle.loads(base64.b64decode(msg)) - return None - - -def cookie_is_encoded(data): - ''' Return True if the argument looks like a encoded cookie.''' - return bool(data.startswith(tob('!')) and tob('?') in data) - - -def yieldroutes(func): - """ Return a generator for routes that match the signature (name, args) - of the func parameter. This may yield more than one route if the function - takes optional keyword arguments. The output is best described by example:: - - a() -> '/a' - b(x, y) -> '/b/:x/:y' - c(x, y=5) -> '/c/:x' and '/c/:x/:y' - d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' - """ - import inspect # Expensive module. Only import if necessary. - path = '/' + func.__name__.replace('__','/').lstrip('/') - spec = inspect.getargspec(func) - argc = len(spec[0]) - len(spec[3] or []) - path += ('/:%s' * argc) % tuple(spec[0][:argc]) - yield path - for arg in spec[0][argc:]: - path += '/:%s' % arg - yield path - - -def path_shift(script_name, path_info, shift=1): - ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - - :return: The modified paths. - :param script_name: The SCRIPT_NAME path. - :param script_name: The PATH_INFO path. - :param shift: The number of path fragments to shift. May be negative to - change the shift direction. (default: 1) - ''' - if shift == 0: return script_name, path_info - pathlist = path_info.strip('/').split('/') - scriptlist = script_name.strip('/').split('/') - if pathlist and pathlist[0] == '': pathlist = [] - if scriptlist and scriptlist[0] == '': scriptlist = [] - if shift > 0 and shift <= len(pathlist): - moved = pathlist[:shift] - scriptlist = scriptlist + moved - pathlist = pathlist[shift:] - elif shift < 0 and shift >= -len(scriptlist): - moved = scriptlist[shift:] - pathlist = moved + pathlist - scriptlist = scriptlist[:shift] - else: - empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' - raise AssertionError("Cannot shift. Nothing left from %s" % empty) - new_script_name = '/' + '/'.join(scriptlist) - new_path_info = '/' + '/'.join(pathlist) - if path_info.endswith('/') and pathlist: new_path_info += '/' - return new_script_name, new_path_info - - - -# Decorators -#TODO: Replace default_app() with app() - -def validate(**vkargs): - """ - Validates and manipulates keyword arguments by user defined callables. - Handles ValueError and missing arguments by raising HTTPError(403). - """ - def decorator(func): - def wrapper(**kargs): - for key, value in vkargs.iteritems(): - if key not in kargs: - abort(403, 'Missing parameter: %s' % key) - try: - kargs[key] = value(kargs[key]) - except ValueError: - abort(403, 'Wrong parameter format for: %s' % key) - return func(**kargs) - return wrapper - return decorator - - -def auth_basic(check, realm="private", text="Access denied"): - ''' Callback decorator to require HTTP auth (basic). - TODO: Add route(check_auth=...) parameter. ''' - def decorator(func): - def wrapper(*a, **ka): - user, password = request.auth or (None, None) - if user is None or not check(user, password): - response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm - return HTTPError(401, text) - return func(*a, **ka) - return wrapper - return decorator - - -def make_default_app_wrapper(name): - ''' Return a callable that relays calls to the current default app. ''' - @functools.wraps(getattr(Bottle, name)) - def wrapper(*a, **ka): - return getattr(app(), name)(*a, **ka) - return wrapper - - -for name in '''route get post put delete error mount - hook install uninstall'''.split(): - globals()[name] = make_default_app_wrapper(name) -url = make_default_app_wrapper('get_url') -del name - - -def default(): - depr("The default() decorator is deprecated. Use @error(404) instead.") - return error(404) - - - - - - -############################################################################### -# Server Adapter ############################################################### -############################################################################### - - -class ServerAdapter(object): - quiet = False - def __init__(self, host='127.0.0.1', port=8080, **config): - self.options = config - self.host = host - self.port = int(port) - - def run(self, handler): # pragma: no cover - pass - - def __repr__(self): - args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()]) - return "%s(%s)" % (self.__class__.__name__, args) - - -class CGIServer(ServerAdapter): - quiet = True - def run(self, handler): # pragma: no cover - from wsgiref.handlers import CGIHandler - def fixed_environ(environ, start_response): - environ.setdefault('PATH_INFO', '') - return handler(environ, start_response) - CGIHandler().run(fixed_environ) - - -class FlupFCGIServer(ServerAdapter): - def run(self, handler): # pragma: no cover - import flup.server.fcgi - kwargs = {'bindAddress':(self.host, self.port)} - kwargs.update(self.options) # allow to override bindAddress and others - flup.server.fcgi.WSGIServer(handler, **kwargs).run() - - -class WSGIRefServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from wsgiref.simple_server import make_server, WSGIRequestHandler - if self.quiet: - class QuietHandler(WSGIRequestHandler): - def log_request(*args, **kw): pass - self.options['handler_class'] = QuietHandler - srv = make_server(self.host, self.port, handler, **self.options) - srv.serve_forever() - - -class CherryPyServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from cherrypy import wsgiserver - server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) - try: - server.start() - finally: - server.stop() - -class PasteServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from paste import httpserver - if not self.quiet: - from paste.translogger import TransLogger - handler = TransLogger(handler) - httpserver.serve(handler, host=self.host, port=str(self.port), - **self.options) - -class MeinheldServer(ServerAdapter): - def run(self, handler): - from meinheld import server - server.listen((self.host, self.port)) - server.run(handler) - - -class FapwsServer(ServerAdapter): - """ Extremely fast webserver using libev. See http://www.fapws.org/ """ - def run(self, handler): # pragma: no cover - import fapws._evwsgi as evwsgi - from fapws import base, config - port = self.port - if float(config.SERVER_IDENT[-2:]) > 0.4: - # fapws3 silently changed its API in 0.5 - port = str(port) - evwsgi.start(self.host, port) - # fapws3 never releases the GIL. Complain upstream. I tried. No luck. - if 'BOTTLE_CHILD' in os.environ and not self.quiet: - print "WARNING: Auto-reloading does not work with Fapws3." - print " (Fapws3 breaks python thread support)" - evwsgi.set_base_module(base) - def app(environ, start_response): - environ['wsgi.multiprocess'] = False - return handler(environ, start_response) - evwsgi.wsgi_cb(('', app)) - evwsgi.run() - - -class TornadoServer(ServerAdapter): - """ The super hyped asynchronous server by facebook. Untested. """ - def run(self, handler): # pragma: no cover - import tornado.wsgi - import tornado.httpserver - import tornado.ioloop - container = tornado.wsgi.WSGIContainer(handler) - server = tornado.httpserver.HTTPServer(container) - server.listen(port=self.port) - tornado.ioloop.IOLoop.instance().start() - - -class AppEngineServer(ServerAdapter): - """ Adapter for Google App Engine. """ - quiet = True - def run(self, handler): - from google.appengine.ext.webapp import util - # A main() function in the handler script enables 'App Caching'. - # Lets makes sure it is there. This _really_ improves performance. - module = sys.modules.get('__main__') - if module and not hasattr(module, 'main'): - module.main = lambda: util.run_wsgi_app(handler) - util.run_wsgi_app(handler) - - -class TwistedServer(ServerAdapter): - """ Untested. """ - def run(self, handler): - from twisted.web import server, wsgi - from twisted.python.threadpool import ThreadPool - from twisted.internet import reactor - thread_pool = ThreadPool() - thread_pool.start() - reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) - factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) - reactor.listenTCP(self.port, factory, interface=self.host) - reactor.run() - - -class DieselServer(ServerAdapter): - """ Untested. """ - def run(self, handler): - from diesel.protocols.wsgi import WSGIApplication - app = WSGIApplication(handler, port=self.port) - app.run() - - -class GeventServer(ServerAdapter): - """ Untested. Options: - - * `monkey` (default: True) fixes the stdlib to use greenthreads. - * `fast` (default: False) uses libevent's http server, but has some - issues: No streaming, no pipelining, no SSL. - """ - def run(self, handler): - from gevent import wsgi as wsgi_fast, pywsgi as wsgi, monkey - if self.options.get('monkey', True): - monkey.patch_all() - if self.options.get('fast', False): - wsgi = wsgi_fast - wsgi.WSGIServer((self.host, self.port), handler).serve_forever() - - -class GunicornServer(ServerAdapter): - """ Untested. """ - def run(self, handler): - from gunicorn.arbiter import Arbiter - from gunicorn.config import Config - handler.cfg = Config({'bind': "%s:%d" % (self.host, self.port), 'workers': 4}) - arbiter = Arbiter(handler) - arbiter.run() - - -class EventletServer(ServerAdapter): - """ Untested """ - def run(self, handler): - from eventlet import wsgi, listen - wsgi.server(listen((self.host, self.port)), handler) - - -class RocketServer(ServerAdapter): - """ Untested. As requested in issue 63 - https://github.com/defnull/bottle/issues/#issue/63 """ - def run(self, handler): - from rocket import Rocket - server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) - server.start() - - -class BjoernServer(ServerAdapter): - """ Screamingly fast server written in C: https://github.com/jonashaag/bjoern """ - def run(self, handler): - from bjoern import run - run(handler, self.host, self.port) - - -class AutoServer(ServerAdapter): - """ Untested. """ - adapters = [PasteServer, CherryPyServer, TwistedServer, WSGIRefServer] - def run(self, handler): - for sa in self.adapters: - try: - return sa(self.host, self.port, **self.options).run(handler) - except ImportError: - pass - - -server_names = { - 'cgi': CGIServer, - 'flup': FlupFCGIServer, - 'wsgiref': WSGIRefServer, - 'cherrypy': CherryPyServer, - 'paste': PasteServer, - 'fapws3': FapwsServer, - 'tornado': TornadoServer, - 'gae': AppEngineServer, - 'twisted': TwistedServer, - 'diesel': DieselServer, - 'meinheld': MeinheldServer, - 'gunicorn': GunicornServer, - 'eventlet': EventletServer, - 'gevent': GeventServer, - 'rocket': RocketServer, - 'bjoern' : BjoernServer, - 'auto': AutoServer, -} - - - - - - -############################################################################### -# Application Control ########################################################## -############################################################################### - - -def _load(target, **vars): - """ Fetch something from a module. The exact behaviour depends on the the - target string: - - If the target is a valid python import path (e.g. `package.module`), - the rightmost part is returned as a module object. - If the target contains a colon (e.g. `package.module:var`) the module - variable specified after the colon is returned. - If the part after the colon contains any non-alphanumeric characters - (e.g. `package.module:func(var)`) the result of the expression - is returned. The expression has access to keyword arguments supplied - to this function. - - Example:: - >>> _load('bottle') - <module 'bottle' from 'bottle.py'> - >>> _load('bottle:Bottle') - <class 'bottle.Bottle'> - >>> _load('bottle:cookie_encode(v, secret)', v='foo', secret='bar') - '!F+hN4dQxaDJ4QxxaZ+Z3jw==?gAJVA2Zvb3EBLg==' - - """ - module, target = target.split(":", 1) if ':' in target else (target, None) - if module not in sys.modules: - __import__(module) - if not target: - return sys.modules[module] - if target.isalnum(): - return getattr(sys.modules[module], target) - package_name = module.split('.')[0] - vars[package_name] = sys.modules[package_name] - return eval('%s.%s' % (module, target), vars) - - -def load_app(target): - """ Load a bottle application based on a target string and return the - application object. - - If the target is an import path (e.g. package.module), the application - stack is used to isolate the routes defined in that module. - If the target contains a colon (e.g. package.module:myapp) the - module variable specified after the colon is returned instead. - """ - tmp = app.push() # Create a new "default application" - rv = _load(target) # Import the target module - app.remove(tmp) # Remove the temporary added default application - return rv if isinstance(rv, Bottle) else tmp - - -def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, - interval=1, reloader=False, quiet=False, **kargs): - """ Start a server instance. This method blocks until the server terminates. - - :param app: WSGI application or target string supported by - :func:`load_app`. (default: :func:`default_app`) - :param server: Server adapter to use. See :data:`server_names` keys - for valid names or pass a :class:`ServerAdapter` subclass. - (default: `wsgiref`) - :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on - all interfaces including the external one. (default: 127.0.0.1) - :param port: Server port to bind to. Values below 1024 require root - privileges. (default: 8080) - :param reloader: Start auto-reloading server? (default: False) - :param interval: Auto-reloader interval in seconds (default: 1) - :param quiet: Suppress output to stdout and stderr? (default: False) - :param options: Options passed to the server adapter. - """ - app = app or default_app() - if isinstance(app, basestring): - app = load_app(app) - if isinstance(server, basestring): - server = server_names.get(server) - if isinstance(server, type): - server = server(host=host, port=port, **kargs) - if not isinstance(server, ServerAdapter): - raise RuntimeError("Server must be a subclass of ServerAdapter") - server.quiet = server.quiet or quiet - if not server.quiet and not os.environ.get('BOTTLE_CHILD'): - print "Bottle server starting up (using %s)..." % repr(server) - print "Listening on http://%s:%d/" % (server.host, server.port) - print "Use Ctrl-C to quit." - print - try: - if reloader: - interval = min(interval, 1) - if os.environ.get('BOTTLE_CHILD'): - _reloader_child(server, app, interval) - else: - _reloader_observer(server, app, interval) - else: - server.run(app) - except KeyboardInterrupt: - pass - if not server.quiet and not os.environ.get('BOTTLE_CHILD'): - print "Shutting down..." - - -class FileCheckerThread(threading.Thread): - ''' Thread that periodically checks for changed module files. ''' - - def __init__(self, lockfile, interval): - threading.Thread.__init__(self) - self.lockfile, self.interval = lockfile, interval - #1: lockfile to old; 2: lockfile missing - #3: module file changed; 5: external exit - self.status = 0 - - def run(self): - exists = os.path.exists - mtime = lambda path: os.stat(path).st_mtime - files = dict() - for module in sys.modules.values(): - path = getattr(module, '__file__', '') - if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] - if path and exists(path): files[path] = mtime(path) - while not self.status: - for path, lmtime in files.iteritems(): - if not exists(path) or mtime(path) > lmtime: - self.status = 3 - if not exists(self.lockfile): - self.status = 2 - elif mtime(self.lockfile) < time.time() - self.interval - 5: - self.status = 1 - if not self.status: - time.sleep(self.interval) - if self.status != 5: - thread.interrupt_main() - - -def _reloader_child(server, app, interval): - ''' Start the server and check for modified files in a background thread. - As soon as an update is detected, KeyboardInterrupt is thrown in - the main thread to exit the server loop. The process exists with status - code 3 to request a reload by the observer process. If the lockfile - is not modified in 2*interval second or missing, we assume that the - observer process died and exit with status code 1 or 2. - ''' - lockfile = os.environ.get('BOTTLE_LOCKFILE') - bgcheck = FileCheckerThread(lockfile, interval) - try: - bgcheck.start() - server.run(app) - except KeyboardInterrupt: - pass - bgcheck.status, status = 5, bgcheck.status - bgcheck.join() # bgcheck.status == 5 --> silent exit - if status: sys.exit(status) - - -def _reloader_observer(server, app, interval): - ''' Start a child process with identical commandline arguments and restart - it as long as it exists with status code 3. Also create a lockfile and - touch it (update mtime) every interval seconds. - ''' - fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock') - os.close(fd) # We only need this file to exist. We never write to it - try: - while os.path.exists(lockfile): - args = [sys.executable] + sys.argv - environ = os.environ.copy() - environ['BOTTLE_CHILD'] = 'true' - environ['BOTTLE_LOCKFILE'] = lockfile - p = subprocess.Popen(args, env=environ) - while p.poll() is None: # Busy wait... - os.utime(lockfile, None) # I am alive! - time.sleep(interval) - if p.poll() != 3: - if os.path.exists(lockfile): os.unlink(lockfile) - sys.exit(p.poll()) - elif not server.quiet: - print "Reloading server..." - except KeyboardInterrupt: - pass - if os.path.exists(lockfile): os.unlink(lockfile) - - - - - - -############################################################################### -# Template Adapters ############################################################ -############################################################################### - - -class TemplateError(HTTPError): - def __init__(self, message): - HTTPError.__init__(self, 500, message) - - -class BaseTemplate(object): - """ Base class and minimal API for template adapters """ - extentions = ['tpl','html','thtml','stpl'] - settings = {} #used in prepare() - defaults = {} #used in render() - - def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings): - """ Create a new template. - If the source parameter (str or buffer) is missing, the name argument - is used to guess a template filename. Subclasses can assume that - self.source and/or self.filename are set. Both are strings. - The lookup, encoding and settings parameters are stored as instance - variables. - The lookup parameter stores a list containing directory paths. - The encoding parameter should be used to decode byte strings or files. - The settings parameter contains a dict for engine-specific settings. - """ - self.name = name - self.source = source.read() if hasattr(source, 'read') else source - self.filename = source.filename if hasattr(source, 'filename') else None - self.lookup = map(os.path.abspath, lookup) - self.encoding = encoding - self.settings = self.settings.copy() # Copy from class variable - self.settings.update(settings) # Apply - if not self.source and self.name: - self.filename = self.search(self.name, self.lookup) - if not self.filename: - raise TemplateError('Template %s not found.' % repr(name)) - if not self.source and not self.filename: - raise TemplateError('No template specified.') - self.prepare(**self.settings) - - @classmethod - def search(cls, name, lookup=[]): - """ Search name in all directories specified in lookup. - First without, then with common extensions. Return first hit. """ - if os.path.isfile(name): return name - for spath in lookup: - fname = os.path.join(spath, name) - if os.path.isfile(fname): - return fname - for ext in cls.extentions: - if os.path.isfile('%s.%s' % (fname, ext)): - return '%s.%s' % (fname, ext) - - @classmethod - def global_config(cls, key, *args): - ''' This reads or sets the global settings stored in class.settings. ''' - if args: - cls.settings[key] = args[0] - else: - return cls.settings[key] - - def prepare(self, **options): - """ Run preparations (parsing, caching, ...). - It should be possible to call this again to refresh a template or to - update settings. - """ - raise NotImplementedError - - def render(self, *args, **kwargs): - """ Render the template with the specified local variables and return - a single byte or unicode string. If it is a byte string, the encoding - must match self.encoding. This method must be thread-safe! - Local variables may be provided in dictionaries (*args) - or directly, as keywords (**kwargs). - """ - raise NotImplementedError - - -class MakoTemplate(BaseTemplate): - def prepare(self, **options): - from mako.template import Template - from mako.lookup import TemplateLookup - options.update({'input_encoding':self.encoding}) - options.setdefault('format_exceptions', bool(DEBUG)) - lookup = TemplateLookup(directories=self.lookup, **options) - if self.source: - self.tpl = Template(self.source, lookup=lookup, **options) - else: - self.tpl = Template(uri=self.name, filename=self.filename, lookup=lookup, **options) - - def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) - _defaults = self.defaults.copy() - _defaults.update(kwargs) - return self.tpl.render(**_defaults) - - -class CheetahTemplate(BaseTemplate): - def prepare(self, **options): - from Cheetah.Template import Template - self.context = threading.local() - self.context.vars = {} - options['searchList'] = [self.context.vars] - if self.source: - self.tpl = Template(source=self.source, **options) - else: - self.tpl = Template(file=self.filename, **options) - - def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) - self.context.vars.update(self.defaults) - self.context.vars.update(kwargs) - out = str(self.tpl) - self.context.vars.clear() - return [out] - - -class Jinja2Template(BaseTemplate): - def prepare(self, filters=None, tests=None, **kwargs): - from jinja2 import Environment, FunctionLoader - if 'prefix' in kwargs: # TODO: to be removed after a while - raise RuntimeError('The keyword argument `prefix` has been removed. ' - 'Use the full jinja2 environment name line_statement_prefix instead.') - self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) - if filters: self.env.filters.update(filters) - if tests: self.env.tests.update(tests) - if self.source: - self.tpl = self.env.from_string(self.source) - else: - self.tpl = self.env.get_template(self.filename) - - def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) - _defaults = self.defaults.copy() - _defaults.update(kwargs) - return self.tpl.render(**_defaults).encode("utf-8") - - def loader(self, name): - fname = self.search(name, self.lookup) - if fname: - with open(fname, "rb") as f: - return f.read().decode(self.encoding) - - -class SimpleTALTemplate(BaseTemplate): - ''' Untested! ''' - def prepare(self, **options): - from simpletal import simpleTAL - # TODO: add option to load METAL files during render - if self.source: - self.tpl = simpleTAL.compileHTMLTemplate(self.source) - else: - with open(self.filename, 'rb') as fp: - self.tpl = simpleTAL.compileHTMLTemplate(tonat(fp.read())) - - def render(self, *args, **kwargs): - from simpletal import simpleTALES - from StringIO import StringIO - for dictarg in args: kwargs.update(dictarg) - # TODO: maybe reuse a context instead of always creating one - context = simpleTALES.Context() - for k,v in self.defaults.items(): - context.addGlobal(k, v) - for k,v in kwargs.items(): - context.addGlobal(k, v) - output = StringIO() - self.tpl.expand(context, output) - return output.getvalue() - - -class SimpleTemplate(BaseTemplate): - blocks = ('if','elif','else','try','except','finally','for','while','with','def','class') - dedent_blocks = ('elif', 'else', 'except', 'finally') - - @lazy_attribute - def re_pytokens(cls): - ''' This matches comments and all kinds of quoted strings but does - NOT match comments (#...) within quoted strings. (trust me) ''' - return re.compile(r''' - (''(?!')|""(?!")|'{6}|"{6} # Empty strings (all 4 types) - |'(?:[^\\']|\\.)+?' # Single quotes (') - |"(?:[^\\"]|\\.)+?" # Double quotes (") - |'{3}(?:[^\\]|\\.|\n)+?'{3} # Triple-quoted strings (') - |"{3}(?:[^\\]|\\.|\n)+?"{3} # Triple-quoted strings (") - |\#.* # Comments - )''', re.VERBOSE) - - def prepare(self, escape_func=cgi.escape, noescape=False): - self.cache = {} - enc = self.encoding - self._str = lambda x: touni(x, enc) - self._escape = lambda x: escape_func(touni(x, enc)) - if noescape: - self._str, self._escape = self._escape, self._str - - @classmethod - def split_comment(cls, code): - """ Removes comments (#...) from python code. """ - if '#' not in code: return code - #: Remove comments only (leave quoted strings as they are) - subf = lambda m: '' if m.group(0)[0]=='#' else m.group(0) - return re.sub(cls.re_pytokens, subf, code) - - @cached_property - def co(self): - return compile(self.code, self.filename or '<string>', 'exec') - - @cached_property - def code(self): - stack = [] # Current Code indentation - lineno = 0 # Current line of code - ptrbuffer = [] # Buffer for printable strings and token tuple instances - codebuffer = [] # Buffer for generated python code - multiline = dedent = oneline = False - template = self.source if self.source else open(self.filename).read() - - def yield_tokens(line): - for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): - if i % 2: - if part.startswith('!'): yield 'RAW', part[1:] - else: yield 'CMD', part - else: yield 'TXT', part - - def flush(): # Flush the ptrbuffer - if not ptrbuffer: return - cline = '' - for line in ptrbuffer: - for token, value in line: - if token == 'TXT': cline += repr(value) - elif token == 'RAW': cline += '_str(%s)' % value - elif token == 'CMD': cline += '_escape(%s)' % value - cline += ', ' - cline = cline[:-2] + '\\\n' - cline = cline[:-2] - if cline[:-1].endswith('\\\\\\\\\\n'): - cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr' - cline = '_printlist([' + cline + '])' - del ptrbuffer[:] # Do this before calling code() again - code(cline) - - def code(stmt): - for line in stmt.splitlines(): - codebuffer.append(' ' * len(stack) + line.strip()) - - for line in template.splitlines(True): - lineno += 1 - line = line if isinstance(line, unicode)\ - else unicode(line, encoding=self.encoding) - if lineno <= 2: - m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line) - if m: self.encoding = m.group(1) - if m: line = line.replace('coding','coding (removed)') - if line.strip()[:2].count('%') == 1: - line = line.split('%',1)[1].lstrip() # Full line following the % - cline = self.split_comment(line).strip() - cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] - flush() ##encodig (TODO: why?) - if cmd in self.blocks or multiline: - cmd = multiline or cmd - dedent = cmd in self.dedent_blocks # "else:" - if dedent and not oneline and not multiline: - cmd = stack.pop() - code(line) - oneline = not cline.endswith(':') # "if 1: pass" - multiline = cmd if cline.endswith('\\') else False - if not oneline and not multiline: - stack.append(cmd) - elif cmd == 'end' and stack: - code('#end(%s) %s' % (stack.pop(), line.strip()[3:])) - elif cmd == 'include': - p = cline.split(None, 2)[1:] - if len(p) == 2: - code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1])) - elif p: - code("_=_include(%s, _stdout)" % repr(p[0])) - else: # Empty %include -> reverse of %rebase - code("_printlist(_base)") - elif cmd == 'rebase': - p = cline.split(None, 2)[1:] - if len(p) == 2: - code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) - elif p: - code("globals()['_rebase']=(%s, {})" % repr(p[0])) - else: - code(line) - else: # Line starting with text (not '%') or '%%' (escaped) - if line.strip().startswith('%%'): - line = line.replace('%%', '%', 1) - ptrbuffer.append(yield_tokens(line)) - flush() - return '\n'.join(codebuffer) + '\n' - - def subtemplate(self, _name, _stdout, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) - if _name not in self.cache: - self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) - return self.cache[_name].execute(_stdout, kwargs) - - def execute(self, _stdout, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) - env = self.defaults.copy() - env.update({'_stdout': _stdout, '_printlist': _stdout.extend, - '_include': self.subtemplate, '_str': self._str, - '_escape': self._escape}) - env.update(kwargs) - eval(self.co, env) - if '_rebase' in env: - subtpl, rargs = env['_rebase'] - subtpl = self.__class__(name=subtpl, lookup=self.lookup) - rargs['_base'] = _stdout[:] #copy stdout - del _stdout[:] # clear stdout - return subtpl.execute(_stdout, rargs) - return env - - def render(self, *args, **kwargs): - """ Render the template using keyword arguments as local variables. """ - for dictarg in args: kwargs.update(dictarg) - stdout = [] - self.execute(stdout, kwargs) - return ''.join(stdout) - - -def template(*args, **kwargs): - ''' - Get a rendered template as a string iterator. - You can use a name, a filename or a template string as first parameter. - Template rendering arguments can be passed as dictionaries - or directly (as keyword arguments). - ''' - tpl = args[0] if args else None - template_adapter = kwargs.pop('template_adapter', SimpleTemplate) - if tpl not in TEMPLATES or DEBUG: - settings = kwargs.pop('template_settings', {}) - lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) - if isinstance(tpl, template_adapter): - TEMPLATES[tpl] = tpl - if settings: TEMPLATES[tpl].prepare(**settings) - elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: - TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) - else: - TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) - if not TEMPLATES[tpl]: - abort(500, 'Template (%s) not found' % tpl) - for dictarg in args[1:]: kwargs.update(dictarg) - return TEMPLATES[tpl].render(kwargs) - -mako_template = functools.partial(template, template_adapter=MakoTemplate) -cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) -jinja2_template = functools.partial(template, template_adapter=Jinja2Template) -simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate) - - -def view(tpl_name, **defaults): - ''' Decorator: renders a template for a handler. - The handler can control its behavior like that: - - - return a dict of template vars to fill out the template - - return something other than a dict and the view decorator will not - process the template, but return the handler result as is. - This includes returning a HTTPResponse(dict) to get, - for instance, JSON with autojson or other castfilters. - ''' - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - result = func(*args, **kwargs) - if isinstance(result, (dict, DictMixin)): - tplvars = defaults.copy() - tplvars.update(result) - return template(tpl_name, **tplvars) - return result - return wrapper - return decorator - -mako_view = functools.partial(view, template_adapter=MakoTemplate) -cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) -jinja2_view = functools.partial(view, template_adapter=Jinja2Template) -simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate) - - - - - - -############################################################################### -# Constants and Globals ######################################################## -############################################################################### - - -TEMPLATE_PATH = ['./', './views/'] -TEMPLATES = {} -DEBUG = False -MEMFILE_MAX = 1024*100 - -#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') -HTTP_CODES = httplib.responses -HTTP_CODES[418] = "I'm a teapot" # RFC 2324 - -#: The default template used for error pages. Override with @error() -ERROR_PAGE_TEMPLATE = """ -%try: - %from bottle import DEBUG, HTTP_CODES, request, touni - %status_name = HTTP_CODES.get(e.status, 'Unknown').title() - <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> - <html> - <head> - <title>Error {{e.status}}: {{status_name}}</title> - <style type="text/css"> - html {background-color: #eee; font-family: sans;} - body {background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 15px;} - pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} - </style> - </head> - <body> - <h1>Error {{e.status}}: {{status_name}}</h1> - <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> caused an error:</p> - <pre>{{e.output}}</pre> - %if DEBUG and e.exception: - <h2>Exception:</h2> - <pre>{{repr(e.exception)}}</pre> - %end - %if DEBUG and e.traceback: - <h2>Traceback:</h2> - <pre>{{e.traceback}}</pre> - %end - </body> - </html> -%except ImportError: - <b>ImportError:</b> Could not generate the error page. Please add bottle to sys.path -%end -""" - -#: A thread-save instance of :class:`Request` representing the `current` request. -request = Request() - -#: A thread-save instance of :class:`Response` used to build the HTTP response. -response = Response() - -#: A thread-save namepsace. Not used by Bottle. -local = threading.local() - -# Initialize app stack (create first empty Bottle app) -# BC: 0.6.4 and needed for run() -app = default_app = AppStack() -app.push() - -#: A virtual package that redirects import statements. -#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module diff --git a/server/favicon.ico b/server/favicon.ico Binary files differdeleted file mode 100644 index 7904b54..0000000 --- a/server/favicon.ico +++ /dev/null diff --git a/server/index.html b/server/index.html deleted file mode 100644 index 3273a1f..0000000 --- a/server/index.html +++ /dev/null @@ -1,15 +0,0 @@ -<!doctype html> - -<html> -Hello world - -<form action="convert" method="POST"> - <textarea cols=80 rows=30 name="data"> - EXT. SOMEWHERE - DAY - - SOME GUY appears out of nowhere. - </textarea> - <input type=submit> -</form> - -</html> diff --git a/server/main.py b/server/main.py deleted file mode 100644 index f3f5e40..0000000 --- a/server/main.py +++ /dev/null @@ -1,34 +0,0 @@ - -# See http://blog.rutwick.com/use-bottle-python-framework-with-google-app-engine - -from StringIO import StringIO - -import bottle -from bottle import route, template, request, response, error, debug -from google.appengine.ext.webapp.util import run_wsgi_app - -from screenplain.export.text import to_text -from screenplain.export.annotated_html import to_annotated_html - -@route('/convert', method='POST') -def DisplayForm(): - response.content_type = 'text/html; charset=utf-8' - input = StringIO(request.forms.get('data')) - output = StringIO() - to_annotated_html(input, output) - return output.getvalue() - -def main(): - debug(True) - run_wsgi_app(bottle.default_app()) - -@error(403) -def Error403(code): - return 'Forbidden!' - -@error(404) -def Error404(code): - return 'Not found' - -if __name__=="__main__": - main() diff --git a/server/style.css b/server/style.css deleted file mode 100644 index 19756f6..0000000 --- a/server/style.css +++ /dev/null @@ -1,64 +0,0 @@ -html, body, div, span, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, -small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, figcaption, figure, -footer, header, hgroup, menu, nav, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} - -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { - display: block; -} - -body { - background: #ccc; - font-family: Courier; - font-size: 12pt; -} - -.block { - margin-left: 20px; - margin-right: 20px; - position: relative; -} - -.type { - position: absolute; - left: 0; - font-family: serif; - font-size: 11pt; - border-radius: .2em; - color: #fff; -} - -.block > p { - background: #fff; - margin-left: 80px; - max-width: 68em; -} - -.typeSlug { - background: #f84; -} -.typeDialog { - background: #080; -} -.typeDual { - background: #088; -} -.typeAction { - background: #333; -} -.typeTransition { - background: #888; -} |