sean@0: #!/usr/bin/env python sean@0: # -*- coding: utf-8 -*- sean@0: """ sean@0: Bottle is a fast and simple micro-framework for small web applications. It sean@0: offers request dispatching (Routes) with url parameter support, templates, sean@0: a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and sean@0: template engines - all in a single file and with no dependencies other than the sean@0: Python Standard Library. sean@0: sean@0: Homepage and documentation: http://bottlepy.org/ sean@0: sean@0: Copyright (c) 2014, Marcel Hellkamp. sean@0: License: MIT (see LICENSE for details) sean@0: """ sean@0: sean@0: from __future__ import with_statement sean@0: sean@0: __author__ = 'Marcel Hellkamp' sean@0: __version__ = '0.13-dev' sean@0: __license__ = 'MIT' sean@0: sean@0: # The gevent and eventlet server adapters need to patch some modules before sean@0: # they are imported. This is why we parse the commandline parameters here but sean@0: # handle them later sean@0: if __name__ == '__main__': sean@0: from optparse import OptionParser sean@0: _cmd_parser = OptionParser( sean@0: usage="usage: %prog [options] package.module:app") sean@0: _opt = _cmd_parser.add_option sean@0: _opt("--version", action="store_true", help="show version number.") sean@0: _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") sean@0: _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") sean@0: _opt("-p", "--plugin", sean@0: action="append", sean@0: help="install additional plugin/s.") sean@0: _opt("--debug", action="store_true", help="start server in debug mode.") sean@0: _opt("--reload", action="store_true", help="auto-reload on file changes.") sean@0: _cmd_options, _cmd_args = _cmd_parser.parse_args() sean@0: if _cmd_options.server: sean@0: if _cmd_options.server.startswith('gevent'): sean@0: import gevent.monkey sean@0: gevent.monkey.patch_all() sean@0: elif _cmd_options.server.startswith('eventlet'): sean@0: import eventlet sean@0: eventlet.monkey_patch() sean@0: sean@0: import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ sean@0: os, re, sys, tempfile, threading, time, warnings sean@0: sean@0: from types import FunctionType sean@0: from datetime import date as datedate, datetime, timedelta sean@0: from tempfile import TemporaryFile sean@0: from traceback import format_exc, print_exc sean@0: from inspect import getargspec sean@0: from unicodedata import normalize sean@0: sean@0: try: sean@0: from simplejson import dumps as json_dumps, loads as json_lds sean@0: except ImportError: # pragma: no cover sean@0: try: sean@0: from json import dumps as json_dumps, loads as json_lds sean@0: except ImportError: sean@0: try: sean@0: from django.utils.simplejson import dumps as json_dumps, loads as json_lds sean@0: except ImportError: sean@0: sean@0: def json_dumps(data): sean@0: raise ImportError( sean@0: "JSON support requires Python 2.6 or simplejson.") sean@0: sean@0: json_lds = json_dumps sean@0: sean@0: # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. sean@0: # It ain't pretty but it works... Sorry for the mess. sean@0: sean@0: py = sys.version_info sean@0: py3k = py >= (3, 0, 0) sean@0: py25 = py < (2, 6, 0) sean@0: py31 = (3, 1, 0) <= py < (3, 2, 0) sean@0: sean@0: # Workaround for the missing "as" keyword in py3k. sean@0: def _e(): sean@0: return sys.exc_info()[1] sean@0: sean@0: # Workaround for the "print is a keyword/function" Python 2/3 dilemma sean@0: # and a fallback for mod_wsgi (resticts stdout/err attribute access) sean@0: try: sean@0: _stdout, _stderr = sys.stdout.write, sys.stderr.write sean@0: except IOError: sean@0: _stdout = lambda x: sys.stdout.write(x) sean@0: _stderr = lambda x: sys.stderr.write(x) sean@0: sean@0: # Lots of stdlib and builtin differences. sean@0: if py3k: sean@0: import http.client as httplib sean@0: import _thread as thread sean@0: from urllib.parse import urljoin, SplitResult as UrlSplitResult sean@0: from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote sean@0: urlunquote = functools.partial(urlunquote, encoding='latin1') sean@0: from http.cookies import SimpleCookie sean@0: from collections import MutableMapping as DictMixin sean@0: import pickle sean@0: from io import BytesIO sean@0: from configparser import ConfigParser sean@0: basestring = str sean@0: unicode = str sean@0: json_loads = lambda s: json_lds(touni(s)) sean@0: callable = lambda x: hasattr(x, '__call__') sean@0: imap = map sean@0: sean@0: def _raise(*a): sean@0: raise a[0](a[1]).with_traceback(a[2]) sean@0: else: # 2.x sean@0: import httplib sean@0: import thread sean@0: from urlparse import urljoin, SplitResult as UrlSplitResult sean@0: from urllib import urlencode, quote as urlquote, unquote as urlunquote sean@0: from Cookie import SimpleCookie sean@0: from itertools import imap sean@0: import cPickle as pickle sean@0: from StringIO import StringIO as BytesIO sean@0: from ConfigParser import SafeConfigParser as ConfigParser sean@0: if py25: sean@0: msg = "Python 2.5 support may be dropped in future versions of Bottle." sean@0: warnings.warn(msg, DeprecationWarning) sean@0: from UserDict import DictMixin sean@0: sean@0: def next(it): sean@0: return it.next() sean@0: sean@0: bytes = str sean@0: else: # 2.6, 2.7 sean@0: from collections import MutableMapping as DictMixin sean@0: unicode = unicode sean@0: json_loads = json_lds sean@0: eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) sean@0: sean@0: sean@0: # Some helpers for string/byte handling sean@0: def tob(s, enc='utf8'): sean@0: return s.encode(enc) if isinstance(s, unicode) else bytes(s) sean@0: sean@0: sean@0: def touni(s, enc='utf8', err='strict'): sean@0: if isinstance(s, bytes): sean@0: return s.decode(enc, err) sean@0: else: sean@0: return unicode(s or ("" if s is None else s)) sean@0: sean@0: sean@0: tonat = touni if py3k else tob sean@0: sean@0: # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). sean@0: # 3.1 needs a workaround. sean@0: if py31: sean@0: from io import TextIOWrapper sean@0: sean@0: class NCTextIOWrapper(TextIOWrapper): sean@0: def close(self): sean@0: pass # Keep wrapped buffer open. sean@0: sean@0: sean@0: # A bug in functools causes it to break if the wrapper is an instance method sean@0: def update_wrapper(wrapper, wrapped, *a, **ka): sean@0: try: sean@0: functools.update_wrapper(wrapper, wrapped, *a, **ka) sean@0: except AttributeError: sean@0: pass sean@0: sean@0: # These helpers are used at module level and need to be defined first. sean@0: # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. sean@0: sean@0: sean@0: def depr(message, strict=False): sean@0: warnings.warn(message, DeprecationWarning, stacklevel=3) sean@0: sean@0: sean@0: def makelist(data): # This is just too handy sean@0: if isinstance(data, (tuple, list, set, dict)): sean@0: return list(data) sean@0: elif data: sean@0: return [data] sean@0: else: sean@0: return [] sean@0: sean@0: sean@0: class DictProperty(object): sean@0: """ Property that maps to a key in a local dict-like attribute. """ sean@0: sean@0: def __init__(self, attr, key=None, read_only=False): sean@0: self.attr, self.key, self.read_only = attr, key, read_only sean@0: sean@0: def __call__(self, func): sean@0: functools.update_wrapper(self, func, updated=[]) sean@0: self.getter, self.key = func, self.key or func.__name__ sean@0: return self sean@0: sean@0: def __get__(self, obj, cls): sean@0: if obj is None: return self sean@0: key, storage = self.key, getattr(obj, self.attr) sean@0: if key not in storage: storage[key] = self.getter(obj) sean@0: return storage[key] sean@0: sean@0: def __set__(self, obj, value): sean@0: if self.read_only: raise AttributeError("Read-Only property.") sean@0: getattr(obj, self.attr)[self.key] = value sean@0: sean@0: def __delete__(self, obj): sean@0: if self.read_only: raise AttributeError("Read-Only property.") sean@0: del getattr(obj, self.attr)[self.key] sean@0: sean@0: sean@0: class cached_property(object): sean@0: """ A property that is only computed once per instance and then replaces sean@0: itself with an ordinary attribute. Deleting the attribute resets the sean@0: property. """ sean@0: sean@0: def __init__(self, func): sean@0: self.__doc__ = getattr(func, '__doc__') sean@0: self.func = func sean@0: sean@0: def __get__(self, obj, cls): sean@0: if obj is None: return self sean@0: value = obj.__dict__[self.func.__name__] = self.func(obj) sean@0: return value sean@0: sean@0: sean@0: class lazy_attribute(object): sean@0: """ A property that caches itself to the class object. """ sean@0: sean@0: def __init__(self, func): sean@0: functools.update_wrapper(self, func, updated=[]) sean@0: self.getter = func sean@0: sean@0: def __get__(self, obj, cls): sean@0: value = self.getter(cls) sean@0: setattr(cls, self.__name__, value) sean@0: return value sean@0: sean@0: ############################################################################### sean@0: # Exceptions and Events ######################################################## sean@0: ############################################################################### sean@0: sean@0: sean@0: class BottleException(Exception): sean@0: """ A base class for exceptions used by bottle. """ sean@0: pass sean@0: sean@0: ############################################################################### sean@0: # Routing ###################################################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: class RouteError(BottleException): sean@0: """ This is a base class for all routing related exceptions """ sean@0: sean@0: sean@0: class RouteReset(BottleException): sean@0: """ If raised by a plugin or request handler, the route is reset and all sean@0: plugins are re-applied. """ sean@0: sean@0: sean@0: class RouterUnknownModeError(RouteError): sean@0: sean@0: pass sean@0: sean@0: sean@0: class RouteSyntaxError(RouteError): sean@0: """ The route parser found something not supported by this router. """ sean@0: sean@0: sean@0: class RouteBuildError(RouteError): sean@0: """ The route could not be built. """ sean@0: sean@0: sean@0: def _re_flatten(p): sean@0: """ Turn all capturing groups in a regular expression pattern into sean@0: non-capturing groups. """ sean@0: if '(' not in p: sean@0: return p sean@0: return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if sean@0: len(m.group(1)) % 2 else m.group(1) + '(?:', p) sean@0: sean@0: sean@0: class Router(object): sean@0: """ A Router is an ordered collection of route->target pairs. It is used to sean@0: efficiently match WSGI requests against a number of routes and return sean@0: the first target that satisfies the request. The target may be anything, sean@0: usually a string, ID or callable object. A route consists of a path-rule sean@0: and a HTTP method. sean@0: sean@0: The path-rule is either a static path (e.g. `/contact`) or a dynamic sean@0: path that contains wildcards (e.g. `/wiki/`). The wildcard syntax sean@0: and details on the matching order are described in docs:`routing`. sean@0: """ sean@0: sean@0: default_pattern = '[^/]+' sean@0: default_filter = 're' sean@0: sean@0: #: The current CPython regexp implementation does not allow more sean@0: #: than 99 matching groups per regular expression. sean@0: _MAX_GROUPS_PER_PATTERN = 99 sean@0: sean@0: def __init__(self, strict=False): sean@0: self.rules = [] # All rules in order sean@0: self._groups = {} # index of regexes to find them in dyna_routes sean@0: self.builder = {} # Data structure for the url builder sean@0: self.static = {} # Search structure for static routes sean@0: self.dyna_routes = {} sean@0: self.dyna_regexes = {} # Search structure for dynamic routes sean@0: #: If true, static routes are no longer checked first. sean@0: self.strict_order = strict sean@0: self.filters = { sean@0: 're': lambda conf: (_re_flatten(conf or self.default_pattern), sean@0: None, None), sean@0: 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), sean@0: 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), sean@0: 'path': lambda conf: (r'.+?', None, None) sean@0: } sean@0: sean@0: def add_filter(self, name, func): sean@0: """ Add a filter. The provided function is called with the configuration sean@0: string as parameter and must return a (regexp, to_python, to_url) tuple. sean@0: The first element is a string, the last two are callables or None. """ sean@0: self.filters[name] = func sean@0: sean@0: rule_syntax = re.compile('(\\\\*)' sean@0: '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' sean@0: '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' sean@0: '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') sean@0: sean@0: def _itertokens(self, rule): sean@0: offset, prefix = 0, '' sean@0: for match in self.rule_syntax.finditer(rule): sean@0: prefix += rule[offset:match.start()] sean@0: g = match.groups() sean@0: if len(g[0]) % 2: # Escaped wildcard sean@0: prefix += match.group(0)[len(g[0]):] sean@0: offset = match.end() sean@0: continue sean@0: if prefix: sean@0: yield prefix, None, None sean@0: name, filtr, conf = g[4:7] if g[2] is None else g[1:4] sean@0: yield name, filtr or 'default', conf or None sean@0: offset, prefix = match.end(), '' sean@0: if offset <= len(rule) or prefix: sean@0: yield prefix + rule[offset:], None, None sean@0: sean@0: def add(self, rule, method, target, name=None): sean@0: """ Add a new rule or replace the target for an existing rule. """ sean@0: anons = 0 # Number of anonymous wildcards found sean@0: keys = [] # Names of keys sean@0: pattern = '' # Regular expression pattern with named groups sean@0: filters = [] # Lists of wildcard input filters sean@0: builder = [] # Data structure for the URL builder sean@0: is_static = True sean@0: sean@0: for key, mode, conf in self._itertokens(rule): sean@0: if mode: sean@0: is_static = False sean@0: if mode == 'default': mode = self.default_filter sean@0: mask, in_filter, out_filter = self.filters[mode](conf) sean@0: if not key: sean@0: pattern += '(?:%s)' % mask sean@0: key = 'anon%d' % anons sean@0: anons += 1 sean@0: else: sean@0: pattern += '(?P<%s>%s)' % (key, mask) sean@0: keys.append(key) sean@0: if in_filter: filters.append((key, in_filter)) sean@0: builder.append((key, out_filter or str)) sean@0: elif key: sean@0: pattern += re.escape(key) sean@0: builder.append((None, key)) sean@0: sean@0: self.builder[rule] = builder sean@0: if name: self.builder[name] = builder sean@0: sean@0: if is_static and not self.strict_order: sean@0: self.static.setdefault(method, {}) sean@0: self.static[method][self.build(rule)] = (target, None) sean@0: return sean@0: sean@0: try: sean@0: re_pattern = re.compile('^(%s)$' % pattern) sean@0: re_match = re_pattern.match sean@0: except re.error: sean@0: raise RouteSyntaxError("Could not add Route: %s (%s)" % sean@0: (rule, _e())) sean@0: sean@0: if filters: sean@0: sean@0: def getargs(path): sean@0: url_args = re_match(path).groupdict() sean@0: for name, wildcard_filter in filters: sean@0: try: sean@0: url_args[name] = wildcard_filter(url_args[name]) sean@0: except ValueError: sean@0: raise HTTPError(400, 'Path has wrong format.') sean@0: return url_args sean@0: elif re_pattern.groupindex: sean@0: sean@0: def getargs(path): sean@0: return re_match(path).groupdict() sean@0: else: sean@0: getargs = None sean@0: sean@0: flatpat = _re_flatten(pattern) sean@0: whole_rule = (rule, flatpat, target, getargs) sean@0: sean@0: if (flatpat, method) in self._groups: sean@0: if DEBUG: sean@0: msg = 'Route <%s %s> overwrites a previously defined route' sean@0: warnings.warn(msg % (method, rule), RuntimeWarning) sean@0: self.dyna_routes[method][ sean@0: self._groups[flatpat, method]] = whole_rule sean@0: else: sean@0: self.dyna_routes.setdefault(method, []).append(whole_rule) sean@0: self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 sean@0: sean@0: self._compile(method) sean@0: sean@0: def _compile(self, method): sean@0: all_rules = self.dyna_routes[method] sean@0: comborules = self.dyna_regexes[method] = [] sean@0: maxgroups = self._MAX_GROUPS_PER_PATTERN sean@0: for x in range(0, len(all_rules), maxgroups): sean@0: some = all_rules[x:x + maxgroups] sean@0: combined = (flatpat for (_, flatpat, _, _) in some) sean@0: combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) sean@0: combined = re.compile(combined).match sean@0: rules = [(target, getargs) for (_, _, target, getargs) in some] sean@0: comborules.append((combined, rules)) sean@0: sean@0: def build(self, _name, *anons, **query): sean@0: """ Build an URL by filling the wildcards in a rule. """ sean@0: builder = self.builder.get(_name) sean@0: if not builder: sean@0: raise RouteBuildError("No route with that name.", _name) sean@0: try: sean@0: for i, value in enumerate(anons): sean@0: query['anon%d' % i] = value sean@0: url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) sean@0: return url if not query else url + '?' + urlencode(query) sean@0: except KeyError: sean@0: raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) sean@0: sean@0: def match(self, environ): sean@0: """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ sean@0: verb = environ['REQUEST_METHOD'].upper() sean@0: path = environ['PATH_INFO'] or '/' sean@0: sean@0: if verb == 'HEAD': sean@0: methods = ['PROXY', verb, 'GET', 'ANY'] sean@0: else: sean@0: methods = ['PROXY', verb, 'ANY'] sean@0: sean@0: for method in methods: sean@0: if method in self.static and path in self.static[method]: sean@0: target, getargs = self.static[method][path] sean@0: return target, getargs(path) if getargs else {} sean@0: elif method in self.dyna_regexes: sean@0: for combined, rules in self.dyna_regexes[method]: sean@0: match = combined(path) sean@0: if match: sean@0: target, getargs = rules[match.lastindex - 1] sean@0: return target, getargs(path) if getargs else {} sean@0: sean@0: # No matching route found. Collect alternative methods for 405 response sean@0: allowed = set([]) sean@0: nocheck = set(methods) sean@0: for method in set(self.static) - nocheck: sean@0: if path in self.static[method]: sean@0: allowed.add(verb) sean@0: for method in set(self.dyna_regexes) - allowed - nocheck: sean@0: for combined, rules in self.dyna_regexes[method]: sean@0: match = combined(path) sean@0: if match: sean@0: allowed.add(method) sean@0: if allowed: sean@0: allow_header = ",".join(sorted(allowed)) sean@0: raise HTTPError(405, "Method not allowed.", Allow=allow_header) sean@0: sean@0: # No matching route and no alternative method found. We give up sean@0: raise HTTPError(404, "Not found: " + repr(path)) sean@0: sean@0: sean@0: class Route(object): sean@0: """ This class wraps a route callback along with route specific metadata and sean@0: configuration and applies Plugins on demand. It is also responsible for sean@0: turing an URL path rule into a regular expression usable by the Router. sean@0: """ sean@0: sean@0: def __init__(self, app, rule, method, callback, sean@0: name=None, sean@0: plugins=None, sean@0: skiplist=None, **config): sean@0: #: The application this route is installed to. sean@0: self.app = app sean@0: #: The path-rule string (e.g. ``/wiki/``). sean@0: self.rule = rule sean@0: #: The HTTP method as a string (e.g. ``GET``). sean@0: self.method = method sean@0: #: The original callback with no plugins applied. Useful for introspection. sean@0: self.callback = callback sean@0: #: The name of the route (if specified) or ``None``. sean@0: self.name = name or None sean@0: #: A list of route-specific plugins (see :meth:`Bottle.route`). sean@0: self.plugins = plugins or [] sean@0: #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). sean@0: self.skiplist = skiplist or [] sean@0: #: Additional keyword arguments passed to the :meth:`Bottle.route` sean@0: #: decorator are stored in this dictionary. Used for route-specific sean@0: #: plugin configuration and meta-data. sean@0: self.config = ConfigDict().load_dict(config) sean@0: sean@0: @cached_property sean@0: def call(self): sean@0: """ The route callback with all plugins applied. This property is sean@0: created on demand and then cached to speed up subsequent requests.""" sean@0: return self._make_callback() sean@0: sean@0: def reset(self): sean@0: """ Forget any cached values. The next time :attr:`call` is accessed, sean@0: all plugins are re-applied. """ sean@0: self.__dict__.pop('call', None) sean@0: sean@0: def prepare(self): sean@0: """ Do all on-demand work immediately (useful for debugging).""" sean@0: self.call sean@0: sean@0: def all_plugins(self): sean@0: """ Yield all Plugins affecting this route. """ sean@0: unique = set() sean@0: for p in reversed(self.app.plugins + self.plugins): sean@0: if True in self.skiplist: break sean@0: name = getattr(p, 'name', False) sean@0: if name and (name in self.skiplist or name in unique): continue sean@0: if p in self.skiplist or type(p) in self.skiplist: continue sean@0: if name: unique.add(name) sean@0: yield p sean@0: sean@0: def _make_callback(self): sean@0: callback = self.callback sean@0: for plugin in self.all_plugins(): sean@0: try: sean@0: if hasattr(plugin, 'apply'): sean@0: callback = plugin.apply(callback, self) sean@0: else: sean@0: callback = plugin(callback) sean@0: except RouteReset: # Try again with changed configuration. sean@0: return self._make_callback() sean@0: if not callback is self.callback: sean@0: update_wrapper(callback, self.callback) sean@0: return callback sean@0: sean@0: def get_undecorated_callback(self): sean@0: """ Return the callback. If the callback is a decorated function, try to sean@0: recover the original function. """ sean@0: func = self.callback sean@0: func = getattr(func, '__func__' if py3k else 'im_func', func) sean@0: closure_attr = '__closure__' if py3k else 'func_closure' sean@0: while hasattr(func, closure_attr) and getattr(func, closure_attr): sean@0: attributes = getattr(func, closure_attr) sean@0: func = attributes[0].cell_contents sean@0: sean@0: # in case of decorators with multiple arguments sean@0: if not isinstance(func, FunctionType): sean@0: # pick first FunctionType instance from multiple arguments sean@0: func = filter(lambda x: isinstance(x, FunctionType), sean@0: map(lambda x: x.cell_contents, attributes)) sean@0: func = list(func)[0] # py3 support sean@0: return func sean@0: sean@0: def get_callback_args(self): sean@0: """ Return a list of argument names the callback (most likely) accepts sean@0: as keyword arguments. If the callback is a decorated function, try sean@0: to recover the original function before inspection. """ sean@0: return getargspec(self.get_undecorated_callback())[0] sean@0: sean@0: def get_config(self, key, default=None): sean@0: """ Lookup a config field and return its value, first checking the sean@0: route.config, then route.app.config.""" sean@0: for conf in (self.config, self.app.config): sean@0: if key in conf: return conf[key] sean@0: return default sean@0: sean@0: def __repr__(self): sean@0: cb = self.get_undecorated_callback() sean@0: return '<%s %r %r>' % (self.method, self.rule, cb) sean@0: sean@0: ############################################################################### sean@0: # Application Object ########################################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: class Bottle(object): sean@0: """ Each Bottle object represents a single, distinct web application and sean@0: consists of routes, callbacks, plugins, resources and configuration. sean@0: Instances are callable WSGI applications. sean@0: sean@0: :param catchall: If true (default), handle all exceptions. Turn off to sean@0: let debugging middleware handle exceptions. sean@0: """ sean@0: sean@0: def __init__(self, catchall=True, autojson=True): sean@0: sean@0: #: A :class:`ConfigDict` for app specific configuration. sean@0: self.config = ConfigDict() sean@0: self.config._on_change = functools.partial(self.trigger_hook, 'config') sean@0: self.config.meta_set('autojson', 'validate', bool) sean@0: self.config.meta_set('catchall', 'validate', bool) sean@0: self.config['catchall'] = catchall sean@0: self.config['autojson'] = autojson sean@0: sean@0: #: A :class:`ResourceManager` for application files sean@0: self.resources = ResourceManager() sean@0: sean@0: self.routes = [] # List of installed :class:`Route` instances. sean@0: self.router = Router() # Maps requests to :class:`Route` instances. sean@0: self.error_handler = {} sean@0: sean@0: # Core plugins sean@0: self.plugins = [] # List of installed plugins. sean@0: if self.config['autojson']: sean@0: self.install(JSONPlugin()) sean@0: self.install(TemplatePlugin()) sean@0: sean@0: #: If true, most exceptions are caught and returned as :exc:`HTTPError` sean@0: catchall = DictProperty('config', 'catchall') sean@0: sean@0: __hook_names = 'before_request', 'after_request', 'app_reset', 'config' sean@0: __hook_reversed = 'after_request' sean@0: sean@0: @cached_property sean@0: def _hooks(self): sean@0: return dict((name, []) for name in self.__hook_names) sean@0: sean@0: def add_hook(self, name, func): sean@0: """ Attach a callback to a hook. Three hooks are currently implemented: sean@0: sean@0: before_request sean@0: Executed once before each request. The request context is sean@0: available, but no routing has happened yet. sean@0: after_request sean@0: Executed once after each request regardless of its outcome. sean@0: app_reset sean@0: Called whenever :meth:`Bottle.reset` is called. sean@0: """ sean@0: if name in self.__hook_reversed: sean@0: self._hooks[name].insert(0, func) sean@0: else: sean@0: self._hooks[name].append(func) sean@0: sean@0: def remove_hook(self, name, func): sean@0: """ Remove a callback from a hook. """ sean@0: if name in self._hooks and func in self._hooks[name]: sean@0: self._hooks[name].remove(func) sean@0: return True sean@0: sean@0: def trigger_hook(self, __name, *args, **kwargs): sean@0: """ Trigger a hook and return a list of results. """ sean@0: return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] sean@0: sean@0: def hook(self, name): sean@0: """ Return a decorator that attaches a callback to a hook. See sean@0: :meth:`add_hook` for details.""" sean@0: sean@0: def decorator(func): sean@0: self.add_hook(name, func) sean@0: return func sean@0: sean@0: return decorator sean@0: sean@0: def mount(self, prefix, app, **options): sean@0: """ Mount an application (:class:`Bottle` or plain WSGI) to a specific sean@0: URL prefix. Example:: sean@0: sean@0: root_app.mount('/admin/', admin_app) sean@0: sean@0: :param prefix: path prefix or `mount-point`. If it ends in a slash, sean@0: that slash is mandatory. sean@0: :param app: an instance of :class:`Bottle` or a WSGI application. sean@0: sean@0: All other parameters are passed to the underlying :meth:`route` call. sean@0: """ sean@0: sean@0: segments = [p for p in prefix.split('/') if p] sean@0: if not segments: raise ValueError('Empty path prefix.') sean@0: path_depth = len(segments) sean@0: sean@0: def mountpoint_wrapper(): sean@0: try: sean@0: request.path_shift(path_depth) sean@0: rs = HTTPResponse([]) sean@0: sean@0: def start_response(status, headerlist, exc_info=None): sean@0: if exc_info: sean@0: _raise(*exc_info) sean@0: rs.status = status sean@0: for name, value in headerlist: sean@0: rs.add_header(name, value) sean@0: return rs.body.append sean@0: sean@0: body = app(request.environ, start_response) sean@0: rs.body = itertools.chain(rs.body, body) if rs.body else body sean@0: return rs sean@0: finally: sean@0: request.path_shift(-path_depth) sean@0: sean@0: options.setdefault('skip', True) sean@0: options.setdefault('method', 'PROXY') sean@0: options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) sean@0: options['callback'] = mountpoint_wrapper sean@0: sean@0: self.route('/%s/<:re:.*>' % '/'.join(segments), **options) sean@0: if not prefix.endswith('/'): sean@0: self.route('/' + '/'.join(segments), **options) sean@0: sean@0: def merge(self, routes): sean@0: """ Merge the routes of another :class:`Bottle` application or a list of sean@0: :class:`Route` objects into this application. The routes keep their sean@0: 'owner', meaning that the :data:`Route.app` attribute is not sean@0: changed. """ sean@0: if isinstance(routes, Bottle): sean@0: routes = routes.routes sean@0: for route in routes: sean@0: self.add_route(route) sean@0: sean@0: def install(self, plugin): sean@0: """ Add a plugin to the list of plugins and prepare it for being sean@0: applied to all routes of this application. A plugin may be a simple sean@0: decorator or an object that implements the :class:`Plugin` API. sean@0: """ sean@0: if hasattr(plugin, 'setup'): plugin.setup(self) sean@0: if not callable(plugin) and not hasattr(plugin, 'apply'): sean@0: raise TypeError("Plugins must be callable or implement .apply()") sean@0: self.plugins.append(plugin) sean@0: self.reset() sean@0: return plugin sean@0: sean@0: def uninstall(self, plugin): sean@0: """ Uninstall plugins. Pass an instance to remove a specific plugin, a type sean@0: object to remove all plugins that match that type, a string to remove sean@0: all plugins with a matching ``name`` attribute or ``True`` to remove all sean@0: plugins. Return the list of removed plugins. """ sean@0: removed, remove = [], plugin sean@0: for i, plugin in list(enumerate(self.plugins))[::-1]: sean@0: if remove is True or remove is plugin or remove is type(plugin) \ sean@0: or getattr(plugin, 'name', True) == remove: sean@0: removed.append(plugin) sean@0: del self.plugins[i] sean@0: if hasattr(plugin, 'close'): plugin.close() sean@0: if removed: self.reset() sean@0: return removed sean@0: sean@0: def reset(self, route=None): sean@0: """ Reset all routes (force plugins to be re-applied) and clear all sean@0: caches. If an ID or route object is given, only that specific route sean@0: is affected. """ sean@0: if route is None: routes = self.routes sean@0: elif isinstance(route, Route): routes = [route] sean@0: else: routes = [self.routes[route]] sean@0: for route in routes: sean@0: route.reset() sean@0: if DEBUG: sean@0: for route in routes: sean@0: route.prepare() sean@0: self.trigger_hook('app_reset') sean@0: sean@0: def close(self): sean@0: """ Close the application and all installed plugins. """ sean@0: for plugin in self.plugins: sean@0: if hasattr(plugin, 'close'): plugin.close() sean@0: sean@0: def run(self, **kwargs): sean@0: """ Calls :func:`run` with the same parameters. """ sean@0: run(self, **kwargs) sean@0: sean@0: def match(self, environ): sean@0: """ Search for a matching route and return a (:class:`Route` , urlargs) sean@0: tuple. The second value is a dictionary with parameters extracted sean@0: from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" sean@0: return self.router.match(environ) sean@0: sean@0: def get_url(self, routename, **kargs): sean@0: """ Return a string that matches a named route """ sean@0: scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' sean@0: location = self.router.build(routename, **kargs).lstrip('/') sean@0: return urljoin(urljoin('/', scriptname), location) sean@0: sean@0: def add_route(self, route): sean@0: """ Add a route object, but do not change the :data:`Route.app` sean@0: attribute.""" sean@0: self.routes.append(route) sean@0: self.router.add(route.rule, route.method, route, name=route.name) sean@0: if DEBUG: route.prepare() sean@0: sean@0: def route(self, sean@0: path=None, sean@0: method='GET', sean@0: callback=None, sean@0: name=None, sean@0: apply=None, sean@0: skip=None, **config): sean@0: """ A decorator to bind a function to a request URL. Example:: sean@0: sean@0: @app.route('/hello/') sean@0: def hello(name): sean@0: return 'Hello %s' % name sean@0: sean@0: The ```` part is a wildcard. See :class:`Router` for syntax sean@0: details. sean@0: sean@0: :param path: Request path or a list of paths to listen to. If no sean@0: path is specified, it is automatically generated from the sean@0: signature of the function. sean@0: :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of sean@0: methods to listen to. (default: `GET`) sean@0: :param callback: An optional shortcut to avoid the decorator sean@0: syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` sean@0: :param name: The name for this route. (default: None) sean@0: :param apply: A decorator or plugin or a list of plugins. These are sean@0: applied to the route callback in addition to installed plugins. sean@0: :param skip: A list of plugins, plugin classes or names. Matching sean@0: plugins are not installed to this route. ``True`` skips all. sean@0: sean@0: Any additional keyword arguments are stored as route-specific sean@0: configuration and passed to plugins (see :meth:`Plugin.apply`). sean@0: """ sean@0: if callable(path): path, callback = None, path sean@0: plugins = makelist(apply) sean@0: skiplist = makelist(skip) sean@0: sean@0: def decorator(callback): sean@0: if isinstance(callback, basestring): callback = load(callback) sean@0: for rule in makelist(path) or yieldroutes(callback): sean@0: for verb in makelist(method): sean@0: verb = verb.upper() sean@0: route = Route(self, rule, verb, callback, sean@0: name=name, sean@0: plugins=plugins, sean@0: skiplist=skiplist, **config) sean@0: self.add_route(route) sean@0: return callback sean@0: sean@0: return decorator(callback) if callback else decorator sean@0: sean@0: def get(self, path=None, method='GET', **options): sean@0: """ Equals :meth:`route`. """ sean@0: return self.route(path, method, **options) sean@0: sean@0: def post(self, path=None, method='POST', **options): sean@0: """ Equals :meth:`route` with a ``POST`` method parameter. """ sean@0: return self.route(path, method, **options) sean@0: sean@0: def put(self, path=None, method='PUT', **options): sean@0: """ Equals :meth:`route` with a ``PUT`` method parameter. """ sean@0: return self.route(path, method, **options) sean@0: sean@0: def delete(self, path=None, method='DELETE', **options): sean@0: """ Equals :meth:`route` with a ``DELETE`` method parameter. """ sean@0: return self.route(path, method, **options) sean@0: sean@0: def patch(self, path=None, method='PATCH', **options): sean@0: """ Equals :meth:`route` with a ``PATCH`` method parameter. """ sean@0: return self.route(path, method, **options) sean@0: sean@0: def error(self, code=500): sean@0: """ Decorator: Register an output handler for a HTTP error code""" sean@0: sean@0: def wrapper(handler): sean@0: self.error_handler[int(code)] = handler sean@0: return handler sean@0: sean@0: return wrapper sean@0: sean@0: def default_error_handler(self, res): sean@0: return tob(template(ERROR_PAGE_TEMPLATE, e=res)) sean@0: sean@0: def _handle(self, environ): sean@0: path = environ['bottle.raw_path'] = environ['PATH_INFO'] sean@0: if py3k: sean@0: try: sean@0: environ['PATH_INFO'] = path.encode('latin1').decode('utf8') sean@0: except UnicodeError: sean@0: return HTTPError(400, 'Invalid path string. Expected UTF-8') sean@0: sean@0: try: sean@0: environ['bottle.app'] = self sean@0: request.bind(environ) sean@0: response.bind() sean@0: try: sean@0: self.trigger_hook('before_request') sean@0: route, args = self.router.match(environ) sean@0: environ['route.handle'] = route sean@0: environ['bottle.route'] = route sean@0: environ['route.url_args'] = args sean@0: return route.call(**args) sean@0: finally: sean@0: self.trigger_hook('after_request') sean@0: except HTTPResponse: sean@0: return _e() sean@0: except RouteReset: sean@0: route.reset() sean@0: return self._handle(environ) sean@0: except (KeyboardInterrupt, SystemExit, MemoryError): sean@0: raise sean@0: except Exception: sean@0: if not self.catchall: raise sean@0: stacktrace = format_exc() sean@0: environ['wsgi.errors'].write(stacktrace) sean@0: return HTTPError(500, "Internal Server Error", _e(), stacktrace) sean@0: sean@0: def _cast(self, out, peek=None): sean@0: """ Try to convert the parameter into something WSGI compatible and set sean@0: correct HTTP headers when possible. sean@0: Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, sean@0: iterable of strings and iterable of unicodes sean@0: """ sean@0: sean@0: # Empty output is done here sean@0: if not out: sean@0: if 'Content-Length' not in response: sean@0: response['Content-Length'] = 0 sean@0: return [] sean@0: # Join lists of byte or unicode strings. Mixed lists are NOT supported sean@0: if isinstance(out, (tuple, list))\ sean@0: and isinstance(out[0], (bytes, unicode)): sean@0: out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' sean@0: # Encode unicode strings sean@0: if isinstance(out, unicode): sean@0: out = out.encode(response.charset) sean@0: # Byte Strings are just returned sean@0: if isinstance(out, bytes): sean@0: if 'Content-Length' not in response: sean@0: response['Content-Length'] = len(out) sean@0: return [out] sean@0: # HTTPError or HTTPException (recursive, because they may wrap anything) sean@0: # TODO: Handle these explicitly in handle() or make them iterable. sean@0: if isinstance(out, HTTPError): sean@0: out.apply(response) sean@0: out = self.error_handler.get(out.status_code, sean@0: self.default_error_handler)(out) sean@0: return self._cast(out) sean@0: if isinstance(out, HTTPResponse): sean@0: out.apply(response) sean@0: return self._cast(out.body) sean@0: sean@0: # File-like objects. sean@0: if hasattr(out, 'read'): sean@0: if 'wsgi.file_wrapper' in request.environ: sean@0: return request.environ['wsgi.file_wrapper'](out) sean@0: elif hasattr(out, 'close') or not hasattr(out, '__iter__'): sean@0: return WSGIFileWrapper(out) sean@0: sean@0: # Handle Iterables. We peek into them to detect their inner type. sean@0: try: sean@0: iout = iter(out) sean@0: first = next(iout) sean@0: while not first: sean@0: first = next(iout) sean@0: except StopIteration: sean@0: return self._cast('') sean@0: except HTTPResponse: sean@0: first = _e() sean@0: except (KeyboardInterrupt, SystemExit, MemoryError): sean@0: raise sean@0: except: sean@0: if not self.catchall: raise sean@0: first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) sean@0: sean@0: # These are the inner types allowed in iterator or generator objects. sean@0: if isinstance(first, HTTPResponse): sean@0: return self._cast(first) sean@0: elif isinstance(first, bytes): sean@0: new_iter = itertools.chain([first], iout) sean@0: elif isinstance(first, unicode): sean@0: encoder = lambda x: x.encode(response.charset) sean@0: new_iter = imap(encoder, itertools.chain([first], iout)) sean@0: else: sean@0: msg = 'Unsupported response type: %s' % type(first) sean@0: return self._cast(HTTPError(500, msg)) sean@0: if hasattr(out, 'close'): sean@0: new_iter = _closeiter(new_iter, out.close) sean@0: return new_iter sean@0: sean@0: def wsgi(self, environ, start_response): sean@0: """ The bottle WSGI-interface. """ sean@0: try: sean@0: out = self._cast(self._handle(environ)) sean@0: # rfc2616 section 4.3 sean@0: if response._status_code in (100, 101, 204, 304)\ sean@0: or environ['REQUEST_METHOD'] == 'HEAD': sean@0: if hasattr(out, 'close'): out.close() sean@0: out = [] sean@0: start_response(response._status_line, response.headerlist) sean@0: return out sean@0: except (KeyboardInterrupt, SystemExit, MemoryError): sean@0: raise sean@0: except: sean@0: if not self.catchall: raise sean@0: err = '

Critical error while processing request: %s

' \ sean@0: % html_escape(environ.get('PATH_INFO', '/')) sean@0: if DEBUG: sean@0: err += '

Error:

\n
\n%s\n
\n' \ sean@0: '

Traceback:

\n
\n%s\n
\n' \ sean@0: % (html_escape(repr(_e())), html_escape(format_exc())) sean@0: environ['wsgi.errors'].write(err) sean@0: headers = [('Content-Type', 'text/html; charset=UTF-8')] sean@0: start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) sean@0: return [tob(err)] sean@0: sean@0: def __call__(self, environ, start_response): sean@0: """ Each instance of :class:'Bottle' is a WSGI application. """ sean@0: return self.wsgi(environ, start_response) sean@0: sean@0: def __enter__(self): sean@0: """ Use this application as default for all module-level shortcuts. """ sean@0: default_app.push(self) sean@0: return self sean@0: sean@0: def __exit__(self, exc_type, exc_value, traceback): sean@0: default_app.pop() sean@0: sean@0: ############################################################################### sean@0: # HTTP and WSGI Tools ########################################################## sean@0: ############################################################################### sean@0: sean@0: sean@0: class BaseRequest(object): sean@0: """ A wrapper for WSGI environment dictionaries that adds a lot of sean@0: convenient access methods and properties. Most of them are read-only. sean@0: sean@0: Adding new attributes to a request actually adds them to the environ sean@0: dictionary (as 'bottle.request.ext.'). This is the recommended sean@0: way to store and access request-specific data. sean@0: """ sean@0: sean@0: __slots__ = ('environ', ) sean@0: sean@0: #: Maximum size of memory buffer for :attr:`body` in bytes. sean@0: MEMFILE_MAX = 102400 sean@0: sean@0: def __init__(self, environ=None): sean@0: """ Wrap a WSGI environ dictionary. """ sean@0: #: The wrapped WSGI environ dictionary. This is the only real attribute. sean@0: #: All other attributes actually are read-only properties. sean@0: self.environ = {} if environ is None else environ sean@0: self.environ['bottle.request'] = self sean@0: sean@0: @DictProperty('environ', 'bottle.app', read_only=True) sean@0: def app(self): sean@0: """ Bottle application handling this request. """ sean@0: raise RuntimeError('This request is not connected to an application.') sean@0: sean@0: @DictProperty('environ', 'bottle.route', read_only=True) sean@0: def route(self): sean@0: """ The bottle :class:`Route` object that matches this request. """ sean@0: raise RuntimeError('This request is not connected to a route.') sean@0: sean@0: @DictProperty('environ', 'route.url_args', read_only=True) sean@0: def url_args(self): sean@0: """ The arguments extracted from the URL. """ sean@0: raise RuntimeError('This request is not connected to a route.') sean@0: sean@0: @property sean@0: def path(self): sean@0: """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix sean@0: broken clients and avoid the "empty path" edge case). """ sean@0: return '/' + self.environ.get('PATH_INFO', '').lstrip('/') sean@0: sean@0: @property sean@0: def method(self): sean@0: """ The ``REQUEST_METHOD`` value as an uppercase string. """ sean@0: return self.environ.get('REQUEST_METHOD', 'GET').upper() sean@0: sean@0: @DictProperty('environ', 'bottle.request.headers', read_only=True) sean@0: def headers(self): sean@0: """ A :class:`WSGIHeaderDict` that provides case-insensitive access to sean@0: HTTP request headers. """ sean@0: return WSGIHeaderDict(self.environ) sean@0: sean@0: def get_header(self, name, default=None): sean@0: """ Return the value of a request header, or a given default value. """ sean@0: return self.headers.get(name, default) sean@0: sean@0: @DictProperty('environ', 'bottle.request.cookies', read_only=True) sean@0: def cookies(self): sean@0: """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT sean@0: decoded. Use :meth:`get_cookie` if you expect signed cookies. """ sean@0: cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() sean@0: return FormsDict((c.key, c.value) for c in cookies) sean@0: sean@0: def get_cookie(self, key, default=None, secret=None): sean@0: """ Return the content of a cookie. To read a `Signed Cookie`, the sean@0: `secret` must match the one used to create the cookie (see sean@0: :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing sean@0: cookie or wrong signature), return a default value. """ sean@0: value = self.cookies.get(key) sean@0: if secret and value: sean@0: dec = cookie_decode(value, secret) # (key, value) tuple or None sean@0: return dec[1] if dec and dec[0] == key else default sean@0: return value or default sean@0: sean@0: @DictProperty('environ', 'bottle.request.query', read_only=True) sean@0: def query(self): sean@0: """ The :attr:`query_string` parsed into a :class:`FormsDict`. These sean@0: values are sometimes called "URL arguments" or "GET parameters", but sean@0: not to be confused with "URL wildcards" as they are provided by the sean@0: :class:`Router`. """ sean@0: get = self.environ['bottle.get'] = FormsDict() sean@0: pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) sean@0: for key, value in pairs: sean@0: get[key] = value sean@0: return get sean@0: sean@0: @DictProperty('environ', 'bottle.request.forms', read_only=True) sean@0: def forms(self): sean@0: """ Form values parsed from an `url-encoded` or `multipart/form-data` sean@0: encoded POST or PUT request body. The result is returned as a sean@0: :class:`FormsDict`. All keys and values are strings. File uploads sean@0: are stored separately in :attr:`files`. """ sean@0: forms = FormsDict() sean@0: for name, item in self.POST.allitems(): sean@0: if not isinstance(item, FileUpload): sean@0: forms[name] = item sean@0: return forms sean@0: sean@0: @DictProperty('environ', 'bottle.request.params', read_only=True) sean@0: def params(self): sean@0: """ A :class:`FormsDict` with the combined values of :attr:`query` and sean@0: :attr:`forms`. File uploads are stored in :attr:`files`. """ sean@0: params = FormsDict() sean@0: for key, value in self.query.allitems(): sean@0: params[key] = value sean@0: for key, value in self.forms.allitems(): sean@0: params[key] = value sean@0: return params sean@0: sean@0: @DictProperty('environ', 'bottle.request.files', read_only=True) sean@0: def files(self): sean@0: """ File uploads parsed from `multipart/form-data` encoded POST or PUT sean@0: request body. The values are instances of :class:`FileUpload`. sean@0: sean@0: """ sean@0: files = FormsDict() sean@0: for name, item in self.POST.allitems(): sean@0: if isinstance(item, FileUpload): sean@0: files[name] = item sean@0: return files sean@0: sean@0: @DictProperty('environ', 'bottle.request.json', read_only=True) sean@0: def json(self): sean@0: """ If the ``Content-Type`` header is ``application/json``, this sean@0: property holds the parsed content of the request body. Only requests sean@0: smaller than :attr:`MEMFILE_MAX` are processed to avoid memory sean@0: exhaustion. """ sean@0: ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] sean@0: if ctype == 'application/json': sean@0: b = self._get_body_string() sean@0: if not b: sean@0: return None sean@0: return json_loads(b) sean@0: return None sean@0: sean@0: def _iter_body(self, read, bufsize): sean@0: maxread = max(0, self.content_length) sean@0: while maxread: sean@0: part = read(min(maxread, bufsize)) sean@0: if not part: break sean@0: yield part sean@0: maxread -= len(part) sean@0: sean@0: @staticmethod sean@0: def _iter_chunked(read, bufsize): sean@0: err = HTTPError(400, 'Error while parsing chunked transfer body.') sean@0: rn, sem, bs = tob('\r\n'), tob(';'), tob('') sean@0: while True: sean@0: header = read(1) sean@0: while header[-2:] != rn: sean@0: c = read(1) sean@0: header += c sean@0: if not c: raise err sean@0: if len(header) > bufsize: raise err sean@0: size, _, _ = header.partition(sem) sean@0: try: sean@0: maxread = int(tonat(size.strip()), 16) sean@0: except ValueError: sean@0: raise err sean@0: if maxread == 0: break sean@0: buff = bs sean@0: while maxread > 0: sean@0: if not buff: sean@0: buff = read(min(maxread, bufsize)) sean@0: part, buff = buff[:maxread], buff[maxread:] sean@0: if not part: raise err sean@0: yield part sean@0: maxread -= len(part) sean@0: if read(2) != rn: sean@0: raise err sean@0: sean@0: @DictProperty('environ', 'bottle.request.body', read_only=True) sean@0: def _body(self): sean@0: try: sean@0: read_func = self.environ['wsgi.input'].read sean@0: except KeyError: sean@0: self.environ['wsgi.input'] = BytesIO() sean@0: return self.environ['wsgi.input'] sean@0: body_iter = self._iter_chunked if self.chunked else self._iter_body sean@0: body, body_size, is_temp_file = BytesIO(), 0, False sean@0: for part in body_iter(read_func, self.MEMFILE_MAX): sean@0: body.write(part) sean@0: body_size += len(part) sean@0: if not is_temp_file and body_size > self.MEMFILE_MAX: sean@0: body, tmp = TemporaryFile(mode='w+b'), body sean@0: body.write(tmp.getvalue()) sean@0: del tmp sean@0: is_temp_file = True sean@0: self.environ['wsgi.input'] = body sean@0: body.seek(0) sean@0: return body sean@0: sean@0: def _get_body_string(self): sean@0: """ read body until content-length or MEMFILE_MAX into a string. Raise sean@0: HTTPError(413) on requests that are to large. """ sean@0: clen = self.content_length sean@0: if clen > self.MEMFILE_MAX: sean@0: raise HTTPError(413, 'Request entity too large') sean@0: if clen < 0: clen = self.MEMFILE_MAX + 1 sean@0: data = self.body.read(clen) sean@0: if len(data) > self.MEMFILE_MAX: # Fail fast sean@0: raise HTTPError(413, 'Request entity too large') sean@0: return data sean@0: sean@0: @property sean@0: def body(self): sean@0: """ The HTTP request body as a seek-able file-like object. Depending on sean@0: :attr:`MEMFILE_MAX`, this is either a temporary file or a sean@0: :class:`io.BytesIO` instance. Accessing this property for the first sean@0: time reads and replaces the ``wsgi.input`` environ variable. sean@0: Subsequent accesses just do a `seek(0)` on the file object. """ sean@0: self._body.seek(0) sean@0: return self._body sean@0: sean@0: @property sean@0: def chunked(self): sean@0: """ True if Chunked transfer encoding was. """ sean@0: return 'chunked' in self.environ.get( sean@0: 'HTTP_TRANSFER_ENCODING', '').lower() sean@0: sean@0: #: An alias for :attr:`query`. sean@0: GET = query sean@0: sean@0: @DictProperty('environ', 'bottle.request.post', read_only=True) sean@0: def POST(self): sean@0: """ The values of :attr:`forms` and :attr:`files` combined into a single sean@0: :class:`FormsDict`. Values are either strings (form values) or sean@0: instances of :class:`cgi.FieldStorage` (file uploads). sean@0: """ sean@0: post = FormsDict() sean@0: # We default to application/x-www-form-urlencoded for everything that sean@0: # is not multipart and take the fast path (also: 3.1 workaround) sean@0: if not self.content_type.startswith('multipart/'): sean@0: pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) sean@0: for key, value in pairs: sean@0: post[key] = value sean@0: return post sean@0: sean@0: safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi sean@0: for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): sean@0: if key in self.environ: safe_env[key] = self.environ[key] sean@0: args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) sean@0: if py31: sean@0: args['fp'] = NCTextIOWrapper(args['fp'], sean@0: encoding='utf8', sean@0: newline='\n') sean@0: elif py3k: sean@0: args['encoding'] = 'utf8' sean@0: data = cgi.FieldStorage(**args) sean@0: self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 sean@0: data = data.list or [] sean@0: for item in data: sean@0: if item.filename: sean@0: post[item.name] = FileUpload(item.file, item.name, sean@0: item.filename, item.headers) sean@0: else: sean@0: post[item.name] = item.value sean@0: return post sean@0: sean@0: @property sean@0: def url(self): sean@0: """ The full request URI including hostname and scheme. If your app sean@0: lives behind a reverse proxy or load balancer and you get confusing sean@0: results, make sure that the ``X-Forwarded-Host`` header is set sean@0: correctly. """ sean@0: return self.urlparts.geturl() sean@0: sean@0: @DictProperty('environ', 'bottle.request.urlparts', read_only=True) sean@0: def urlparts(self): sean@0: """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. sean@0: The tuple contains (scheme, host, path, query_string and fragment), sean@0: but the fragment is always empty because it is not visible to the sean@0: server. """ sean@0: env = self.environ sean@0: http = env.get('HTTP_X_FORWARDED_PROTO') \ sean@0: or env.get('wsgi.url_scheme', 'http') sean@0: host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') sean@0: if not host: sean@0: # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. sean@0: host = env.get('SERVER_NAME', '127.0.0.1') sean@0: port = env.get('SERVER_PORT') sean@0: if port and port != ('80' if http == 'http' else '443'): sean@0: host += ':' + port sean@0: path = urlquote(self.fullpath) sean@0: return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') sean@0: sean@0: @property sean@0: def fullpath(self): sean@0: """ Request path including :attr:`script_name` (if present). """ sean@0: return urljoin(self.script_name, self.path.lstrip('/')) sean@0: sean@0: @property sean@0: def query_string(self): sean@0: """ The raw :attr:`query` part of the URL (everything in between ``?`` sean@0: and ``#``) as a string. """ sean@0: return self.environ.get('QUERY_STRING', '') sean@0: sean@0: @property sean@0: def script_name(self): sean@0: """ The initial portion of the URL's `path` that was removed by a higher sean@0: level (server or routing middleware) before the application was sean@0: called. This script path is returned with leading and tailing sean@0: slashes. """ sean@0: script_name = self.environ.get('SCRIPT_NAME', '').strip('/') sean@0: return '/' + script_name + '/' if script_name else '/' sean@0: sean@0: def path_shift(self, shift=1): sean@0: """ Shift path segments from :attr:`path` to :attr:`script_name` and sean@0: vice versa. sean@0: sean@0: :param shift: The number of path segments to shift. May be negative sean@0: to change the shift direction. (default: 1) sean@0: """ sean@0: script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) sean@0: self['SCRIPT_NAME'], self['PATH_INFO'] = script, path sean@0: sean@0: @property sean@0: def content_length(self): sean@0: """ The request body length as an integer. The client is responsible to sean@0: set this header. Otherwise, the real length of the body is unknown sean@0: and -1 is returned. In this case, :attr:`body` will be empty. """ sean@0: return int(self.environ.get('CONTENT_LENGTH') or -1) sean@0: sean@0: @property sean@0: def content_type(self): sean@0: """ The Content-Type header as a lowercase-string (default: empty). """ sean@0: return self.environ.get('CONTENT_TYPE', '').lower() sean@0: sean@0: @property sean@0: def is_xhr(self): sean@0: """ True if the request was triggered by a XMLHttpRequest. This only sean@0: works with JavaScript libraries that support the `X-Requested-With` sean@0: header (most of the popular libraries do). """ sean@0: requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') sean@0: return requested_with.lower() == 'xmlhttprequest' sean@0: sean@0: @property sean@0: def is_ajax(self): sean@0: """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ sean@0: return self.is_xhr sean@0: sean@0: @property sean@0: def auth(self): sean@0: """ HTTP authentication data as a (user, password) tuple. This sean@0: implementation currently supports basic (not digest) authentication sean@0: only. If the authentication happened at a higher level (e.g. in the sean@0: front web-server or a middleware), the password field is None, but sean@0: the user field is looked up from the ``REMOTE_USER`` environ sean@0: variable. On any errors, None is returned. """ sean@0: basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) sean@0: if basic: return basic sean@0: ruser = self.environ.get('REMOTE_USER') sean@0: if ruser: return (ruser, None) sean@0: return None sean@0: sean@0: @property sean@0: def remote_route(self): sean@0: """ A list of all IPs that were involved in this request, starting with sean@0: the client IP and followed by zero or more proxies. This does only sean@0: work if all proxies support the ```X-Forwarded-For`` header. Note sean@0: that this information can be forged by malicious clients. """ sean@0: proxy = self.environ.get('HTTP_X_FORWARDED_FOR') sean@0: if proxy: return [ip.strip() for ip in proxy.split(',')] sean@0: remote = self.environ.get('REMOTE_ADDR') sean@0: return [remote] if remote else [] sean@0: sean@0: @property sean@0: def remote_addr(self): sean@0: """ The client IP as a string. Note that this information can be forged sean@0: by malicious clients. """ sean@0: route = self.remote_route sean@0: return route[0] if route else None sean@0: sean@0: def copy(self): sean@0: """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ sean@0: return Request(self.environ.copy()) sean@0: sean@0: def get(self, value, default=None): sean@0: return self.environ.get(value, default) sean@0: sean@0: def __getitem__(self, key): sean@0: return self.environ[key] sean@0: sean@0: def __delitem__(self, key): sean@0: self[key] = "" sean@0: del (self.environ[key]) sean@0: sean@0: def __iter__(self): sean@0: return iter(self.environ) sean@0: sean@0: def __len__(self): sean@0: return len(self.environ) sean@0: sean@0: def keys(self): sean@0: return self.environ.keys() sean@0: sean@0: def __setitem__(self, key, value): sean@0: """ Change an environ value and clear all caches that depend on it. """ sean@0: sean@0: if self.environ.get('bottle.request.readonly'): sean@0: raise KeyError('The environ dictionary is read-only.') sean@0: sean@0: self.environ[key] = value sean@0: todelete = () sean@0: sean@0: if key == 'wsgi.input': sean@0: todelete = ('body', 'forms', 'files', 'params', 'post', 'json') sean@0: elif key == 'QUERY_STRING': sean@0: todelete = ('query', 'params') sean@0: elif key.startswith('HTTP_'): sean@0: todelete = ('headers', 'cookies') sean@0: sean@0: for key in todelete: sean@0: self.environ.pop('bottle.request.' + key, None) sean@0: sean@0: def __repr__(self): sean@0: return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) sean@0: sean@0: def __getattr__(self, name): sean@0: """ Search in self.environ for additional user defined attributes. """ sean@0: try: sean@0: var = self.environ['bottle.request.ext.%s' % name] sean@0: return var.__get__(self) if hasattr(var, '__get__') else var sean@0: except KeyError: sean@0: raise AttributeError('Attribute %r not defined.' % name) sean@0: sean@0: def __setattr__(self, name, value): sean@0: if name == 'environ': return object.__setattr__(self, name, value) sean@0: self.environ['bottle.request.ext.%s' % name] = value sean@0: sean@0: sean@0: def _hkey(s): sean@0: return s.title().replace('_', '-') sean@0: sean@0: sean@0: class HeaderProperty(object): sean@0: def __init__(self, name, reader=None, writer=str, default=''): sean@0: self.name, self.default = name, default sean@0: self.reader, self.writer = reader, writer sean@0: self.__doc__ = 'Current value of the %r header.' % name.title() sean@0: sean@0: def __get__(self, obj, _): sean@0: if obj is None: return self sean@0: value = obj.headers.get(self.name, self.default) sean@0: return self.reader(value) if self.reader else value sean@0: sean@0: def __set__(self, obj, value): sean@0: obj.headers[self.name] = self.writer(value) sean@0: sean@0: def __delete__(self, obj): sean@0: del obj.headers[self.name] sean@0: sean@0: sean@0: class BaseResponse(object): sean@0: """ Storage class for a response body as well as headers and cookies. sean@0: sean@0: This class does support dict-like case-insensitive item-access to sean@0: headers, but is NOT a dict. Most notably, iterating over a response sean@0: yields parts of the body and not the headers. sean@0: sean@0: :param body: The response body as one of the supported types. sean@0: :param status: Either an HTTP status code (e.g. 200) or a status line sean@0: including the reason phrase (e.g. '200 OK'). sean@0: :param headers: A dictionary or a list of name-value pairs. sean@0: sean@0: Additional keyword arguments are added to the list of headers. sean@0: Underscores in the header name are replaced with dashes. sean@0: """ sean@0: sean@0: default_status = 200 sean@0: default_content_type = 'text/html; charset=UTF-8' sean@0: sean@0: # Header blacklist for specific response codes sean@0: # (rfc2616 section 10.2.3 and 10.3.5) sean@0: bad_headers = { sean@0: 204: set(('Content-Type', )), sean@0: 304: set(('Allow', 'Content-Encoding', 'Content-Language', sean@0: 'Content-Length', 'Content-Range', 'Content-Type', sean@0: 'Content-Md5', 'Last-Modified')) sean@0: } sean@0: sean@0: def __init__(self, body='', status=None, headers=None, **more_headers): sean@0: self._cookies = None sean@0: self._headers = {} sean@0: self.body = body sean@0: self.status = status or self.default_status sean@0: if headers: sean@0: if isinstance(headers, dict): sean@0: headers = headers.items() sean@0: for name, value in headers: sean@0: self.add_header(name, value) sean@0: if more_headers: sean@0: for name, value in more_headers.items(): sean@0: self.add_header(name, value) sean@0: sean@0: def copy(self, cls=None): sean@0: """ Returns a copy of self. """ sean@0: cls = cls or BaseResponse sean@0: assert issubclass(cls, BaseResponse) sean@0: copy = cls() sean@0: copy.status = self.status sean@0: copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) sean@0: if self._cookies: sean@0: copy._cookies = SimpleCookie() sean@0: copy._cookies.load(self._cookies.output(header='')) sean@0: return copy sean@0: sean@0: def __iter__(self): sean@0: return iter(self.body) sean@0: sean@0: def close(self): sean@0: if hasattr(self.body, 'close'): sean@0: self.body.close() sean@0: sean@0: @property sean@0: def status_line(self): sean@0: """ The HTTP status line as a string (e.g. ``404 Not Found``).""" sean@0: return self._status_line sean@0: sean@0: @property sean@0: def status_code(self): sean@0: """ The HTTP status code as an integer (e.g. 404).""" sean@0: return self._status_code sean@0: sean@0: def _set_status(self, status): sean@0: if isinstance(status, int): sean@0: code, status = status, _HTTP_STATUS_LINES.get(status) sean@0: elif ' ' in status: sean@0: status = status.strip() sean@0: code = int(status.split()[0]) sean@0: else: sean@0: raise ValueError('String status line without a reason phrase.') sean@0: if not 100 <= code <= 999: sean@0: raise ValueError('Status code out of range.') sean@0: self._status_code = code sean@0: self._status_line = str(status or ('%d Unknown' % code)) sean@0: sean@0: def _get_status(self): sean@0: return self._status_line sean@0: sean@0: status = property( sean@0: _get_status, _set_status, None, sean@0: ''' A writeable property to change the HTTP response status. It accepts sean@0: either a numeric code (100-999) or a string with a custom reason sean@0: phrase (e.g. "404 Brain not found"). Both :data:`status_line` and sean@0: :data:`status_code` are updated accordingly. The return value is sean@0: always a status string. ''') sean@0: del _get_status, _set_status sean@0: sean@0: @property sean@0: def headers(self): sean@0: """ An instance of :class:`HeaderDict`, a case-insensitive dict-like sean@0: view on the response headers. """ sean@0: hdict = HeaderDict() sean@0: hdict.dict = self._headers sean@0: return hdict sean@0: sean@0: def __contains__(self, name): sean@0: return _hkey(name) in self._headers sean@0: sean@0: def __delitem__(self, name): sean@0: del self._headers[_hkey(name)] sean@0: sean@0: def __getitem__(self, name): sean@0: return self._headers[_hkey(name)][-1] sean@0: sean@0: def __setitem__(self, name, value): sean@0: self._headers[_hkey(name)] = [value if isinstance(value, unicode) else sean@0: str(value)] sean@0: sean@0: def get_header(self, name, default=None): sean@0: """ Return the value of a previously defined header. If there is no sean@0: header with that name, return a default value. """ sean@0: return self._headers.get(_hkey(name), [default])[-1] sean@0: sean@0: def set_header(self, name, value): sean@0: """ Create a new response header, replacing any previously defined sean@0: headers with the same name. """ sean@0: self._headers[_hkey(name)] = [value if isinstance(value, unicode) sean@0: else str(value)] sean@0: sean@0: def add_header(self, name, value): sean@0: """ Add an additional response header, not removing duplicates. """ sean@0: self._headers.setdefault(_hkey(name), []).append( sean@0: value if isinstance(value, unicode) else str(value)) sean@0: sean@0: def iter_headers(self): sean@0: """ Yield (header, value) tuples, skipping headers that are not sean@0: allowed with the current response status code. """ sean@0: return self.headerlist sean@0: sean@0: @property sean@0: def headerlist(self): sean@0: """ WSGI conform list of (header, value) tuples. """ sean@0: out = [] sean@0: headers = list(self._headers.items()) sean@0: if 'Content-Type' not in self._headers: sean@0: headers.append(('Content-Type', [self.default_content_type])) sean@0: if self._status_code in self.bad_headers: sean@0: bad_headers = self.bad_headers[self._status_code] sean@0: headers = [h for h in headers if h[0] not in bad_headers] sean@0: out += [(name, val) for (name, vals) in headers for val in vals] sean@0: if self._cookies: sean@0: for c in self._cookies.values(): sean@0: out.append(('Set-Cookie', c.OutputString())) sean@0: if py3k: sean@0: return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] sean@0: else: sean@0: return [(k, v.encode('utf8') if isinstance(v, unicode) else v) sean@0: for (k, v) in out] sean@0: sean@0: content_type = HeaderProperty('Content-Type') sean@0: content_length = HeaderProperty('Content-Length', reader=int) sean@0: expires = HeaderProperty( sean@0: 'Expires', sean@0: reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), sean@0: writer=lambda x: http_date(x)) sean@0: sean@0: @property sean@0: def charset(self, default='UTF-8'): sean@0: """ Return the charset specified in the content-type header (default: utf8). """ sean@0: if 'charset=' in self.content_type: sean@0: return self.content_type.split('charset=')[-1].split(';')[0].strip() sean@0: return default sean@0: sean@0: def set_cookie(self, name, value, secret=None, **options): sean@0: """ Create a new cookie or replace an old one. If the `secret` parameter is sean@0: set, create a `Signed Cookie` (described below). sean@0: sean@0: :param name: the name of the cookie. sean@0: :param value: the value of the cookie. sean@0: :param secret: a signature key required for signed cookies. sean@0: sean@0: Additionally, this method accepts all RFC 2109 attributes that are sean@0: supported by :class:`cookie.Morsel`, including: sean@0: sean@0: :param max_age: maximum age in seconds. (default: None) sean@0: :param expires: a datetime object or UNIX timestamp. (default: None) sean@0: :param domain: the domain that is allowed to read the cookie. sean@0: (default: current domain) sean@0: :param path: limits the cookie to a given path (default: current path) sean@0: :param secure: limit the cookie to HTTPS connections (default: off). sean@0: :param httponly: prevents client-side javascript to read this cookie sean@0: (default: off, requires Python 2.6 or newer). sean@0: sean@0: If neither `expires` nor `max_age` is set (default), the cookie will sean@0: expire at the end of the browser session (as soon as the browser sean@0: window is closed). sean@0: sean@0: Signed cookies may store any pickle-able object and are sean@0: cryptographically signed to prevent manipulation. Keep in mind that sean@0: cookies are limited to 4kb in most browsers. sean@0: sean@0: Warning: Signed cookies are not encrypted (the client can still see sean@0: the content) and not copy-protected (the client can restore an old sean@0: cookie). The main intention is to make pickling and unpickling sean@0: save, not to store secret information at client side. sean@0: """ sean@0: if not self._cookies: sean@0: self._cookies = SimpleCookie() sean@0: sean@0: if secret: sean@0: value = touni(cookie_encode((name, value), secret)) sean@0: elif not isinstance(value, basestring): sean@0: raise TypeError('Secret key missing for non-string Cookie.') sean@0: sean@0: if len(value) > 4096: raise ValueError('Cookie value to long.') sean@0: self._cookies[name] = value sean@0: sean@0: for key, value in options.items(): sean@0: if key == 'max_age': sean@0: if isinstance(value, timedelta): sean@0: value = value.seconds + value.days * 24 * 3600 sean@0: if key == 'expires': sean@0: if isinstance(value, (datedate, datetime)): sean@0: value = value.timetuple() sean@0: elif isinstance(value, (int, float)): sean@0: value = time.gmtime(value) sean@0: value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) sean@0: if key in ('secure', 'httponly') and not value: sean@0: continue sean@0: self._cookies[name][key.replace('_', '-')] = value sean@0: sean@0: def delete_cookie(self, key, **kwargs): sean@0: """ Delete a cookie. Be sure to use the same `domain` and `path` sean@0: settings as used to create the cookie. """ sean@0: kwargs['max_age'] = -1 sean@0: kwargs['expires'] = 0 sean@0: self.set_cookie(key, '', **kwargs) sean@0: sean@0: def __repr__(self): sean@0: out = '' sean@0: for name, value in self.headerlist: sean@0: out += '%s: %s\n' % (name.title(), value.strip()) sean@0: return out sean@0: sean@0: sean@0: def _local_property(): sean@0: ls = threading.local() sean@0: sean@0: def fget(_): sean@0: try: sean@0: return ls.var sean@0: except AttributeError: sean@0: raise RuntimeError("Request context not initialized.") sean@0: sean@0: def fset(_, value): sean@0: ls.var = value sean@0: sean@0: def fdel(_): sean@0: del ls.var sean@0: sean@0: return property(fget, fset, fdel, 'Thread-local property') sean@0: sean@0: sean@0: class LocalRequest(BaseRequest): sean@0: """ A thread-local subclass of :class:`BaseRequest` with a different sean@0: set of attributes for each thread. There is usually only one global sean@0: instance of this class (:data:`request`). If accessed during a sean@0: request/response cycle, this instance always refers to the *current* sean@0: request (even on a multithreaded server). """ sean@0: bind = BaseRequest.__init__ sean@0: environ = _local_property() sean@0: sean@0: sean@0: class LocalResponse(BaseResponse): sean@0: """ A thread-local subclass of :class:`BaseResponse` with a different sean@0: set of attributes for each thread. There is usually only one global sean@0: instance of this class (:data:`response`). Its attributes are used sean@0: to build the HTTP response at the end of the request/response cycle. sean@0: """ sean@0: bind = BaseResponse.__init__ sean@0: _status_line = _local_property() sean@0: _status_code = _local_property() sean@0: _cookies = _local_property() sean@0: _headers = _local_property() sean@0: body = _local_property() sean@0: sean@0: sean@0: Request = BaseRequest sean@0: Response = BaseResponse sean@0: sean@0: sean@0: class HTTPResponse(Response, BottleException): sean@0: def __init__(self, body='', status=None, headers=None, **more_headers): sean@0: super(HTTPResponse, self).__init__(body, status, headers, **more_headers) sean@0: sean@0: def apply(self, other): sean@0: other._status_code = self._status_code sean@0: other._status_line = self._status_line sean@0: other._headers = self._headers sean@0: other._cookies = self._cookies sean@0: other.body = self.body sean@0: sean@0: sean@0: class HTTPError(HTTPResponse): sean@0: default_status = 500 sean@0: sean@0: def __init__(self, sean@0: status=None, sean@0: body=None, sean@0: exception=None, sean@0: traceback=None, **options): sean@0: self.exception = exception sean@0: self.traceback = traceback sean@0: super(HTTPError, self).__init__(body, status, **options) sean@0: sean@0: ############################################################################### sean@0: # Plugins ###################################################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: class PluginError(BottleException): sean@0: pass sean@0: sean@0: sean@0: class JSONPlugin(object): sean@0: name = 'json' sean@0: api = 2 sean@0: sean@0: def __init__(self, json_dumps=json_dumps): sean@0: self.json_dumps = json_dumps sean@0: sean@0: def apply(self, callback, _): sean@0: dumps = self.json_dumps sean@0: if not dumps: return callback sean@0: sean@0: def wrapper(*a, **ka): sean@0: try: sean@0: rv = callback(*a, **ka) sean@0: except HTTPError: sean@0: rv = _e() sean@0: sean@0: if isinstance(rv, dict): sean@0: #Attempt to serialize, raises exception on failure sean@0: json_response = dumps(rv) sean@0: #Set content type only if serialization successful sean@0: response.content_type = 'application/json' sean@0: return json_response sean@0: elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): sean@0: rv.body = dumps(rv.body) sean@0: rv.content_type = 'application/json' sean@0: return rv sean@0: sean@0: return wrapper sean@0: sean@0: sean@0: class TemplatePlugin(object): sean@0: """ This plugin applies the :func:`view` decorator to all routes with a sean@0: `template` config parameter. If the parameter is a tuple, the second sean@0: element must be a dict with additional options (e.g. `template_engine`) sean@0: or default variables for the template. """ sean@0: name = 'template' sean@0: api = 2 sean@0: sean@0: def apply(self, callback, route): sean@0: conf = route.config.get('template') sean@0: if isinstance(conf, (tuple, list)) and len(conf) == 2: sean@0: return view(conf[0], **conf[1])(callback) sean@0: elif isinstance(conf, str): sean@0: return view(conf)(callback) sean@0: else: sean@0: return callback sean@0: sean@0: sean@0: #: Not a plugin, but part of the plugin API. TODO: Find a better place. sean@0: class _ImportRedirect(object): sean@0: def __init__(self, name, impmask): sean@0: """ Create a virtual package that redirects imports (see PEP 302). """ sean@0: self.name = name sean@0: self.impmask = impmask sean@0: self.module = sys.modules.setdefault(name, imp.new_module(name)) sean@0: self.module.__dict__.update({ sean@0: '__file__': __file__, sean@0: '__path__': [], sean@0: '__all__': [], sean@0: '__loader__': self sean@0: }) sean@0: sys.meta_path.append(self) sean@0: sean@0: def find_module(self, fullname, path=None): sean@0: if '.' not in fullname: return sean@0: packname = fullname.rsplit('.', 1)[0] sean@0: if packname != self.name: return sean@0: return self sean@0: sean@0: def load_module(self, fullname): sean@0: if fullname in sys.modules: return sys.modules[fullname] sean@0: modname = fullname.rsplit('.', 1)[1] sean@0: realname = self.impmask % modname sean@0: __import__(realname) sean@0: module = sys.modules[fullname] = sys.modules[realname] sean@0: setattr(self.module, modname, module) sean@0: module.__loader__ = self sean@0: return module sean@0: sean@0: ############################################################################### sean@0: # Common Utilities ############################################################# sean@0: ############################################################################### sean@0: sean@0: sean@0: class MultiDict(DictMixin): sean@0: """ This dict stores multiple values per key, but behaves exactly like a sean@0: normal dict in that it returns only the newest value for any given key. sean@0: There are special methods available to access the full list of values. sean@0: """ sean@0: sean@0: def __init__(self, *a, **k): sean@0: self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) sean@0: sean@0: def __len__(self): sean@0: return len(self.dict) sean@0: sean@0: def __iter__(self): sean@0: return iter(self.dict) sean@0: sean@0: def __contains__(self, key): sean@0: return key in self.dict sean@0: sean@0: def __delitem__(self, key): sean@0: del self.dict[key] sean@0: sean@0: def __getitem__(self, key): sean@0: return self.dict[key][-1] sean@0: sean@0: def __setitem__(self, key, value): sean@0: self.append(key, value) sean@0: sean@0: def keys(self): sean@0: return self.dict.keys() sean@0: sean@0: if py3k: sean@0: sean@0: def values(self): sean@0: return (v[-1] for v in self.dict.values()) sean@0: sean@0: def items(self): sean@0: return ((k, v[-1]) for k, v in self.dict.items()) sean@0: sean@0: def allitems(self): sean@0: return ((k, v) for k, vl in self.dict.items() for v in vl) sean@0: sean@0: iterkeys = keys sean@0: itervalues = values sean@0: iteritems = items sean@0: iterallitems = allitems sean@0: sean@0: else: sean@0: sean@0: def values(self): sean@0: return [v[-1] for v in self.dict.values()] sean@0: sean@0: def items(self): sean@0: return [(k, v[-1]) for k, v in self.dict.items()] sean@0: sean@0: def iterkeys(self): sean@0: return self.dict.iterkeys() sean@0: sean@0: def itervalues(self): sean@0: return (v[-1] for v in self.dict.itervalues()) sean@0: sean@0: def iteritems(self): sean@0: return ((k, v[-1]) for k, v in self.dict.iteritems()) sean@0: sean@0: def iterallitems(self): sean@0: return ((k, v) for k, vl in self.dict.iteritems() for v in vl) sean@0: sean@0: def allitems(self): sean@0: return [(k, v) for k, vl in self.dict.iteritems() for v in vl] sean@0: sean@0: def get(self, key, default=None, index=-1, type=None): sean@0: """ Return the most recent value for a key. sean@0: sean@0: :param default: The default value to be returned if the key is not sean@0: present or the type conversion fails. sean@0: :param index: An index for the list of available values. sean@0: :param type: If defined, this callable is used to cast the value sean@0: into a specific type. Exception are suppressed and result in sean@0: the default value to be returned. sean@0: """ sean@0: try: sean@0: val = self.dict[key][index] sean@0: return type(val) if type else val sean@0: except Exception: sean@0: pass sean@0: return default sean@0: sean@0: def append(self, key, value): sean@0: """ Add a new value to the list of values for this key. """ sean@0: self.dict.setdefault(key, []).append(value) sean@0: sean@0: def replace(self, key, value): sean@0: """ Replace the list of values with a single value. """ sean@0: self.dict[key] = [value] sean@0: sean@0: def getall(self, key): sean@0: """ Return a (possibly empty) list of values for a key. """ sean@0: return self.dict.get(key) or [] sean@0: sean@0: #: Aliases for WTForms to mimic other multi-dict APIs (Django) sean@0: getone = get sean@0: getlist = getall sean@0: sean@0: sean@0: class FormsDict(MultiDict): sean@0: """ This :class:`MultiDict` subclass is used to store request form data. sean@0: Additionally to the normal dict-like item access methods (which return sean@0: unmodified data as native strings), this container also supports sean@0: attribute-like access to its values. Attributes are automatically de- sean@0: or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing sean@0: attributes default to an empty string. """ sean@0: sean@0: #: Encoding used for attribute values. sean@0: input_encoding = 'utf8' sean@0: #: If true (default), unicode strings are first encoded with `latin1` sean@0: #: and then decoded to match :attr:`input_encoding`. sean@0: recode_unicode = True sean@0: sean@0: def _fix(self, s, encoding=None): sean@0: if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI sean@0: return s.encode('latin1').decode(encoding or self.input_encoding) sean@0: elif isinstance(s, bytes): # Python 2 WSGI sean@0: return s.decode(encoding or self.input_encoding) sean@0: else: sean@0: return s sean@0: sean@0: def decode(self, encoding=None): sean@0: """ Returns a copy with all keys and values de- or recoded to match sean@0: :attr:`input_encoding`. Some libraries (e.g. WTForms) want a sean@0: unicode dictionary. """ sean@0: copy = FormsDict() sean@0: enc = copy.input_encoding = encoding or self.input_encoding sean@0: copy.recode_unicode = False sean@0: for key, value in self.allitems(): sean@0: copy.append(self._fix(key, enc), self._fix(value, enc)) sean@0: return copy sean@0: sean@0: def getunicode(self, name, default=None, encoding=None): sean@0: """ Return the value as a unicode string, or the default. """ sean@0: try: sean@0: return self._fix(self[name], encoding) sean@0: except (UnicodeError, KeyError): sean@0: return default sean@0: sean@0: def __getattr__(self, name, default=unicode()): sean@0: # Without this guard, pickle generates a cryptic TypeError: sean@0: if name.startswith('__') and name.endswith('__'): sean@0: return super(FormsDict, self).__getattr__(name) sean@0: return self.getunicode(name, default=default) sean@0: sean@0: sean@0: class HeaderDict(MultiDict): sean@0: """ A case-insensitive version of :class:`MultiDict` that defaults to sean@0: replace the old value instead of appending it. """ sean@0: sean@0: def __init__(self, *a, **ka): sean@0: self.dict = {} sean@0: if a or ka: self.update(*a, **ka) sean@0: sean@0: def __contains__(self, key): sean@0: return _hkey(key) in self.dict sean@0: sean@0: def __delitem__(self, key): sean@0: del self.dict[_hkey(key)] sean@0: sean@0: def __getitem__(self, key): sean@0: return self.dict[_hkey(key)][-1] sean@0: sean@0: def __setitem__(self, key, value): sean@0: self.dict[_hkey(key)] = [value if isinstance(value, unicode) else sean@0: str(value)] sean@0: sean@0: def append(self, key, value): sean@0: self.dict.setdefault(_hkey(key), []).append( sean@0: value if isinstance(value, unicode) else str(value)) sean@0: sean@0: def replace(self, key, value): sean@0: self.dict[_hkey(key)] = [value if isinstance(value, unicode) else sean@0: str(value)] sean@0: sean@0: def getall(self, key): sean@0: return self.dict.get(_hkey(key)) or [] sean@0: sean@0: def get(self, key, default=None, index=-1): sean@0: return MultiDict.get(self, _hkey(key), default, index) sean@0: sean@0: def filter(self, names): sean@0: for name in [_hkey(n) for n in names]: sean@0: if name in self.dict: sean@0: del self.dict[name] sean@0: sean@0: sean@0: class WSGIHeaderDict(DictMixin): sean@0: """ This dict-like class wraps a WSGI environ dict and provides convenient sean@0: access to HTTP_* fields. Keys and values are native strings sean@0: (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI sean@0: environment contains non-native string values, these are de- or encoded sean@0: using a lossless 'latin1' character set. sean@0: sean@0: The API will remain stable even on changes to the relevant PEPs. sean@0: Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one sean@0: that uses non-native strings.) sean@0: """ sean@0: #: List of keys that do not have a ``HTTP_`` prefix. sean@0: cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') sean@0: sean@0: def __init__(self, environ): sean@0: self.environ = environ sean@0: sean@0: def _ekey(self, key): sean@0: """ Translate header field name to CGI/WSGI environ key. """ sean@0: key = key.replace('-', '_').upper() sean@0: if key in self.cgikeys: sean@0: return key sean@0: return 'HTTP_' + key sean@0: sean@0: def raw(self, key, default=None): sean@0: """ Return the header value as is (may be bytes or unicode). """ sean@0: return self.environ.get(self._ekey(key), default) sean@0: sean@0: def __getitem__(self, key): sean@0: val = self.environ[self._ekey(key)] sean@0: if py3k: sean@0: if isinstance(val, unicode): sean@0: val = val.encode('latin1').decode('utf8') sean@0: else: sean@0: val = val.decode('utf8') sean@0: return val sean@0: sean@0: def __setitem__(self, key, value): sean@0: raise TypeError("%s is read-only." % self.__class__) sean@0: sean@0: def __delitem__(self, key): sean@0: raise TypeError("%s is read-only." % self.__class__) sean@0: sean@0: def __iter__(self): sean@0: for key in self.environ: sean@0: if key[:5] == 'HTTP_': sean@0: yield _hkey(key[5:]) sean@0: elif key in self.cgikeys: sean@0: yield _hkey(key) sean@0: sean@0: def keys(self): sean@0: return [x for x in self] sean@0: sean@0: def __len__(self): sean@0: return len(self.keys()) sean@0: sean@0: def __contains__(self, key): sean@0: return self._ekey(key) in self.environ sean@0: sean@0: sean@0: class ConfigDict(dict): sean@0: """ A dict-like configuration storage with additional support for sean@0: namespaces, validators, meta-data, on_change listeners and more. sean@0: """ sean@0: sean@0: __slots__ = ('_meta', '_on_change') sean@0: sean@0: def __init__(self): sean@0: self._meta = {} sean@0: self._on_change = lambda name, value: None sean@0: sean@0: def load_config(self, filename): sean@0: """ Load values from an ``*.ini`` style config file. sean@0: sean@0: If the config file contains sections, their names are used as sean@0: namespaces for the values within. The two special sections sean@0: ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). sean@0: """ sean@0: conf = ConfigParser() sean@0: conf.read(filename) sean@0: for section in conf.sections(): sean@0: for key, value in conf.items(section): sean@0: if section not in ('DEFAULT', 'bottle'): sean@0: key = section + '.' + key sean@0: self[key] = value sean@0: return self sean@0: sean@0: def load_dict(self, source, namespace=''): sean@0: """ Load values from a dictionary structure. Nesting can be used to sean@0: represent namespaces. sean@0: sean@0: >>> c = ConfigDict() sean@0: >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) sean@0: {'some.namespace.key': 'value'} sean@0: """ sean@0: for key, value in source.items(): sean@0: if isinstance(key, str): sean@0: nskey = (namespace + '.' + key).strip('.') sean@0: if isinstance(value, dict): sean@0: self.load_dict(value, namespace=nskey) sean@0: else: sean@0: self[nskey] = value sean@0: else: sean@0: raise TypeError('Key has type %r (not a string)' % type(key)) sean@0: return self sean@0: sean@0: def update(self, *a, **ka): sean@0: """ If the first parameter is a string, all keys are prefixed with this sean@0: namespace. Apart from that it works just as the usual dict.update(). sean@0: Example: ``update('some.namespace', key='value')`` """ sean@0: prefix = '' sean@0: if a and isinstance(a[0], str): sean@0: prefix = a[0].strip('.') + '.' sean@0: a = a[1:] sean@0: for key, value in dict(*a, **ka).items(): sean@0: self[prefix + key] = value sean@0: sean@0: def setdefault(self, key, value): sean@0: if key not in self: sean@0: self[key] = value sean@0: return self[key] sean@0: sean@0: def __setitem__(self, key, value): sean@0: if not isinstance(key, str): sean@0: raise TypeError('Key has type %r (not a string)' % type(key)) sean@0: value = self.meta_get(key, 'filter', lambda x: x)(value) sean@0: if key in self and self[key] is value: sean@0: return sean@0: self._on_change(key, value) sean@0: dict.__setitem__(self, key, value) sean@0: sean@0: def __delitem__(self, key): sean@0: self._on_change(key, None) sean@0: dict.__delitem__(self, key) sean@0: sean@0: def meta_get(self, key, metafield, default=None): sean@0: """ Return the value of a meta field for a key. """ sean@0: return self._meta.get(key, {}).get(metafield, default) sean@0: sean@0: def meta_set(self, key, metafield, value): sean@0: """ Set the meta field for a key to a new value. This triggers the sean@0: on-change handler for existing keys. """ sean@0: self._meta.setdefault(key, {})[metafield] = value sean@0: if key in self: sean@0: self[key] = self[key] sean@0: sean@0: def meta_list(self, key): sean@0: """ Return an iterable of meta field names defined for a key. """ sean@0: return self._meta.get(key, {}).keys() sean@0: sean@0: sean@0: class AppStack(list): sean@0: """ A stack-like list. Calling it returns the head of the stack. """ sean@0: sean@0: def __call__(self): sean@0: """ Return the current default application. """ sean@0: return self[-1] sean@0: sean@0: def push(self, value=None): sean@0: """ Add a new :class:`Bottle` instance to the stack """ sean@0: if not isinstance(value, Bottle): sean@0: value = Bottle() sean@0: self.append(value) sean@0: return value sean@0: sean@0: sean@0: class WSGIFileWrapper(object): sean@0: def __init__(self, fp, buffer_size=1024 * 64): sean@0: self.fp, self.buffer_size = fp, buffer_size sean@0: for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): sean@0: if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) sean@0: sean@0: def __iter__(self): sean@0: buff, read = self.buffer_size, self.read sean@0: while True: sean@0: part = read(buff) sean@0: if not part: return sean@0: yield part sean@0: sean@0: sean@0: class _closeiter(object): sean@0: """ This only exists to be able to attach a .close method to iterators that sean@0: do not support attribute assignment (most of itertools). """ sean@0: sean@0: def __init__(self, iterator, close=None): sean@0: self.iterator = iterator sean@0: self.close_callbacks = makelist(close) sean@0: sean@0: def __iter__(self): sean@0: return iter(self.iterator) sean@0: sean@0: def close(self): sean@0: for func in self.close_callbacks: sean@0: func() sean@0: sean@0: sean@0: class ResourceManager(object): sean@0: """ This class manages a list of search paths and helps to find and open sean@0: application-bound resources (files). sean@0: sean@0: :param base: default value for :meth:`add_path` calls. sean@0: :param opener: callable used to open resources. sean@0: :param cachemode: controls which lookups are cached. One of 'all', sean@0: 'found' or 'none'. sean@0: """ sean@0: sean@0: def __init__(self, base='./', opener=open, cachemode='all'): sean@0: self.opener = opener sean@0: self.base = base sean@0: self.cachemode = cachemode sean@0: sean@0: #: A list of search paths. See :meth:`add_path` for details. sean@0: self.path = [] sean@0: #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. sean@0: self.cache = {} sean@0: sean@0: def add_path(self, path, base=None, index=None, create=False): sean@0: """ Add a new path to the list of search paths. Return False if the sean@0: path does not exist. sean@0: sean@0: :param path: The new search path. Relative paths are turned into sean@0: an absolute and normalized form. If the path looks like a file sean@0: (not ending in `/`), the filename is stripped off. sean@0: :param base: Path used to absolutize relative search paths. sean@0: Defaults to :attr:`base` which defaults to ``os.getcwd()``. sean@0: :param index: Position within the list of search paths. Defaults sean@0: to last index (appends to the list). sean@0: sean@0: The `base` parameter makes it easy to reference files installed sean@0: along with a python module or package:: sean@0: sean@0: res.add_path('./resources/', __file__) sean@0: """ sean@0: base = os.path.abspath(os.path.dirname(base or self.base)) sean@0: path = os.path.abspath(os.path.join(base, os.path.dirname(path))) sean@0: path += os.sep sean@0: if path in self.path: sean@0: self.path.remove(path) sean@0: if create and not os.path.isdir(path): sean@0: os.makedirs(path) sean@0: if index is None: sean@0: self.path.append(path) sean@0: else: sean@0: self.path.insert(index, path) sean@0: self.cache.clear() sean@0: return os.path.exists(path) sean@0: sean@0: def __iter__(self): sean@0: """ Iterate over all existing files in all registered paths. """ sean@0: search = self.path[:] sean@0: while search: sean@0: path = search.pop() sean@0: if not os.path.isdir(path): continue sean@0: for name in os.listdir(path): sean@0: full = os.path.join(path, name) sean@0: if os.path.isdir(full): search.append(full) sean@0: else: yield full sean@0: sean@0: def lookup(self, name): sean@0: """ Search for a resource and return an absolute file path, or `None`. sean@0: sean@0: The :attr:`path` list is searched in order. The first match is sean@0: returend. Symlinks are followed. The result is cached to speed up sean@0: future lookups. """ sean@0: if name not in self.cache or DEBUG: sean@0: for path in self.path: sean@0: fpath = os.path.join(path, name) sean@0: if os.path.isfile(fpath): sean@0: if self.cachemode in ('all', 'found'): sean@0: self.cache[name] = fpath sean@0: return fpath sean@0: if self.cachemode == 'all': sean@0: self.cache[name] = None sean@0: return self.cache[name] sean@0: sean@0: def open(self, name, mode='r', *args, **kwargs): sean@0: """ Find a resource and return a file object, or raise IOError. """ sean@0: fname = self.lookup(name) sean@0: if not fname: raise IOError("Resource %r not found." % name) sean@0: return self.opener(fname, mode=mode, *args, **kwargs) sean@0: sean@0: sean@0: class FileUpload(object): sean@0: def __init__(self, fileobj, name, filename, headers=None): sean@0: """ Wrapper for file uploads. """ sean@0: #: Open file(-like) object (BytesIO buffer or temporary file) sean@0: self.file = fileobj sean@0: #: Name of the upload form field sean@0: self.name = name sean@0: #: Raw filename as sent by the client (may contain unsafe characters) sean@0: self.raw_filename = filename sean@0: #: A :class:`HeaderDict` with additional headers (e.g. content-type) sean@0: self.headers = HeaderDict(headers) if headers else HeaderDict() sean@0: sean@0: content_type = HeaderProperty('Content-Type') sean@0: content_length = HeaderProperty('Content-Length', reader=int, default=-1) sean@0: sean@0: @cached_property sean@0: def filename(self): sean@0: """ Name of the file on the client file system, but normalized to ensure sean@0: file system compatibility. An empty filename is returned as 'empty'. sean@0: sean@0: Only ASCII letters, digits, dashes, underscores and dots are sean@0: allowed in the final filename. Accents are removed, if possible. sean@0: Whitespace is replaced by a single dash. Leading or tailing dots sean@0: or dashes are removed. The filename is limited to 255 characters. sean@0: """ sean@0: fname = self.raw_filename sean@0: if not isinstance(fname, unicode): sean@0: fname = fname.decode('utf8', 'ignore') sean@0: fname = normalize('NFKD', fname) sean@0: fname = fname.encode('ASCII', 'ignore').decode('ASCII') sean@0: fname = os.path.basename(fname.replace('\\', os.path.sep)) sean@0: fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() sean@0: fname = re.sub(r'[-\s]+', '-', fname).strip('.-') sean@0: return fname[:255] or 'empty' sean@0: sean@0: def _copy_file(self, fp, chunk_size=2 ** 16): sean@0: read, write, offset = self.file.read, fp.write, self.file.tell() sean@0: while 1: sean@0: buf = read(chunk_size) sean@0: if not buf: break sean@0: write(buf) sean@0: self.file.seek(offset) sean@0: sean@0: def save(self, destination, overwrite=False, chunk_size=2 ** 16): sean@0: """ Save file to disk or copy its content to an open file(-like) object. sean@0: If *destination* is a directory, :attr:`filename` is added to the sean@0: path. Existing files are not overwritten by default (IOError). sean@0: sean@0: :param destination: File path, directory or file(-like) object. sean@0: :param overwrite: If True, replace existing files. (default: False) sean@0: :param chunk_size: Bytes to read at a time. (default: 64kb) sean@0: """ sean@0: if isinstance(destination, basestring): # Except file-likes here sean@0: if os.path.isdir(destination): sean@0: destination = os.path.join(destination, self.filename) sean@0: if not overwrite and os.path.exists(destination): sean@0: raise IOError('File exists.') sean@0: with open(destination, 'wb') as fp: sean@0: self._copy_file(fp, chunk_size) sean@0: else: sean@0: self._copy_file(destination, chunk_size) sean@0: sean@0: ############################################################################### sean@0: # Application Helper ########################################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: def abort(code=500, text='Unknown Error.'): sean@0: """ Aborts execution and causes a HTTP error. """ sean@0: raise HTTPError(code, text) sean@0: sean@0: sean@0: def redirect(url, code=None): sean@0: """ Aborts execution and causes a 303 or 302 redirect, depending on sean@0: the HTTP protocol version. """ sean@0: if not code: sean@0: code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 sean@0: res = response.copy(cls=HTTPResponse) sean@0: res.status = code sean@0: res.body = "" sean@0: res.set_header('Location', urljoin(request.url, url)) sean@0: raise res sean@0: sean@0: sean@0: def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): sean@0: """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" sean@0: fp.seek(offset) sean@0: while bytes > 0: sean@0: part = fp.read(min(bytes, maxread)) sean@0: if not part: break sean@0: bytes -= len(part) sean@0: yield part sean@0: sean@0: sean@0: def static_file(filename, root, sean@0: mimetype='auto', sean@0: download=False, sean@0: charset='UTF-8'): sean@0: """ Open a file in a safe way and return :exc:`HTTPResponse` with status sean@0: code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, sean@0: ``Content-Length`` and ``Last-Modified`` headers are set if possible. sean@0: Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` sean@0: requests. sean@0: sean@0: :param filename: Name or path of the file to send. sean@0: :param root: Root path for file lookups. Should be an absolute directory sean@0: path. sean@0: :param mimetype: Defines the content-type header (default: guess from sean@0: file extension) sean@0: :param download: If True, ask the browser to open a `Save as...` dialog sean@0: instead of opening the file with the associated program. You can sean@0: specify a custom filename as a string. If not specified, the sean@0: original filename is used (default: False). sean@0: :param charset: The charset to use for files with a ``text/*`` sean@0: mime-type. (default: UTF-8) sean@0: """ sean@0: sean@0: root = os.path.abspath(root) + os.sep sean@0: filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) sean@0: headers = dict() sean@0: sean@0: if not filename.startswith(root): sean@0: return HTTPError(403, "Access denied.") sean@0: if not os.path.exists(filename) or not os.path.isfile(filename): sean@0: return HTTPError(404, "File does not exist.") sean@0: if not os.access(filename, os.R_OK): sean@0: return HTTPError(403, "You do not have permission to access this file.") sean@0: sean@0: if mimetype == 'auto': sean@0: if download and download != True: sean@0: mimetype, encoding = mimetypes.guess_type(download) sean@0: else: sean@0: mimetype, encoding = mimetypes.guess_type(filename) sean@0: if encoding: headers['Content-Encoding'] = encoding sean@0: sean@0: if mimetype: sean@0: if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: sean@0: mimetype += '; charset=%s' % charset sean@0: headers['Content-Type'] = mimetype sean@0: sean@0: if download: sean@0: download = os.path.basename(filename if download == True else download) sean@0: headers['Content-Disposition'] = 'attachment; filename="%s"' % download sean@0: sean@0: stats = os.stat(filename) sean@0: headers['Content-Length'] = clen = stats.st_size sean@0: lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) sean@0: headers['Last-Modified'] = lm sean@0: sean@0: ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') sean@0: if ims: sean@0: ims = parse_date(ims.split(";")[0].strip()) sean@0: if ims is not None and ims >= int(stats.st_mtime): sean@0: headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", sean@0: time.gmtime()) sean@0: return HTTPResponse(status=304, **headers) sean@0: sean@0: body = '' if request.method == 'HEAD' else open(filename, 'rb') sean@0: sean@0: headers["Accept-Ranges"] = "bytes" sean@0: ranges = request.environ.get('HTTP_RANGE') sean@0: if 'HTTP_RANGE' in request.environ: sean@0: ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) sean@0: if not ranges: sean@0: return HTTPError(416, "Requested Range Not Satisfiable") sean@0: offset, end = ranges[0] sean@0: headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) sean@0: headers["Content-Length"] = str(end - offset) sean@0: if body: body = _file_iter_range(body, offset, end - offset) sean@0: return HTTPResponse(body, status=206, **headers) sean@0: return HTTPResponse(body, **headers) sean@0: sean@0: ############################################################################### sean@0: # HTTP Utilities and MISC (TODO) ############################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: def debug(mode=True): sean@0: """ Change the debug level. sean@0: There is only one debug level supported at the moment.""" sean@0: global DEBUG sean@0: if mode: warnings.simplefilter('default') sean@0: DEBUG = bool(mode) sean@0: sean@0: sean@0: def http_date(value): sean@0: if isinstance(value, (datedate, datetime)): sean@0: value = value.utctimetuple() sean@0: elif isinstance(value, (int, float)): sean@0: value = time.gmtime(value) sean@0: if not isinstance(value, basestring): sean@0: value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) sean@0: return value sean@0: sean@0: sean@0: def parse_date(ims): sean@0: """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ sean@0: try: sean@0: ts = email.utils.parsedate_tz(ims) sean@0: return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone sean@0: except (TypeError, ValueError, IndexError, OverflowError): sean@0: return None sean@0: sean@0: sean@0: def parse_auth(header): sean@0: """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" sean@0: try: sean@0: method, data = header.split(None, 1) sean@0: if method.lower() == 'basic': sean@0: user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) sean@0: return user, pwd sean@0: except (KeyError, ValueError): sean@0: return None sean@0: sean@0: sean@0: def parse_range_header(header, maxlen=0): sean@0: """ Yield (start, end) ranges parsed from a HTTP Range header. Skip sean@0: unsatisfiable ranges. The end index is non-inclusive.""" sean@0: if not header or header[:6] != 'bytes=': return sean@0: ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] sean@0: for start, end in ranges: sean@0: try: sean@0: if not start: # bytes=-100 -> last 100 bytes sean@0: start, end = max(0, maxlen - int(end)), maxlen sean@0: elif not end: # bytes=100- -> all but the first 99 bytes sean@0: start, end = int(start), maxlen sean@0: else: # bytes=100-200 -> bytes 100-200 (inclusive) sean@0: start, end = int(start), min(int(end) + 1, maxlen) sean@0: if 0 <= start < end <= maxlen: sean@0: yield start, end sean@0: except ValueError: sean@0: pass sean@0: sean@0: sean@0: def _parse_qsl(qs): sean@0: r = [] sean@0: for pair in qs.replace(';', '&').split('&'): sean@0: if not pair: continue sean@0: nv = pair.split('=', 1) sean@0: if len(nv) != 2: nv.append('') sean@0: key = urlunquote(nv[0].replace('+', ' ')) sean@0: value = urlunquote(nv[1].replace('+', ' ')) sean@0: r.append((key, value)) sean@0: return r sean@0: sean@0: sean@0: def _lscmp(a, b): sean@0: """ Compares two strings in a cryptographically safe way: sean@0: Runtime is not affected by length of common prefix. """ sean@0: return not sum(0 if x == y else 1 sean@0: for x, y in zip(a, b)) and len(a) == len(b) sean@0: sean@0: sean@0: def cookie_encode(data, key): sean@0: """ Encode and sign a pickle-able object. Return a (byte) string """ sean@0: msg = base64.b64encode(pickle.dumps(data, -1)) sean@0: sig = base64.b64encode(hmac.new(tob(key), msg).digest()) sean@0: return tob('!') + sig + tob('?') + msg sean@0: sean@0: sean@0: def cookie_decode(data, key): sean@0: """ Verify and decode an encoded string. Return an object or None.""" sean@0: data = tob(data) sean@0: if cookie_is_encoded(data): sean@0: sig, msg = data.split(tob('?'), 1) sean@0: if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): sean@0: return pickle.loads(base64.b64decode(msg)) sean@0: return None sean@0: sean@0: sean@0: def cookie_is_encoded(data): sean@0: """ Return True if the argument looks like a encoded cookie.""" sean@0: return bool(data.startswith(tob('!')) and tob('?') in data) sean@0: sean@0: sean@0: def html_escape(string): sean@0: """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ sean@0: return string.replace('&', '&').replace('<', '<').replace('>', '>')\ sean@0: .replace('"', '"').replace("'", ''') sean@0: sean@0: sean@0: def html_quote(string): sean@0: """ Escape and quote a string to be used as an HTTP attribute.""" sean@0: return '"%s"' % html_escape(string).replace('\n', ' ')\ sean@0: .replace('\r', ' ').replace('\t', ' ') sean@0: sean@0: sean@0: def yieldroutes(func): sean@0: """ Return a generator for routes that match the signature (name, args) sean@0: of the func parameter. This may yield more than one route if the function sean@0: takes optional keyword arguments. The output is best described by example:: sean@0: sean@0: a() -> '/a' sean@0: b(x, y) -> '/b//' sean@0: c(x, y=5) -> '/c/' and '/c//' sean@0: d(x=5, y=6) -> '/d' and '/d/' and '/d//' sean@0: """ sean@0: path = '/' + func.__name__.replace('__', '/').lstrip('/') sean@0: spec = getargspec(func) sean@0: argc = len(spec[0]) - len(spec[3] or []) sean@0: path += ('/<%s>' * argc) % tuple(spec[0][:argc]) sean@0: yield path sean@0: for arg in spec[0][argc:]: sean@0: path += '/<%s>' % arg sean@0: yield path sean@0: sean@0: sean@0: def path_shift(script_name, path_info, shift=1): sean@0: """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. sean@0: sean@0: :return: The modified paths. sean@0: :param script_name: The SCRIPT_NAME path. sean@0: :param script_name: The PATH_INFO path. sean@0: :param shift: The number of path fragments to shift. May be negative to sean@0: change the shift direction. (default: 1) sean@0: """ sean@0: if shift == 0: return script_name, path_info sean@0: pathlist = path_info.strip('/').split('/') sean@0: scriptlist = script_name.strip('/').split('/') sean@0: if pathlist and pathlist[0] == '': pathlist = [] sean@0: if scriptlist and scriptlist[0] == '': scriptlist = [] sean@0: if 0 < shift <= len(pathlist): sean@0: moved = pathlist[:shift] sean@0: scriptlist = scriptlist + moved sean@0: pathlist = pathlist[shift:] sean@0: elif 0 > shift >= -len(scriptlist): sean@0: moved = scriptlist[shift:] sean@0: pathlist = moved + pathlist sean@0: scriptlist = scriptlist[:shift] sean@0: else: sean@0: empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' sean@0: raise AssertionError("Cannot shift. Nothing left from %s" % empty) sean@0: new_script_name = '/' + '/'.join(scriptlist) sean@0: new_path_info = '/' + '/'.join(pathlist) sean@0: if path_info.endswith('/') and pathlist: new_path_info += '/' sean@0: return new_script_name, new_path_info sean@0: sean@0: sean@0: def auth_basic(check, realm="private", text="Access denied"): sean@0: """ Callback decorator to require HTTP auth (basic). sean@0: TODO: Add route(check_auth=...) parameter. """ sean@0: sean@0: def decorator(func): sean@0: sean@0: @functools.wraps(func) sean@0: def wrapper(*a, **ka): sean@0: user, password = request.auth or (None, None) sean@0: if user is None or not check(user, password): sean@0: err = HTTPError(401, text) sean@0: err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) sean@0: return err sean@0: return func(*a, **ka) sean@0: sean@0: return wrapper sean@0: sean@0: return decorator sean@0: sean@0: # Shortcuts for common Bottle methods. sean@0: # They all refer to the current default application. sean@0: sean@0: sean@0: def make_default_app_wrapper(name): sean@0: """ Return a callable that relays calls to the current default app. """ sean@0: sean@0: @functools.wraps(getattr(Bottle, name)) sean@0: def wrapper(*a, **ka): sean@0: return getattr(app(), name)(*a, **ka) sean@0: sean@0: return wrapper sean@0: sean@0: sean@0: route = make_default_app_wrapper('route') sean@0: get = make_default_app_wrapper('get') sean@0: post = make_default_app_wrapper('post') sean@0: put = make_default_app_wrapper('put') sean@0: delete = make_default_app_wrapper('delete') sean@0: patch = make_default_app_wrapper('patch') sean@0: error = make_default_app_wrapper('error') sean@0: mount = make_default_app_wrapper('mount') sean@0: hook = make_default_app_wrapper('hook') sean@0: install = make_default_app_wrapper('install') sean@0: uninstall = make_default_app_wrapper('uninstall') sean@0: url = make_default_app_wrapper('get_url') sean@0: sean@0: ############################################################################### sean@0: # Server Adapter ############################################################### sean@0: ############################################################################### sean@0: sean@0: sean@0: class ServerAdapter(object): sean@0: quiet = False sean@0: sean@0: def __init__(self, host='127.0.0.1', port=8080, **options): sean@0: self.options = options sean@0: self.host = host sean@0: self.port = int(port) sean@0: sean@0: def run(self, handler): # pragma: no cover sean@0: pass sean@0: sean@0: def __repr__(self): sean@0: args = ', '.join(['%s=%s' % (k, repr(v)) sean@0: for k, v in self.options.items()]) sean@0: return "%s(%s)" % (self.__class__.__name__, args) sean@0: sean@0: sean@0: class CGIServer(ServerAdapter): sean@0: quiet = True sean@0: sean@0: def run(self, handler): # pragma: no cover sean@0: from wsgiref.handlers import CGIHandler sean@0: sean@0: def fixed_environ(environ, start_response): sean@0: environ.setdefault('PATH_INFO', '') sean@0: return handler(environ, start_response) sean@0: sean@0: CGIHandler().run(fixed_environ) sean@0: sean@0: sean@0: class FlupFCGIServer(ServerAdapter): sean@0: def run(self, handler): # pragma: no cover sean@0: import flup.server.fcgi sean@0: self.options.setdefault('bindAddress', (self.host, self.port)) sean@0: flup.server.fcgi.WSGIServer(handler, **self.options).run() sean@0: sean@0: sean@0: class WSGIRefServer(ServerAdapter): sean@0: def run(self, app): # pragma: no cover sean@0: from wsgiref.simple_server import make_server sean@0: from wsgiref.simple_server import WSGIRequestHandler, WSGIServer sean@0: import socket sean@0: sean@0: class FixedHandler(WSGIRequestHandler): sean@0: def address_string(self): # Prevent reverse DNS lookups please. sean@0: return self.client_address[0] sean@0: sean@0: def log_request(*args, **kw): sean@0: if not self.quiet: sean@0: return WSGIRequestHandler.log_request(*args, **kw) sean@0: sean@0: handler_cls = self.options.get('handler_class', FixedHandler) sean@0: server_cls = self.options.get('server_class', WSGIServer) sean@0: sean@0: if ':' in self.host: # Fix wsgiref for IPv6 addresses. sean@0: if getattr(server_cls, 'address_family') == socket.AF_INET: sean@0: sean@0: class server_cls(server_cls): sean@0: address_family = socket.AF_INET6 sean@0: sean@0: self.srv = make_server(self.host, self.port, app, server_cls, sean@0: handler_cls) sean@0: self.port = self.srv.server_port # update port actual port (0 means random) sean@0: try: sean@0: self.srv.serve_forever() sean@0: except KeyboardInterrupt: sean@0: self.srv.server_close() # Prevent ResourceWarning: unclosed socket sean@0: raise sean@0: sean@0: sean@0: class CherryPyServer(ServerAdapter): sean@0: def run(self, handler): # pragma: no cover sean@0: from cherrypy import wsgiserver sean@0: self.options['bind_addr'] = (self.host, self.port) sean@0: self.options['wsgi_app'] = handler sean@0: sean@0: certfile = self.options.get('certfile') sean@0: if certfile: sean@0: del self.options['certfile'] sean@0: keyfile = self.options.get('keyfile') sean@0: if keyfile: sean@0: del self.options['keyfile'] sean@0: sean@0: server = wsgiserver.CherryPyWSGIServer(**self.options) sean@0: if certfile: sean@0: server.ssl_certificate = certfile sean@0: if keyfile: sean@0: server.ssl_private_key = keyfile sean@0: sean@0: try: sean@0: server.start() sean@0: finally: sean@0: server.stop() sean@0: sean@0: sean@0: class WaitressServer(ServerAdapter): sean@0: def run(self, handler): sean@0: from waitress import serve sean@0: serve(handler, host=self.host, port=self.port, _quiet=self.quiet) sean@0: sean@0: sean@0: class PasteServer(ServerAdapter): sean@0: def run(self, handler): # pragma: no cover sean@0: from paste import httpserver sean@0: from paste.translogger import TransLogger sean@0: handler = TransLogger(handler, setup_console_handler=(not self.quiet)) sean@0: httpserver.serve(handler, sean@0: host=self.host, sean@0: port=str(self.port), **self.options) sean@0: sean@0: sean@0: class MeinheldServer(ServerAdapter): sean@0: def run(self, handler): sean@0: from meinheld import server sean@0: server.listen((self.host, self.port)) sean@0: server.run(handler) sean@0: sean@0: sean@0: class FapwsServer(ServerAdapter): sean@0: """ Extremely fast webserver using libev. See http://www.fapws.org/ """ sean@0: sean@0: def run(self, handler): # pragma: no cover sean@0: import fapws._evwsgi as evwsgi sean@0: from fapws import base, config sean@0: port = self.port sean@0: if float(config.SERVER_IDENT[-2:]) > 0.4: sean@0: # fapws3 silently changed its API in 0.5 sean@0: port = str(port) sean@0: evwsgi.start(self.host, port) sean@0: # fapws3 never releases the GIL. Complain upstream. I tried. No luck. sean@0: if 'BOTTLE_CHILD' in os.environ and not self.quiet: sean@0: _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") sean@0: _stderr(" (Fapws3 breaks python thread support)\n") sean@0: evwsgi.set_base_module(base) sean@0: sean@0: def app(environ, start_response): sean@0: environ['wsgi.multiprocess'] = False sean@0: return handler(environ, start_response) sean@0: sean@0: evwsgi.wsgi_cb(('', app)) sean@0: evwsgi.run() sean@0: sean@0: sean@0: class TornadoServer(ServerAdapter): sean@0: """ The super hyped asynchronous server by facebook. Untested. """ sean@0: sean@0: def run(self, handler): # pragma: no cover sean@0: import tornado.wsgi, tornado.httpserver, tornado.ioloop sean@0: container = tornado.wsgi.WSGIContainer(handler) sean@0: server = tornado.httpserver.HTTPServer(container) sean@0: server.listen(port=self.port, address=self.host) sean@0: tornado.ioloop.IOLoop.instance().start() sean@0: sean@0: sean@0: class AppEngineServer(ServerAdapter): sean@0: """ Adapter for Google App Engine. """ sean@0: quiet = True sean@0: sean@0: def run(self, handler): sean@0: from google.appengine.ext.webapp import util sean@0: # A main() function in the handler script enables 'App Caching'. sean@0: # Lets makes sure it is there. This _really_ improves performance. sean@0: module = sys.modules.get('__main__') sean@0: if module and not hasattr(module, 'main'): sean@0: module.main = lambda: util.run_wsgi_app(handler) sean@0: util.run_wsgi_app(handler) sean@0: sean@0: sean@0: class TwistedServer(ServerAdapter): sean@0: """ Untested. """ sean@0: sean@0: def run(self, handler): sean@0: from twisted.web import server, wsgi sean@0: from twisted.python.threadpool import ThreadPool sean@0: from twisted.internet import reactor sean@0: thread_pool = ThreadPool() sean@0: thread_pool.start() sean@0: reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) sean@0: factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) sean@0: reactor.listenTCP(self.port, factory, interface=self.host) sean@0: if not reactor.running: sean@0: reactor.run() sean@0: sean@0: sean@0: class DieselServer(ServerAdapter): sean@0: """ Untested. """ sean@0: sean@0: def run(self, handler): sean@0: from diesel.protocols.wsgi import WSGIApplication sean@0: app = WSGIApplication(handler, port=self.port) sean@0: app.run() sean@0: sean@0: sean@0: class GeventServer(ServerAdapter): sean@0: """ Untested. Options: sean@0: sean@0: * `fast` (default: False) uses libevent's http server, but has some sean@0: issues: No streaming, no pipelining, no SSL. sean@0: * See gevent.wsgi.WSGIServer() documentation for more options. sean@0: """ sean@0: sean@0: def run(self, handler): sean@0: from gevent import wsgi, pywsgi, local sean@0: if not isinstance(threading.local(), local.local): sean@0: msg = "Bottle requires gevent.monkey.patch_all() (before import)" sean@0: raise RuntimeError(msg) sean@0: if not self.options.pop('fast', None): wsgi = pywsgi sean@0: self.options['log'] = None if self.quiet else 'default' sean@0: address = (self.host, self.port) sean@0: server = wsgi.WSGIServer(address, handler, **self.options) sean@0: if 'BOTTLE_CHILD' in os.environ: sean@0: import signal sean@0: signal.signal(signal.SIGINT, lambda s, f: server.stop()) sean@0: server.serve_forever() sean@0: sean@0: sean@0: class GeventSocketIOServer(ServerAdapter): sean@0: def run(self, handler): sean@0: from socketio import server sean@0: address = (self.host, self.port) sean@0: server.SocketIOServer(address, handler, **self.options).serve_forever() sean@0: sean@0: sean@0: class GunicornServer(ServerAdapter): sean@0: """ Untested. See http://gunicorn.org/configure.html for options. """ sean@0: sean@0: def run(self, handler): sean@0: from gunicorn.app.base import Application sean@0: sean@0: config = {'bind': "%s:%d" % (self.host, int(self.port))} sean@0: config.update(self.options) sean@0: sean@0: class GunicornApplication(Application): sean@0: def init(self, parser, opts, args): sean@0: return config sean@0: sean@0: def load(self): sean@0: return handler sean@0: sean@0: GunicornApplication().run() sean@0: sean@0: sean@0: class EventletServer(ServerAdapter): sean@0: """ Untested. Options: sean@0: sean@0: * `backlog` adjust the eventlet backlog parameter which is the maximum sean@0: number of queued connections. Should be at least 1; the maximum sean@0: value is system-dependent. sean@0: * `family`: (default is 2) socket family, optional. See socket sean@0: documentation for available families. sean@0: """ sean@0: sean@0: def run(self, handler): sean@0: from eventlet import wsgi, listen, patcher sean@0: if not patcher.is_monkey_patched(os): sean@0: msg = "Bottle requires eventlet.monkey_patch() (before import)" sean@0: raise RuntimeError(msg) sean@0: socket_args = {} sean@0: for arg in ('backlog', 'family'): sean@0: try: sean@0: socket_args[arg] = self.options.pop(arg) sean@0: except KeyError: sean@0: pass sean@0: address = (self.host, self.port) sean@0: try: sean@0: wsgi.server(listen(address, **socket_args), handler, sean@0: log_output=(not self.quiet)) sean@0: except TypeError: sean@0: # Fallback, if we have old version of eventlet sean@0: wsgi.server(listen(address), handler) sean@0: sean@0: sean@0: class RocketServer(ServerAdapter): sean@0: """ Untested. """ sean@0: sean@0: def run(self, handler): sean@0: from rocket import Rocket sean@0: server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) sean@0: server.start() sean@0: sean@0: sean@0: class BjoernServer(ServerAdapter): sean@0: """ Fast server written in C: https://github.com/jonashaag/bjoern """ sean@0: sean@0: def run(self, handler): sean@0: from bjoern import run sean@0: run(handler, self.host, self.port) sean@0: sean@0: sean@0: class AiohttpServer(ServerAdapter): sean@0: """ Untested. sean@0: aiohttp sean@0: https://pypi.python.org/pypi/aiohttp/ sean@0: """ sean@0: sean@0: def run(self, handler): sean@0: import asyncio sean@0: from aiohttp.wsgi import WSGIServerHttpProtocol sean@0: self.loop = asyncio.new_event_loop() sean@0: asyncio.set_event_loop(self.loop) sean@0: sean@0: protocol_factory = lambda: WSGIServerHttpProtocol( sean@0: handler, sean@0: readpayload=True, sean@0: debug=(not self.quiet)) sean@0: self.loop.run_until_complete(self.loop.create_server(protocol_factory, sean@0: self.host, sean@0: self.port)) sean@0: sean@0: if 'BOTTLE_CHILD' in os.environ: sean@0: import signal sean@0: signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) sean@0: sean@0: try: sean@0: self.loop.run_forever() sean@0: except KeyboardInterrupt: sean@0: self.loop.stop() sean@0: sean@0: sean@0: class AutoServer(ServerAdapter): sean@0: """ Untested. """ sean@0: adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, sean@0: WSGIRefServer] sean@0: sean@0: def run(self, handler): sean@0: for sa in self.adapters: sean@0: try: sean@0: return sa(self.host, self.port, **self.options).run(handler) sean@0: except ImportError: sean@0: pass sean@0: sean@0: sean@0: server_names = { sean@0: 'cgi': CGIServer, sean@0: 'flup': FlupFCGIServer, sean@0: 'wsgiref': WSGIRefServer, sean@0: 'waitress': WaitressServer, sean@0: 'cherrypy': CherryPyServer, sean@0: 'paste': PasteServer, sean@0: 'fapws3': FapwsServer, sean@0: 'tornado': TornadoServer, sean@0: 'gae': AppEngineServer, sean@0: 'twisted': TwistedServer, sean@0: 'diesel': DieselServer, sean@0: 'meinheld': MeinheldServer, sean@0: 'gunicorn': GunicornServer, sean@0: 'eventlet': EventletServer, sean@0: 'gevent': GeventServer, sean@0: 'geventSocketIO': GeventSocketIOServer, sean@0: 'rocket': RocketServer, sean@0: 'bjoern': BjoernServer, sean@0: 'aiohttp': AiohttpServer, sean@0: 'auto': AutoServer, sean@0: } sean@0: sean@0: ############################################################################### sean@0: # Application Control ########################################################## sean@0: ############################################################################### sean@0: sean@0: sean@0: def load(target, **namespace): sean@0: """ Import a module or fetch an object from a module. sean@0: sean@0: * ``package.module`` returns `module` as a module object. sean@0: * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. sean@0: * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. sean@0: sean@0: The last form accepts not only function calls, but any type of sean@0: expression. Keyword arguments passed to this function are available as sean@0: local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` sean@0: """ sean@0: module, target = target.split(":", 1) if ':' in target else (target, None) sean@0: if module not in sys.modules: __import__(module) sean@0: if not target: return sys.modules[module] sean@0: if target.isalnum(): return getattr(sys.modules[module], target) sean@0: package_name = module.split('.')[0] sean@0: namespace[package_name] = sys.modules[package_name] sean@0: return eval('%s.%s' % (module, target), namespace) sean@0: sean@0: sean@0: def load_app(target): sean@0: """ Load a bottle application from a module and make sure that the import sean@0: does not affect the current default application, but returns a separate sean@0: application object. See :func:`load` for the target parameter. """ sean@0: global NORUN sean@0: NORUN, nr_old = True, NORUN sean@0: tmp = default_app.push() # Create a new "default application" sean@0: try: sean@0: rv = load(target) # Import the target module sean@0: return rv if callable(rv) else tmp sean@0: finally: sean@0: default_app.remove(tmp) # Remove the temporary added default application sean@0: NORUN = nr_old sean@0: sean@0: sean@0: _debug = debug sean@0: sean@0: sean@0: def run(app=None, sean@0: server='wsgiref', sean@0: host='127.0.0.1', sean@0: port=8080, sean@0: interval=1, sean@0: reloader=False, sean@0: quiet=False, sean@0: plugins=None, sean@0: debug=None, **kargs): sean@0: """ Start a server instance. This method blocks until the server terminates. sean@0: sean@0: :param app: WSGI application or target string supported by sean@0: :func:`load_app`. (default: :func:`default_app`) sean@0: :param server: Server adapter to use. See :data:`server_names` keys sean@0: for valid names or pass a :class:`ServerAdapter` subclass. sean@0: (default: `wsgiref`) sean@0: :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on sean@0: all interfaces including the external one. (default: 127.0.0.1) sean@0: :param port: Server port to bind to. Values below 1024 require root sean@0: privileges. (default: 8080) sean@0: :param reloader: Start auto-reloading server? (default: False) sean@0: :param interval: Auto-reloader interval in seconds (default: 1) sean@0: :param quiet: Suppress output to stdout and stderr? (default: False) sean@0: :param options: Options passed to the server adapter. sean@0: """ sean@0: if NORUN: return sean@0: if reloader and not os.environ.get('BOTTLE_CHILD'): sean@0: import subprocess sean@0: lockfile = None sean@0: try: sean@0: fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') sean@0: os.close(fd) # We only need this file to exist. We never write to it sean@0: while os.path.exists(lockfile): sean@0: args = [sys.executable] + sys.argv sean@0: environ = os.environ.copy() sean@0: environ['BOTTLE_CHILD'] = 'true' sean@0: environ['BOTTLE_LOCKFILE'] = lockfile sean@0: p = subprocess.Popen(args, env=environ) sean@0: while p.poll() is None: # Busy wait... sean@0: os.utime(lockfile, None) # I am alive! sean@0: time.sleep(interval) sean@0: if p.poll() != 3: sean@0: if os.path.exists(lockfile): os.unlink(lockfile) sean@0: sys.exit(p.poll()) sean@0: except KeyboardInterrupt: sean@0: pass sean@0: finally: sean@0: if os.path.exists(lockfile): sean@0: os.unlink(lockfile) sean@0: return sean@0: sean@0: try: sean@0: if debug is not None: _debug(debug) sean@0: app = app or default_app() sean@0: if isinstance(app, basestring): sean@0: app = load_app(app) sean@0: if not callable(app): sean@0: raise ValueError("Application is not callable: %r" % app) sean@0: sean@0: for plugin in plugins or []: sean@0: if isinstance(plugin, basestring): sean@0: plugin = load(plugin) sean@0: app.install(plugin) sean@0: sean@0: if server in server_names: sean@0: server = server_names.get(server) sean@0: if isinstance(server, basestring): sean@0: server = load(server) sean@0: if isinstance(server, type): sean@0: server = server(host=host, port=port, **kargs) sean@0: if not isinstance(server, ServerAdapter): sean@0: raise ValueError("Unknown or unsupported server: %r" % server) sean@0: sean@0: server.quiet = server.quiet or quiet sean@0: if not server.quiet: sean@0: _stderr("Bottle v%s server starting up (using %s)...\n" % sean@0: (__version__, repr(server))) sean@0: _stderr("Listening on http://%s:%d/\n" % sean@0: (server.host, server.port)) sean@0: _stderr("Hit Ctrl-C to quit.\n\n") sean@0: sean@0: if reloader: sean@0: lockfile = os.environ.get('BOTTLE_LOCKFILE') sean@0: bgcheck = FileCheckerThread(lockfile, interval) sean@0: with bgcheck: sean@0: server.run(app) sean@0: if bgcheck.status == 'reload': sean@0: sys.exit(3) sean@0: else: sean@0: server.run(app) sean@0: except KeyboardInterrupt: sean@0: pass sean@0: except (SystemExit, MemoryError): sean@0: raise sean@0: except: sean@0: if not reloader: raise sean@0: if not getattr(server, 'quiet', quiet): sean@0: print_exc() sean@0: time.sleep(interval) sean@0: sys.exit(3) sean@0: sean@0: sean@0: class FileCheckerThread(threading.Thread): sean@0: """ Interrupt main-thread as soon as a changed module file is detected, sean@0: the lockfile gets deleted or gets to old. """ sean@0: sean@0: def __init__(self, lockfile, interval): sean@0: threading.Thread.__init__(self) sean@0: self.daemon = True sean@0: self.lockfile, self.interval = lockfile, interval sean@0: #: Is one of 'reload', 'error' or 'exit' sean@0: self.status = None sean@0: sean@0: def run(self): sean@0: exists = os.path.exists sean@0: mtime = lambda p: os.stat(p).st_mtime sean@0: files = dict() sean@0: sean@0: for module in list(sys.modules.values()): sean@0: path = getattr(module, '__file__', '') sean@0: if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] sean@0: if path and exists(path): files[path] = mtime(path) sean@0: sean@0: while not self.status: sean@0: if not exists(self.lockfile)\ sean@0: or mtime(self.lockfile) < time.time() - self.interval - 5: sean@0: self.status = 'error' sean@0: thread.interrupt_main() sean@0: for path, lmtime in list(files.items()): sean@0: if not exists(path) or mtime(path) > lmtime: sean@0: self.status = 'reload' sean@0: thread.interrupt_main() sean@0: break sean@0: time.sleep(self.interval) sean@0: sean@0: def __enter__(self): sean@0: self.start() sean@0: sean@0: def __exit__(self, exc_type, *_): sean@0: if not self.status: self.status = 'exit' # silent exit sean@0: self.join() sean@0: return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) sean@0: sean@0: ############################################################################### sean@0: # Template Adapters ############################################################ sean@0: ############################################################################### sean@0: sean@0: sean@0: class TemplateError(HTTPError): sean@0: def __init__(self, message): sean@0: HTTPError.__init__(self, 500, message) sean@0: sean@0: sean@0: class BaseTemplate(object): sean@0: """ Base class and minimal API for template adapters """ sean@0: extensions = ['tpl', 'html', 'thtml', 'stpl'] sean@0: settings = {} #used in prepare() sean@0: defaults = {} #used in render() sean@0: sean@0: def __init__(self, sean@0: source=None, sean@0: name=None, sean@0: lookup=None, sean@0: encoding='utf8', **settings): sean@0: """ Create a new template. sean@0: If the source parameter (str or buffer) is missing, the name argument sean@0: is used to guess a template filename. Subclasses can assume that sean@0: self.source and/or self.filename are set. Both are strings. sean@0: The lookup, encoding and settings parameters are stored as instance sean@0: variables. sean@0: The lookup parameter stores a list containing directory paths. sean@0: The encoding parameter should be used to decode byte strings or files. sean@0: The settings parameter contains a dict for engine-specific settings. sean@0: """ sean@0: self.name = name sean@0: self.source = source.read() if hasattr(source, 'read') else source sean@0: self.filename = source.filename if hasattr(source, 'filename') else None sean@0: self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] sean@0: self.encoding = encoding sean@0: self.settings = self.settings.copy() # Copy from class variable sean@0: self.settings.update(settings) # Apply sean@0: if not self.source and self.name: sean@0: self.filename = self.search(self.name, self.lookup) sean@0: if not self.filename: sean@0: raise TemplateError('Template %s not found.' % repr(name)) sean@0: if not self.source and not self.filename: sean@0: raise TemplateError('No template specified.') sean@0: self.prepare(**self.settings) sean@0: sean@0: @classmethod sean@0: def search(cls, name, lookup=None): sean@0: """ Search name in all directories specified in lookup. sean@0: First without, then with common extensions. Return first hit. """ sean@0: if not lookup: sean@0: depr('The template lookup path list should not be empty.', sean@0: True) #0.12 sean@0: lookup = ['.'] sean@0: sean@0: if os.path.isabs(name) and os.path.isfile(name): sean@0: depr('Absolute template path names are deprecated.', True) #0.12 sean@0: return os.path.abspath(name) sean@0: sean@0: for spath in lookup: sean@0: spath = os.path.abspath(spath) + os.sep sean@0: fname = os.path.abspath(os.path.join(spath, name)) sean@0: if not fname.startswith(spath): continue sean@0: if os.path.isfile(fname): return fname sean@0: for ext in cls.extensions: sean@0: if os.path.isfile('%s.%s' % (fname, ext)): sean@0: return '%s.%s' % (fname, ext) sean@0: sean@0: @classmethod sean@0: def global_config(cls, key, *args): sean@0: """ This reads or sets the global settings stored in class.settings. """ sean@0: if args: sean@0: cls.settings = cls.settings.copy() # Make settings local to class sean@0: cls.settings[key] = args[0] sean@0: else: sean@0: return cls.settings[key] sean@0: sean@0: def prepare(self, **options): sean@0: """ Run preparations (parsing, caching, ...). sean@0: It should be possible to call this again to refresh a template or to sean@0: update settings. sean@0: """ sean@0: raise NotImplementedError sean@0: sean@0: def render(self, *args, **kwargs): sean@0: """ Render the template with the specified local variables and return sean@0: a single byte or unicode string. If it is a byte string, the encoding sean@0: must match self.encoding. This method must be thread-safe! sean@0: Local variables may be provided in dictionaries (args) sean@0: or directly, as keywords (kwargs). sean@0: """ sean@0: raise NotImplementedError sean@0: sean@0: sean@0: class MakoTemplate(BaseTemplate): sean@0: def prepare(self, **options): sean@0: from mako.template import Template sean@0: from mako.lookup import TemplateLookup sean@0: options.update({'input_encoding': self.encoding}) sean@0: options.setdefault('format_exceptions', bool(DEBUG)) sean@0: lookup = TemplateLookup(directories=self.lookup, **options) sean@0: if self.source: sean@0: self.tpl = Template(self.source, lookup=lookup, **options) sean@0: else: sean@0: self.tpl = Template(uri=self.name, sean@0: filename=self.filename, sean@0: lookup=lookup, **options) sean@0: sean@0: def render(self, *args, **kwargs): sean@0: for dictarg in args: sean@0: kwargs.update(dictarg) sean@0: _defaults = self.defaults.copy() sean@0: _defaults.update(kwargs) sean@0: return self.tpl.render(**_defaults) sean@0: sean@0: sean@0: class CheetahTemplate(BaseTemplate): sean@0: def prepare(self, **options): sean@0: from Cheetah.Template import Template sean@0: self.context = threading.local() sean@0: self.context.vars = {} sean@0: options['searchList'] = [self.context.vars] sean@0: if self.source: sean@0: self.tpl = Template(source=self.source, **options) sean@0: else: sean@0: self.tpl = Template(file=self.filename, **options) sean@0: sean@0: def render(self, *args, **kwargs): sean@0: for dictarg in args: sean@0: kwargs.update(dictarg) sean@0: self.context.vars.update(self.defaults) sean@0: self.context.vars.update(kwargs) sean@0: out = str(self.tpl) sean@0: self.context.vars.clear() sean@0: return out sean@0: sean@0: sean@0: class Jinja2Template(BaseTemplate): sean@0: def prepare(self, filters=None, tests=None, globals={}, **kwargs): sean@0: from jinja2 import Environment, FunctionLoader sean@0: self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) sean@0: if filters: self.env.filters.update(filters) sean@0: if tests: self.env.tests.update(tests) sean@0: if globals: self.env.globals.update(globals) sean@0: if self.source: sean@0: self.tpl = self.env.from_string(self.source) sean@0: else: sean@0: self.tpl = self.env.get_template(self.filename) sean@0: sean@0: def render(self, *args, **kwargs): sean@0: for dictarg in args: sean@0: kwargs.update(dictarg) sean@0: _defaults = self.defaults.copy() sean@0: _defaults.update(kwargs) sean@0: return self.tpl.render(**_defaults) sean@0: sean@0: def loader(self, name): sean@0: fname = self.search(name, self.lookup) sean@0: if not fname: return sean@0: with open(fname, "rb") as f: sean@0: return f.read().decode(self.encoding) sean@0: sean@0: sean@0: class SimpleTemplate(BaseTemplate): sean@0: def prepare(self, sean@0: escape_func=html_escape, sean@0: noescape=False, sean@0: syntax=None, **ka): sean@0: self.cache = {} sean@0: enc = self.encoding sean@0: self._str = lambda x: touni(x, enc) sean@0: self._escape = lambda x: escape_func(touni(x, enc)) sean@0: self.syntax = syntax sean@0: if noescape: sean@0: self._str, self._escape = self._escape, self._str sean@0: sean@0: @cached_property sean@0: def co(self): sean@0: return compile(self.code, self.filename or '', 'exec') sean@0: sean@0: @cached_property sean@0: def code(self): sean@0: source = self.source sean@0: if not source: sean@0: with open(self.filename, 'rb') as f: sean@0: source = f.read() sean@0: try: sean@0: source, encoding = touni(source), 'utf8' sean@0: except UnicodeError: sean@0: depr('Template encodings other than utf8 are not supported.') #0.11 sean@0: source, encoding = touni(source, 'latin1'), 'latin1' sean@0: parser = StplParser(source, encoding=encoding, syntax=self.syntax) sean@0: code = parser.translate() sean@0: self.encoding = parser.encoding sean@0: return code sean@0: sean@0: def _rebase(self, _env, _name=None, **kwargs): sean@0: _env['_rebase'] = (_name, kwargs) sean@0: sean@0: def _include(self, _env, _name=None, **kwargs): sean@0: env = _env.copy() sean@0: env.update(kwargs) sean@0: if _name not in self.cache: sean@0: self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) sean@0: return self.cache[_name].execute(env['_stdout'], env) sean@0: sean@0: def execute(self, _stdout, kwargs): sean@0: env = self.defaults.copy() sean@0: env.update(kwargs) sean@0: env.update({ sean@0: '_stdout': _stdout, sean@0: '_printlist': _stdout.extend, sean@0: 'include': functools.partial(self._include, env), sean@0: 'rebase': functools.partial(self._rebase, env), sean@0: '_rebase': None, sean@0: '_str': self._str, sean@0: '_escape': self._escape, sean@0: 'get': env.get, sean@0: 'setdefault': env.setdefault, sean@0: 'defined': env.__contains__ sean@0: }) sean@0: eval(self.co, env) sean@0: if env.get('_rebase'): sean@0: subtpl, rargs = env.pop('_rebase') sean@0: rargs['base'] = ''.join(_stdout) #copy stdout sean@0: del _stdout[:] # clear stdout sean@0: return self._include(env, subtpl, **rargs) sean@0: return env sean@0: sean@0: def render(self, *args, **kwargs): sean@0: """ Render the template using keyword arguments as local variables. """ sean@0: env = {} sean@0: stdout = [] sean@0: for dictarg in args: sean@0: env.update(dictarg) sean@0: env.update(kwargs) sean@0: self.execute(stdout, env) sean@0: return ''.join(stdout) sean@0: sean@0: sean@0: class StplSyntaxError(TemplateError): sean@0: sean@0: pass sean@0: sean@0: sean@0: class StplParser(object): sean@0: """ Parser for stpl templates. """ sean@0: _re_cache = {} #: Cache for compiled re patterns sean@0: sean@0: # This huge pile of voodoo magic splits python code into 8 different tokens. sean@0: # We use the verbose (?x) regex mode to make this more manageable sean@0: sean@0: _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode sean@0: [urbURB]* sean@0: (?: ''(?!') sean@0: |""(?!") sean@0: |'{6} sean@0: |"{6} sean@0: |'(?:[^\\']|\\.)+?' sean@0: |"(?:[^\\"]|\\.)+?" sean@0: |'{3}(?:[^\\]|\\.|\n)+?'{3} sean@0: |"{3}(?:[^\\]|\\.|\n)+?"{3} sean@0: ) sean@0: )''' sean@0: sean@0: _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later sean@0: sean@0: _re_tok += r''' sean@0: # 2: Comments (until end of line, but not the newline itself) sean@0: |(\#.*) sean@0: sean@0: # 3: Open and close (4) grouping tokens sean@0: |([\[\{\(]) sean@0: |([\]\}\)]) sean@0: sean@0: # 5,6: Keywords that start or continue a python block (only start of line) sean@0: |^([\ \t]*(?:if|for|while|with|try|def|class)\b) sean@0: |^([\ \t]*(?:elif|else|except|finally)\b) sean@0: sean@0: # 7: Our special 'end' keyword (but only if it stands alone) sean@0: |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) sean@0: sean@0: # 8: A customizable end-of-code-block template token (only end of line) sean@0: |(%(block_close)s[\ \t]*(?=\r?$)) sean@0: sean@0: # 9: And finally, a single newline. The 10th token is 'everything else' sean@0: |(\r?\n) sean@0: ''' sean@0: sean@0: # Match the start tokens of code areas in a template sean@0: _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' sean@0: # Match inline statements (may contain python strings) sean@0: _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl sean@0: sean@0: default_syntax = '<% %> % {{ }}' sean@0: sean@0: def __init__(self, source, syntax=None, encoding='utf8'): sean@0: self.source, self.encoding = touni(source, encoding), encoding sean@0: self.set_syntax(syntax or self.default_syntax) sean@0: self.code_buffer, self.text_buffer = [], [] sean@0: self.lineno, self.offset = 1, 0 sean@0: self.indent, self.indent_mod = 0, 0 sean@0: self.paren_depth = 0 sean@0: sean@0: def get_syntax(self): sean@0: """ Tokens as a space separated string (default: <% %> % {{ }}) """ sean@0: return self._syntax sean@0: sean@0: def set_syntax(self, syntax): sean@0: self._syntax = syntax sean@0: self._tokens = syntax.split() sean@0: if not syntax in self._re_cache: sean@0: names = 'block_start block_close line_start inline_start inline_end' sean@0: etokens = map(re.escape, self._tokens) sean@0: pattern_vars = dict(zip(names.split(), etokens)) sean@0: patterns = (self._re_split, self._re_tok, self._re_inl) sean@0: patterns = [re.compile(p % pattern_vars) for p in patterns] sean@0: self._re_cache[syntax] = patterns sean@0: self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] sean@0: sean@0: syntax = property(get_syntax, set_syntax) sean@0: sean@0: def translate(self): sean@0: if self.offset: raise RuntimeError('Parser is a one time instance.') sean@0: while True: sean@0: m = self.re_split.search(self.source, pos=self.offset) sean@0: if m: sean@0: text = self.source[self.offset:m.start()] sean@0: self.text_buffer.append(text) sean@0: self.offset = m.end() sean@0: if m.group(1): # Escape syntax sean@0: line, sep, _ = self.source[self.offset:].partition('\n') sean@0: self.text_buffer.append(self.source[m.start():m.start(1)] + sean@0: m.group(2) + line + sep) sean@0: self.offset += len(line + sep) sean@0: continue sean@0: self.flush_text() sean@0: self.offset += self.read_code(self.source[self.offset:], sean@0: multiline=bool(m.group(4))) sean@0: else: sean@0: break sean@0: self.text_buffer.append(self.source[self.offset:]) sean@0: self.flush_text() sean@0: return ''.join(self.code_buffer) sean@0: sean@0: def read_code(self, pysource, multiline): sean@0: code_line, comment = '', '' sean@0: offset = 0 sean@0: while True: sean@0: m = self.re_tok.search(pysource, pos=offset) sean@0: if not m: sean@0: code_line += pysource[offset:] sean@0: offset = len(pysource) sean@0: self.write_code(code_line.strip(), comment) sean@0: break sean@0: code_line += pysource[offset:m.start()] sean@0: offset = m.end() sean@0: _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() sean@0: if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c sean@0: code_line += _blk1 or _blk2 sean@0: continue sean@0: if _str: # Python string sean@0: code_line += _str sean@0: elif _com: # Python comment (up to EOL) sean@0: comment = _com sean@0: if multiline and _com.strip().endswith(self._tokens[1]): sean@0: multiline = False # Allow end-of-block in comments sean@0: elif _po: # open parenthesis sean@0: self.paren_depth += 1 sean@0: code_line += _po sean@0: elif _pc: # close parenthesis sean@0: if self.paren_depth > 0: sean@0: # we could check for matching parentheses here, but it's sean@0: # easier to leave that to python - just check counts sean@0: self.paren_depth -= 1 sean@0: code_line += _pc sean@0: elif _blk1: # Start-block keyword (if/for/while/def/try/...) sean@0: code_line, self.indent_mod = _blk1, -1 sean@0: self.indent += 1 sean@0: elif _blk2: # Continue-block keyword (else/elif/except/...) sean@0: code_line, self.indent_mod = _blk2, -1 sean@0: elif _end: # The non-standard 'end'-keyword (ends a block) sean@0: self.indent -= 1 sean@0: elif _cend: # The end-code-block template token (usually '%>') sean@0: if multiline: multiline = False sean@0: else: code_line += _cend sean@0: else: # \n sean@0: self.write_code(code_line.strip(), comment) sean@0: self.lineno += 1 sean@0: code_line, comment, self.indent_mod = '', '', 0 sean@0: if not multiline: sean@0: break sean@0: sean@0: return offset sean@0: sean@0: def flush_text(self): sean@0: text = ''.join(self.text_buffer) sean@0: del self.text_buffer[:] sean@0: if not text: return sean@0: parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent sean@0: for m in self.re_inl.finditer(text): sean@0: prefix, pos = text[pos:m.start()], m.end() sean@0: if prefix: sean@0: parts.append(nl.join(map(repr, prefix.splitlines(True)))) sean@0: if prefix.endswith('\n'): parts[-1] += nl sean@0: parts.append(self.process_inline(m.group(1).strip())) sean@0: if pos < len(text): sean@0: prefix = text[pos:] sean@0: lines = prefix.splitlines(True) sean@0: if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] sean@0: elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] sean@0: parts.append(nl.join(map(repr, lines))) sean@0: code = '_printlist((%s,))' % ', '.join(parts) sean@0: self.lineno += code.count('\n') + 1 sean@0: self.write_code(code) sean@0: sean@0: @staticmethod sean@0: def process_inline(chunk): sean@0: if chunk[0] == '!': return '_str(%s)' % chunk[1:] sean@0: return '_escape(%s)' % chunk sean@0: sean@0: def write_code(self, line, comment=''): sean@0: code = ' ' * (self.indent + self.indent_mod) sean@0: code += line.lstrip() + comment + '\n' sean@0: self.code_buffer.append(code) sean@0: sean@0: sean@0: def template(*args, **kwargs): sean@0: """ sean@0: Get a rendered template as a string iterator. sean@0: You can use a name, a filename or a template string as first parameter. sean@0: Template rendering arguments can be passed as dictionaries sean@0: or directly (as keyword arguments). sean@0: """ sean@0: tpl = args[0] if args else None sean@0: adapter = kwargs.pop('template_adapter', SimpleTemplate) sean@0: lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) sean@0: tplid = (id(lookup), tpl) sean@0: if tplid not in TEMPLATES or DEBUG: sean@0: settings = kwargs.pop('template_settings', {}) sean@0: if isinstance(tpl, adapter): sean@0: TEMPLATES[tplid] = tpl sean@0: if settings: TEMPLATES[tplid].prepare(**settings) sean@0: elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: sean@0: TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) sean@0: else: sean@0: TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) sean@0: if not TEMPLATES[tplid]: sean@0: abort(500, 'Template (%s) not found' % tpl) sean@0: for dictarg in args[1:]: sean@0: kwargs.update(dictarg) sean@0: return TEMPLATES[tplid].render(kwargs) sean@0: sean@0: sean@0: mako_template = functools.partial(template, template_adapter=MakoTemplate) sean@0: cheetah_template = functools.partial(template, sean@0: template_adapter=CheetahTemplate) sean@0: jinja2_template = functools.partial(template, template_adapter=Jinja2Template) sean@0: sean@0: sean@0: def view(tpl_name, **defaults): sean@0: """ Decorator: renders a template for a handler. sean@0: The handler can control its behavior like that: sean@0: sean@0: - return a dict of template vars to fill out the template sean@0: - return something other than a dict and the view decorator will not sean@0: process the template, but return the handler result as is. sean@0: This includes returning a HTTPResponse(dict) to get, sean@0: for instance, JSON with autojson or other castfilters. sean@0: """ sean@0: sean@0: def decorator(func): sean@0: sean@0: @functools.wraps(func) sean@0: def wrapper(*args, **kwargs): sean@0: result = func(*args, **kwargs) sean@0: if isinstance(result, (dict, DictMixin)): sean@0: tplvars = defaults.copy() sean@0: tplvars.update(result) sean@0: return template(tpl_name, **tplvars) sean@0: elif result is None: sean@0: return template(tpl_name, defaults) sean@0: return result sean@0: sean@0: return wrapper sean@0: sean@0: return decorator sean@0: sean@0: sean@0: mako_view = functools.partial(view, template_adapter=MakoTemplate) sean@0: cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) sean@0: jinja2_view = functools.partial(view, template_adapter=Jinja2Template) sean@0: sean@0: ############################################################################### sean@0: # Constants and Globals ######################################################## sean@0: ############################################################################### sean@0: sean@0: TEMPLATE_PATH = ['./', './views/'] sean@0: TEMPLATES = {} sean@0: DEBUG = False sean@0: NORUN = False # If set, run() does nothing. Used by load_app() sean@0: sean@0: #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') sean@0: HTTP_CODES = httplib.responses.copy() sean@0: HTTP_CODES[418] = "I'm a teapot" # RFC 2324 sean@0: HTTP_CODES[428] = "Precondition Required" sean@0: HTTP_CODES[429] = "Too Many Requests" sean@0: HTTP_CODES[431] = "Request Header Fields Too Large" sean@0: HTTP_CODES[511] = "Network Authentication Required" sean@0: _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) sean@0: for (k, v) in HTTP_CODES.items()) sean@0: sean@0: #: The default template used for error pages. Override with @error() sean@0: ERROR_PAGE_TEMPLATE = """ sean@0: %%try: sean@0: %%from %s import DEBUG, request sean@0: sean@0: sean@0: sean@0: Error: {{e.status}} sean@0: sean@0: sean@0: sean@0:

Error: {{e.status}}

sean@0:

Sorry, the requested URL {{repr(request.url)}} sean@0: caused an error:

sean@0:
{{e.body}}
sean@0: %%if DEBUG and e.exception: sean@0:

Exception:

sean@0:
{{repr(e.exception)}}
sean@0: %%end sean@0: %%if DEBUG and e.traceback: sean@0:

Traceback:

sean@0:
{{e.traceback}}
sean@0: %%end sean@0: sean@0: sean@0: %%except ImportError: sean@0: ImportError: Could not generate the error page. Please add bottle to sean@0: the import path. sean@0: %%end sean@0: """ % __name__ sean@0: sean@0: #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a sean@0: #: request callback, this instance always refers to the *current* request sean@0: #: (even on a multithreaded server). sean@0: request = LocalRequest() sean@0: sean@0: #: A thread-safe instance of :class:`LocalResponse`. It is used to change the sean@0: #: HTTP response for the *current* request. sean@0: response = LocalResponse() sean@0: sean@0: #: A thread-safe namespace. Not used by Bottle. sean@0: local = threading.local() sean@0: sean@0: # Initialize app stack (create first empty Bottle app) sean@0: # BC: 0.6.4 and needed for run() sean@0: app = default_app = AppStack() sean@0: app.push() sean@0: sean@0: #: A virtual package that redirects import statements. sean@0: #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. sean@0: ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else sean@0: __name__ + ".ext", 'bottle_%s').module sean@0: sean@0: if __name__ == '__main__': sean@0: opt, args, parser = _cmd_options, _cmd_args, _cmd_parser sean@0: if opt.version: sean@0: _stdout('Bottle %s\n' % __version__) sean@0: sys.exit(0) sean@0: if not args: sean@0: parser.print_help() sean@0: _stderr('\nError: No application entry point specified.\n') sean@0: sys.exit(1) sean@0: sean@0: sys.path.insert(0, '.') sean@0: sys.modules.setdefault('bottle', sys.modules['__main__']) sean@0: sean@0: host, port = (opt.bind or 'localhost'), 8080 sean@0: if ':' in host and host.rfind(']') < host.rfind(':'): sean@0: host, port = host.rsplit(':', 1) sean@0: host = host.strip('[]') sean@0: sean@0: run(args[0], sean@0: host=host, sean@0: port=int(port), sean@0: server=opt.server, sean@0: reloader=opt.reload, sean@0: plugins=opt.plugin, sean@0: debug=opt.debug) sean@0: sean@0: # THE END