Mercurial > templog
comparison web/bottle.py @ 27:dbbd503119ba
Add some web server handling
author | Matt Johnston <matt@ucc.asn.au> |
---|---|
date | Tue, 12 Jun 2012 00:09:09 +0800 |
parents | |
children | 317a1738f15c |
comparison
equal
deleted
inserted
replaced
26:d3e5934fe55c | 27:dbbd503119ba |
---|---|
1 #!/usr/bin/env python | |
2 # -*- coding: utf-8 -*- | |
3 """ | |
4 Bottle is a fast and simple micro-framework for small web applications. It | |
5 offers request dispatching (Routes) with url parameter support, templates, | |
6 a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and | |
7 template engines - all in a single file and with no dependencies other than the | |
8 Python Standard Library. | |
9 | |
10 Homepage and documentation: http://bottlepy.org/ | |
11 | |
12 Copyright (c) 2011, Marcel Hellkamp. | |
13 License: MIT (see LICENSE for details) | |
14 """ | |
15 | |
16 from __future__ import with_statement | |
17 | |
18 __author__ = 'Marcel Hellkamp' | |
19 __version__ = '0.11.dev' | |
20 __license__ = 'MIT' | |
21 | |
22 # The gevent server adapter needs to patch some modules before they are imported | |
23 # This is why we parse the commandline parameters here but handle them later | |
24 if __name__ == '__main__': | |
25 from optparse import OptionParser | |
26 _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app") | |
27 _opt = _cmd_parser.add_option | |
28 _opt("--version", action="store_true", help="show version number.") | |
29 _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") | |
30 _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") | |
31 _opt("-p", "--plugin", action="append", help="install additional plugin/s.") | |
32 _opt("--debug", action="store_true", help="start server in debug mode.") | |
33 _opt("--reload", action="store_true", help="auto-reload on file changes.") | |
34 _cmd_options, _cmd_args = _cmd_parser.parse_args() | |
35 if _cmd_options.server and _cmd_options.server.startswith('gevent'): | |
36 import gevent.monkey; gevent.monkey.patch_all() | |
37 | |
38 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ | |
39 os, re, subprocess, sys, tempfile, threading, time, urllib, warnings | |
40 | |
41 from datetime import date as datedate, datetime, timedelta | |
42 from tempfile import TemporaryFile | |
43 from traceback import format_exc, print_exc | |
44 | |
45 try: from json import dumps as json_dumps, loads as json_lds | |
46 except ImportError: # pragma: no cover | |
47 try: from simplejson import dumps as json_dumps, loads as json_lds | |
48 except ImportError: | |
49 try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds | |
50 except ImportError: | |
51 def json_dumps(data): | |
52 raise ImportError("JSON support requires Python 2.6 or simplejson.") | |
53 json_lds = json_dumps | |
54 | |
55 | |
56 | |
57 # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. | |
58 # It ain't pretty but it works... Sorry for the mess. | |
59 | |
60 py = sys.version_info | |
61 py3k = py >= (3,0,0) | |
62 py25 = py < (2,6,0) | |
63 | |
64 # Workaround for the missing "as" keyword in py3k. | |
65 def _e(): return sys.exc_info()[1] | |
66 | |
67 # Workaround for the "print is a keyword/function" Python 2/3 dilemma | |
68 # and a fallback for mod_wsgi (resticts stdout/err attribute access) | |
69 try: | |
70 _stdout, _stderr = sys.stdout.write, sys.stderr.write | |
71 except IOError: | |
72 _stdout = lambda x: sys.stdout.write(x) | |
73 _stderr = lambda x: sys.stderr.write(x) | |
74 | |
75 # Lots of stdlib and builtin differences. | |
76 if py3k: | |
77 import http.client as httplib | |
78 import _thread as thread | |
79 from urllib.parse import urljoin, parse_qsl, SplitResult as UrlSplitResult | |
80 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote | |
81 from http.cookies import SimpleCookie | |
82 from collections import MutableMapping as DictMixin | |
83 import pickle | |
84 from io import BytesIO | |
85 basestring = str | |
86 unicode = str | |
87 json_loads = lambda s: json_lds(touni(s)) | |
88 callable = lambda x: hasattr(x, '__call__') | |
89 imap = map | |
90 else: # 2.x | |
91 import httplib | |
92 import thread | |
93 from urlparse import urljoin, SplitResult as UrlSplitResult | |
94 from urllib import urlencode, quote as urlquote, unquote as urlunquote | |
95 from Cookie import SimpleCookie | |
96 from itertools import imap | |
97 import cPickle as pickle | |
98 from StringIO import StringIO as BytesIO | |
99 if py25: | |
100 msg = "Python 2.5 support may be dropped in future versions of Bottle." | |
101 warnings.warn(msg, DeprecationWarning) | |
102 from cgi import parse_qsl | |
103 from UserDict import DictMixin | |
104 def next(it): return it.next() | |
105 bytes = str | |
106 else: # 2.6, 2.7 | |
107 from urlparse import parse_qsl | |
108 from collections import MutableMapping as DictMixin | |
109 json_loads = json_lds | |
110 | |
111 # Some helpers for string/byte handling | |
112 def tob(s, enc='utf8'): | |
113 return s.encode(enc) if isinstance(s, unicode) else bytes(s) | |
114 def touni(s, enc='utf8', err='strict'): | |
115 return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) | |
116 tonat = touni if py3k else tob | |
117 | |
118 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). | |
119 # 3.1 needs a workaround. | |
120 NCTextIOWrapper = None | |
121 if (3,0,0) < py < (3,2,0): | |
122 from io import TextIOWrapper | |
123 class NCTextIOWrapper(TextIOWrapper): | |
124 def close(self): pass # Keep wrapped buffer open. | |
125 | |
126 # A bug in functools causes it to break if the wrapper is an instance method | |
127 def update_wrapper(wrapper, wrapped, *a, **ka): | |
128 try: functools.update_wrapper(wrapper, wrapped, *a, **ka) | |
129 except AttributeError: pass | |
130 | |
131 | |
132 | |
133 # These helpers are used at module level and need to be defined first. | |
134 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. | |
135 | |
136 def depr(message): | |
137 warnings.warn(message, DeprecationWarning, stacklevel=3) | |
138 | |
139 def makelist(data): # This is just to handy | |
140 if isinstance(data, (tuple, list, set, dict)): return list(data) | |
141 elif data: return [data] | |
142 else: return [] | |
143 | |
144 | |
145 class DictProperty(object): | |
146 ''' Property that maps to a key in a local dict-like attribute. ''' | |
147 def __init__(self, attr, key=None, read_only=False): | |
148 self.attr, self.key, self.read_only = attr, key, read_only | |
149 | |
150 def __call__(self, func): | |
151 functools.update_wrapper(self, func, updated=[]) | |
152 self.getter, self.key = func, self.key or func.__name__ | |
153 return self | |
154 | |
155 def __get__(self, obj, cls): | |
156 if obj is None: return self | |
157 key, storage = self.key, getattr(obj, self.attr) | |
158 if key not in storage: storage[key] = self.getter(obj) | |
159 return storage[key] | |
160 | |
161 def __set__(self, obj, value): | |
162 if self.read_only: raise AttributeError("Read-Only property.") | |
163 getattr(obj, self.attr)[self.key] = value | |
164 | |
165 def __delete__(self, obj): | |
166 if self.read_only: raise AttributeError("Read-Only property.") | |
167 del getattr(obj, self.attr)[self.key] | |
168 | |
169 | |
170 class cached_property(object): | |
171 ''' A property that is only computed once per instance and then replaces | |
172 itself with an ordinary attribute. Deleting the attribute resets the | |
173 property. ''' | |
174 | |
175 def __init__(self, func): | |
176 self.func = func | |
177 | |
178 def __get__(self, obj, cls): | |
179 if obj is None: return self | |
180 value = obj.__dict__[self.func.__name__] = self.func(obj) | |
181 return value | |
182 | |
183 | |
184 class lazy_attribute(object): | |
185 ''' A property that caches itself to the class object. ''' | |
186 def __init__(self, func): | |
187 functools.update_wrapper(self, func, updated=[]) | |
188 self.getter = func | |
189 | |
190 def __get__(self, obj, cls): | |
191 value = self.getter(cls) | |
192 setattr(cls, self.__name__, value) | |
193 return value | |
194 | |
195 | |
196 | |
197 | |
198 | |
199 | |
200 ############################################################################### | |
201 # Exceptions and Events ######################################################## | |
202 ############################################################################### | |
203 | |
204 | |
205 class BottleException(Exception): | |
206 """ A base class for exceptions used by bottle. """ | |
207 pass | |
208 | |
209 | |
210 #TODO: This should subclass BaseRequest | |
211 class HTTPResponse(BottleException): | |
212 """ Used to break execution and immediately finish the response """ | |
213 def __init__(self, output='', status=200, header=None): | |
214 super(BottleException, self).__init__("HTTP Response %d" % status) | |
215 self.status = int(status) | |
216 self.output = output | |
217 self.headers = HeaderDict(header) if header else None | |
218 | |
219 def apply(self, response): | |
220 if self.headers: | |
221 for key, value in self.headers.allitems(): | |
222 response.headers[key] = value | |
223 response.status = self.status | |
224 | |
225 | |
226 class HTTPError(HTTPResponse): | |
227 """ Used to generate an error page """ | |
228 def __init__(self, code=500, output='Unknown Error', exception=None, | |
229 traceback=None, header=None): | |
230 super(HTTPError, self).__init__(output, code, header) | |
231 self.exception = exception | |
232 self.traceback = traceback | |
233 | |
234 def __repr__(self): | |
235 return tonat(template(ERROR_PAGE_TEMPLATE, e=self)) | |
236 | |
237 | |
238 | |
239 | |
240 | |
241 | |
242 ############################################################################### | |
243 # Routing ###################################################################### | |
244 ############################################################################### | |
245 | |
246 | |
247 class RouteError(BottleException): | |
248 """ This is a base class for all routing related exceptions """ | |
249 | |
250 | |
251 class RouteReset(BottleException): | |
252 """ If raised by a plugin or request handler, the route is reset and all | |
253 plugins are re-applied. """ | |
254 | |
255 class RouterUnknownModeError(RouteError): pass | |
256 | |
257 | |
258 class RouteSyntaxError(RouteError): | |
259 """ The route parser found something not supported by this router """ | |
260 | |
261 | |
262 class RouteBuildError(RouteError): | |
263 """ The route could not been built """ | |
264 | |
265 | |
266 class Router(object): | |
267 ''' A Router is an ordered collection of route->target pairs. It is used to | |
268 efficiently match WSGI requests against a number of routes and return | |
269 the first target that satisfies the request. The target may be anything, | |
270 usually a string, ID or callable object. A route consists of a path-rule | |
271 and a HTTP method. | |
272 | |
273 The path-rule is either a static path (e.g. `/contact`) or a dynamic | |
274 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax | |
275 and details on the matching order are described in docs:`routing`. | |
276 ''' | |
277 | |
278 default_pattern = '[^/]+' | |
279 default_filter = 're' | |
280 #: Sorry for the mess. It works. Trust me. | |
281 rule_syntax = re.compile('(\\\\*)'\ | |
282 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ | |
283 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ | |
284 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') | |
285 | |
286 def __init__(self, strict=False): | |
287 self.rules = {} # A {rule: Rule} mapping | |
288 self.builder = {} # A rule/name->build_info mapping | |
289 self.static = {} # Cache for static routes: {path: {method: target}} | |
290 self.dynamic = [] # Cache for dynamic routes. See _compile() | |
291 #: If true, static routes are no longer checked first. | |
292 self.strict_order = strict | |
293 self.filters = {'re': self.re_filter, 'int': self.int_filter, | |
294 'float': self.float_filter, 'path': self.path_filter} | |
295 | |
296 def re_filter(self, conf): | |
297 return conf or self.default_pattern, None, None | |
298 | |
299 def int_filter(self, conf): | |
300 return r'-?\d+', int, lambda x: str(int(x)) | |
301 | |
302 def float_filter(self, conf): | |
303 return r'-?[\d.]+', float, lambda x: str(float(x)) | |
304 | |
305 def path_filter(self, conf): | |
306 return r'.+?', None, None | |
307 | |
308 def add_filter(self, name, func): | |
309 ''' Add a filter. The provided function is called with the configuration | |
310 string as parameter and must return a (regexp, to_python, to_url) tuple. | |
311 The first element is a string, the last two are callables or None. ''' | |
312 self.filters[name] = func | |
313 | |
314 def parse_rule(self, rule): | |
315 ''' Parses a rule into a (name, filter, conf) token stream. If mode is | |
316 None, name contains a static rule part. ''' | |
317 offset, prefix = 0, '' | |
318 for match in self.rule_syntax.finditer(rule): | |
319 prefix += rule[offset:match.start()] | |
320 g = match.groups() | |
321 if len(g[0])%2: # Escaped wildcard | |
322 prefix += match.group(0)[len(g[0]):] | |
323 offset = match.end() | |
324 continue | |
325 if prefix: yield prefix, None, None | |
326 name, filtr, conf = g[1:4] if not g[2] is None else g[4:7] | |
327 if not filtr: filtr = self.default_filter | |
328 yield name, filtr, conf or None | |
329 offset, prefix = match.end(), '' | |
330 if offset <= len(rule) or prefix: | |
331 yield prefix+rule[offset:], None, None | |
332 | |
333 def add(self, rule, method, target, name=None): | |
334 ''' Add a new route or replace the target for an existing route. ''' | |
335 if rule in self.rules: | |
336 self.rules[rule][method] = target | |
337 if name: self.builder[name] = self.builder[rule] | |
338 return | |
339 | |
340 target = self.rules[rule] = {method: target} | |
341 | |
342 # Build pattern and other structures for dynamic routes | |
343 anons = 0 # Number of anonymous wildcards | |
344 pattern = '' # Regular expression pattern | |
345 filters = [] # Lists of wildcard input filters | |
346 builder = [] # Data structure for the URL builder | |
347 is_static = True | |
348 for key, mode, conf in self.parse_rule(rule): | |
349 if mode: | |
350 is_static = False | |
351 mask, in_filter, out_filter = self.filters[mode](conf) | |
352 if key: | |
353 pattern += '(?P<%s>%s)' % (key, mask) | |
354 else: | |
355 pattern += '(?:%s)' % mask | |
356 key = 'anon%d' % anons; anons += 1 | |
357 if in_filter: filters.append((key, in_filter)) | |
358 builder.append((key, out_filter or str)) | |
359 elif key: | |
360 pattern += re.escape(key) | |
361 builder.append((None, key)) | |
362 self.builder[rule] = builder | |
363 if name: self.builder[name] = builder | |
364 | |
365 if is_static and not self.strict_order: | |
366 self.static[self.build(rule)] = target | |
367 return | |
368 | |
369 def fpat_sub(m): | |
370 return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' | |
371 flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern) | |
372 | |
373 try: | |
374 re_match = re.compile('^(%s)$' % pattern).match | |
375 except re.error: | |
376 raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) | |
377 | |
378 def match(path): | |
379 """ Return an url-argument dictionary. """ | |
380 url_args = re_match(path).groupdict() | |
381 for name, wildcard_filter in filters: | |
382 try: | |
383 url_args[name] = wildcard_filter(url_args[name]) | |
384 except ValueError: | |
385 raise HTTPError(400, 'Path has wrong format.') | |
386 return url_args | |
387 | |
388 try: | |
389 combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern) | |
390 self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) | |
391 self.dynamic[-1][1].append((match, target)) | |
392 except (AssertionError, IndexError): # AssertionError: Too many groups | |
393 self.dynamic.append((re.compile('(^%s$)' % flat_pattern), | |
394 [(match, target)])) | |
395 return match | |
396 | |
397 def build(self, _name, *anons, **query): | |
398 ''' Build an URL by filling the wildcards in a rule. ''' | |
399 builder = self.builder.get(_name) | |
400 if not builder: raise RouteBuildError("No route with that name.", _name) | |
401 try: | |
402 for i, value in enumerate(anons): query['anon%d'%i] = value | |
403 url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder]) | |
404 return url if not query else url+'?'+urlencode(query) | |
405 except KeyError: | |
406 raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) | |
407 | |
408 def match(self, environ): | |
409 ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' | |
410 path, targets, urlargs = environ['PATH_INFO'] or '/', None, {} | |
411 if path in self.static: | |
412 targets = self.static[path] | |
413 else: | |
414 for combined, rules in self.dynamic: | |
415 match = combined.match(path) | |
416 if not match: continue | |
417 getargs, targets = rules[match.lastindex - 1] | |
418 urlargs = getargs(path) if getargs else {} | |
419 break | |
420 | |
421 if not targets: | |
422 raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO'])) | |
423 method = environ['REQUEST_METHOD'].upper() | |
424 if method in targets: | |
425 return targets[method], urlargs | |
426 if method == 'HEAD' and 'GET' in targets: | |
427 return targets['GET'], urlargs | |
428 if 'ANY' in targets: | |
429 return targets['ANY'], urlargs | |
430 allowed = [verb for verb in targets if verb != 'ANY'] | |
431 if 'GET' in allowed and 'HEAD' not in allowed: | |
432 allowed.append('HEAD') | |
433 raise HTTPError(405, "Method not allowed.", | |
434 header=[('Allow',",".join(allowed))]) | |
435 | |
436 | |
437 class Route(object): | |
438 ''' This class wraps a route callback along with route specific metadata and | |
439 configuration and applies Plugins on demand. It is also responsible for | |
440 turing an URL path rule into a regular expression usable by the Router. | |
441 ''' | |
442 | |
443 def __init__(self, app, rule, method, callback, name=None, | |
444 plugins=None, skiplist=None, **config): | |
445 #: The application this route is installed to. | |
446 self.app = app | |
447 #: The path-rule string (e.g. ``/wiki/:page``). | |
448 self.rule = rule | |
449 #: The HTTP method as a string (e.g. ``GET``). | |
450 self.method = method | |
451 #: The original callback with no plugins applied. Useful for introspection. | |
452 self.callback = callback | |
453 #: The name of the route (if specified) or ``None``. | |
454 self.name = name or None | |
455 #: A list of route-specific plugins (see :meth:`Bottle.route`). | |
456 self.plugins = plugins or [] | |
457 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). | |
458 self.skiplist = skiplist or [] | |
459 #: Additional keyword arguments passed to the :meth:`Bottle.route` | |
460 #: decorator are stored in this dictionary. Used for route-specific | |
461 #: plugin configuration and meta-data. | |
462 self.config = ConfigDict(config) | |
463 | |
464 def __call__(self, *a, **ka): | |
465 depr("Some APIs changed to return Route() instances instead of"\ | |
466 " callables. Make sure to use the Route.call method and not to"\ | |
467 " call Route instances directly.") | |
468 return self.call(*a, **ka) | |
469 | |
470 @cached_property | |
471 def call(self): | |
472 ''' The route callback with all plugins applied. This property is | |
473 created on demand and then cached to speed up subsequent requests.''' | |
474 return self._make_callback() | |
475 | |
476 def reset(self): | |
477 ''' Forget any cached values. The next time :attr:`call` is accessed, | |
478 all plugins are re-applied. ''' | |
479 self.__dict__.pop('call', None) | |
480 | |
481 def prepare(self): | |
482 ''' Do all on-demand work immediately (useful for debugging).''' | |
483 self.call | |
484 | |
485 @property | |
486 def _context(self): | |
487 depr('Switch to Plugin API v2 and access the Route object directly.') | |
488 return dict(rule=self.rule, method=self.method, callback=self.callback, | |
489 name=self.name, app=self.app, config=self.config, | |
490 apply=self.plugins, skip=self.skiplist) | |
491 | |
492 def all_plugins(self): | |
493 ''' Yield all Plugins affecting this route. ''' | |
494 unique = set() | |
495 for p in reversed(self.app.plugins + self.plugins): | |
496 if True in self.skiplist: break | |
497 name = getattr(p, 'name', False) | |
498 if name and (name in self.skiplist or name in unique): continue | |
499 if p in self.skiplist or type(p) in self.skiplist: continue | |
500 if name: unique.add(name) | |
501 yield p | |
502 | |
503 def _make_callback(self): | |
504 callback = self.callback | |
505 for plugin in self.all_plugins(): | |
506 try: | |
507 if hasattr(plugin, 'apply'): | |
508 api = getattr(plugin, 'api', 1) | |
509 context = self if api > 1 else self._context | |
510 callback = plugin.apply(callback, context) | |
511 else: | |
512 callback = plugin(callback) | |
513 except RouteReset: # Try again with changed configuration. | |
514 return self._make_callback() | |
515 if not callback is self.callback: | |
516 update_wrapper(callback, self.callback) | |
517 return callback | |
518 | |
519 def __repr__(self): | |
520 return '<%s %r %r>' % (self.method, self.rule, self.callback) | |
521 | |
522 | |
523 | |
524 | |
525 | |
526 | |
527 ############################################################################### | |
528 # Application Object ########################################################### | |
529 ############################################################################### | |
530 | |
531 | |
532 class Bottle(object): | |
533 """ Each Bottle object represents a single, distinct web application and | |
534 consists of routes, callbacks, plugins, resources and configuration. | |
535 Instances are callable WSGI applications. | |
536 | |
537 :param catchall: If true (default), handle all exceptions. Turn off to | |
538 let debugging middleware handle exceptions. | |
539 """ | |
540 | |
541 def __init__(self, catchall=True, autojson=True): | |
542 #: If true, most exceptions are caught and returned as :exc:`HTTPError` | |
543 self.catchall = catchall | |
544 | |
545 #: A :cls:`ResourceManager` for application files | |
546 self.resources = ResourceManager() | |
547 | |
548 #: A :cls:`ConfigDict` for app specific configuration. | |
549 self.config = ConfigDict() | |
550 self.config.autojson = autojson | |
551 | |
552 self.routes = [] # List of installed :class:`Route` instances. | |
553 self.router = Router() # Maps requests to :class:`Route` instances. | |
554 self.error_handler = {} | |
555 | |
556 # Core plugins | |
557 self.plugins = [] # List of installed plugins. | |
558 self.hooks = HooksPlugin() | |
559 self.install(self.hooks) | |
560 if self.config.autojson: | |
561 self.install(JSONPlugin()) | |
562 self.install(TemplatePlugin()) | |
563 | |
564 | |
565 def mount(self, prefix, app, **options): | |
566 ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific | |
567 URL prefix. Example:: | |
568 | |
569 root_app.mount('/admin/', admin_app) | |
570 | |
571 :param prefix: path prefix or `mount-point`. If it ends in a slash, | |
572 that slash is mandatory. | |
573 :param app: an instance of :class:`Bottle` or a WSGI application. | |
574 | |
575 All other parameters are passed to the underlying :meth:`route` call. | |
576 ''' | |
577 if isinstance(app, basestring): | |
578 prefix, app = app, prefix | |
579 depr('Parameter order of Bottle.mount() changed.') # 0.10 | |
580 | |
581 segments = [p for p in prefix.split('/') if p] | |
582 if not segments: raise ValueError('Empty path prefix.') | |
583 path_depth = len(segments) | |
584 | |
585 def mountpoint_wrapper(): | |
586 try: | |
587 request.path_shift(path_depth) | |
588 rs = BaseResponse([], 200) | |
589 def start_response(status, header): | |
590 rs.status = status | |
591 for name, value in header: rs.add_header(name, value) | |
592 return rs.body.append | |
593 body = app(request.environ, start_response) | |
594 body = itertools.chain(rs.body, body) | |
595 return HTTPResponse(body, rs.status_code, rs.headers) | |
596 finally: | |
597 request.path_shift(-path_depth) | |
598 | |
599 options.setdefault('skip', True) | |
600 options.setdefault('method', 'ANY') | |
601 options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) | |
602 options['callback'] = mountpoint_wrapper | |
603 | |
604 self.route('/%s/<:re:.*>' % '/'.join(segments), **options) | |
605 if not prefix.endswith('/'): | |
606 self.route('/' + '/'.join(segments), **options) | |
607 | |
608 def merge(self, routes): | |
609 ''' Merge the routes of another :class:`Bottle` application or a list of | |
610 :class:`Route` objects into this application. The routes keep their | |
611 'owner', meaning that the :data:`Route.app` attribute is not | |
612 changed. ''' | |
613 if isinstance(routes, Bottle): | |
614 routes = routes.routes | |
615 for route in routes: | |
616 self.add_route(route) | |
617 | |
618 def install(self, plugin): | |
619 ''' Add a plugin to the list of plugins and prepare it for being | |
620 applied to all routes of this application. A plugin may be a simple | |
621 decorator or an object that implements the :class:`Plugin` API. | |
622 ''' | |
623 if hasattr(plugin, 'setup'): plugin.setup(self) | |
624 if not callable(plugin) and not hasattr(plugin, 'apply'): | |
625 raise TypeError("Plugins must be callable or implement .apply()") | |
626 self.plugins.append(plugin) | |
627 self.reset() | |
628 return plugin | |
629 | |
630 def uninstall(self, plugin): | |
631 ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type | |
632 object to remove all plugins that match that type, a string to remove | |
633 all plugins with a matching ``name`` attribute or ``True`` to remove all | |
634 plugins. Return the list of removed plugins. ''' | |
635 removed, remove = [], plugin | |
636 for i, plugin in list(enumerate(self.plugins))[::-1]: | |
637 if remove is True or remove is plugin or remove is type(plugin) \ | |
638 or getattr(plugin, 'name', True) == remove: | |
639 removed.append(plugin) | |
640 del self.plugins[i] | |
641 if hasattr(plugin, 'close'): plugin.close() | |
642 if removed: self.reset() | |
643 return removed | |
644 | |
645 def run(self, **kwargs): | |
646 ''' Calls :func:`run` with the same parameters. ''' | |
647 run(self, **kwargs) | |
648 | |
649 def reset(self, route=None): | |
650 ''' Reset all routes (force plugins to be re-applied) and clear all | |
651 caches. If an ID or route object is given, only that specific route | |
652 is affected. ''' | |
653 if route is None: routes = self.routes | |
654 elif isinstance(route, Route): routes = [route] | |
655 else: routes = [self.routes[route]] | |
656 for route in routes: route.reset() | |
657 if DEBUG: | |
658 for route in routes: route.prepare() | |
659 self.hooks.trigger('app_reset') | |
660 | |
661 def close(self): | |
662 ''' Close the application and all installed plugins. ''' | |
663 for plugin in self.plugins: | |
664 if hasattr(plugin, 'close'): plugin.close() | |
665 self.stopped = True | |
666 | |
667 def match(self, environ): | |
668 """ Search for a matching route and return a (:class:`Route` , urlargs) | |
669 tuple. The second value is a dictionary with parameters extracted | |
670 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" | |
671 return self.router.match(environ) | |
672 | |
673 def get_url(self, routename, **kargs): | |
674 """ Return a string that matches a named route """ | |
675 scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' | |
676 location = self.router.build(routename, **kargs).lstrip('/') | |
677 return urljoin(urljoin('/', scriptname), location) | |
678 | |
679 def add_route(self, route): | |
680 ''' Add a route object, but do not change the :data:`Route.app` | |
681 attribute.''' | |
682 self.routes.append(route) | |
683 self.router.add(route.rule, route.method, route, name=route.name) | |
684 if DEBUG: route.prepare() | |
685 | |
686 def route(self, path=None, method='GET', callback=None, name=None, | |
687 apply=None, skip=None, **config): | |
688 """ A decorator to bind a function to a request URL. Example:: | |
689 | |
690 @app.route('/hello/:name') | |
691 def hello(name): | |
692 return 'Hello %s' % name | |
693 | |
694 The ``:name`` part is a wildcard. See :class:`Router` for syntax | |
695 details. | |
696 | |
697 :param path: Request path or a list of paths to listen to. If no | |
698 path is specified, it is automatically generated from the | |
699 signature of the function. | |
700 :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of | |
701 methods to listen to. (default: `GET`) | |
702 :param callback: An optional shortcut to avoid the decorator | |
703 syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` | |
704 :param name: The name for this route. (default: None) | |
705 :param apply: A decorator or plugin or a list of plugins. These are | |
706 applied to the route callback in addition to installed plugins. | |
707 :param skip: A list of plugins, plugin classes or names. Matching | |
708 plugins are not installed to this route. ``True`` skips all. | |
709 | |
710 Any additional keyword arguments are stored as route-specific | |
711 configuration and passed to plugins (see :meth:`Plugin.apply`). | |
712 """ | |
713 if callable(path): path, callback = None, path | |
714 plugins = makelist(apply) | |
715 skiplist = makelist(skip) | |
716 def decorator(callback): | |
717 # TODO: Documentation and tests | |
718 if isinstance(callback, basestring): callback = load(callback) | |
719 for rule in makelist(path) or yieldroutes(callback): | |
720 for verb in makelist(method): | |
721 verb = verb.upper() | |
722 route = Route(self, rule, verb, callback, name=name, | |
723 plugins=plugins, skiplist=skiplist, **config) | |
724 self.add_route(route) | |
725 return callback | |
726 return decorator(callback) if callback else decorator | |
727 | |
728 def get(self, path=None, method='GET', **options): | |
729 """ Equals :meth:`route`. """ | |
730 return self.route(path, method, **options) | |
731 | |
732 def post(self, path=None, method='POST', **options): | |
733 """ Equals :meth:`route` with a ``POST`` method parameter. """ | |
734 return self.route(path, method, **options) | |
735 | |
736 def put(self, path=None, method='PUT', **options): | |
737 """ Equals :meth:`route` with a ``PUT`` method parameter. """ | |
738 return self.route(path, method, **options) | |
739 | |
740 def delete(self, path=None, method='DELETE', **options): | |
741 """ Equals :meth:`route` with a ``DELETE`` method parameter. """ | |
742 return self.route(path, method, **options) | |
743 | |
744 def error(self, code=500): | |
745 """ Decorator: Register an output handler for a HTTP error code""" | |
746 def wrapper(handler): | |
747 self.error_handler[int(code)] = handler | |
748 return handler | |
749 return wrapper | |
750 | |
751 def hook(self, name): | |
752 """ Return a decorator that attaches a callback to a hook. Three hooks | |
753 are currently implemented: | |
754 | |
755 - before_request: Executed once before each request | |
756 - after_request: Executed once after each request | |
757 - app_reset: Called whenever :meth:`reset` is called. | |
758 """ | |
759 def wrapper(func): | |
760 self.hooks.add(name, func) | |
761 return func | |
762 return wrapper | |
763 | |
764 def handle(self, path, method='GET'): | |
765 """ (deprecated) Execute the first matching route callback and return | |
766 the result. :exc:`HTTPResponse` exceptions are caught and returned. | |
767 If :attr:`Bottle.catchall` is true, other exceptions are caught as | |
768 well and returned as :exc:`HTTPError` instances (500). | |
769 """ | |
770 depr("This method will change semantics in 0.10. Try to avoid it.") | |
771 if isinstance(path, dict): | |
772 return self._handle(path) | |
773 return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) | |
774 | |
775 def _handle(self, environ): | |
776 try: | |
777 environ['bottle.app'] = self | |
778 request.bind(environ) | |
779 response.bind() | |
780 route, args = self.router.match(environ) | |
781 environ['route.handle'] = environ['bottle.route'] = route | |
782 environ['route.url_args'] = args | |
783 return route.call(**args) | |
784 except HTTPResponse: | |
785 return _e() | |
786 except RouteReset: | |
787 route.reset() | |
788 return self._handle(environ) | |
789 except (KeyboardInterrupt, SystemExit, MemoryError): | |
790 raise | |
791 except Exception: | |
792 if not self.catchall: raise | |
793 stacktrace = format_exc() | |
794 environ['wsgi.errors'].write(stacktrace) | |
795 return HTTPError(500, "Internal Server Error", _e(), stacktrace) | |
796 | |
797 def _cast(self, out, peek=None): | |
798 """ Try to convert the parameter into something WSGI compatible and set | |
799 correct HTTP headers when possible. | |
800 Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, | |
801 iterable of strings and iterable of unicodes | |
802 """ | |
803 | |
804 # Empty output is done here | |
805 if not out: | |
806 response['Content-Length'] = 0 | |
807 return [] | |
808 # Join lists of byte or unicode strings. Mixed lists are NOT supported | |
809 if isinstance(out, (tuple, list))\ | |
810 and isinstance(out[0], (bytes, unicode)): | |
811 out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' | |
812 # Encode unicode strings | |
813 if isinstance(out, unicode): | |
814 out = out.encode(response.charset) | |
815 # Byte Strings are just returned | |
816 if isinstance(out, bytes): | |
817 response['Content-Length'] = len(out) | |
818 return [out] | |
819 # HTTPError or HTTPException (recursive, because they may wrap anything) | |
820 # TODO: Handle these explicitly in handle() or make them iterable. | |
821 if isinstance(out, HTTPError): | |
822 out.apply(response) | |
823 out = self.error_handler.get(out.status, repr)(out) | |
824 if isinstance(out, HTTPResponse): | |
825 depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 | |
826 return self._cast(out) | |
827 if isinstance(out, HTTPResponse): | |
828 out.apply(response) | |
829 return self._cast(out.output) | |
830 | |
831 # File-like objects. | |
832 if hasattr(out, 'read'): | |
833 if 'wsgi.file_wrapper' in request.environ: | |
834 return request.environ['wsgi.file_wrapper'](out) | |
835 elif hasattr(out, 'close') or not hasattr(out, '__iter__'): | |
836 return WSGIFileWrapper(out) | |
837 | |
838 # Handle Iterables. We peek into them to detect their inner type. | |
839 try: | |
840 out = iter(out) | |
841 first = next(out) | |
842 while not first: | |
843 first = next(out) | |
844 except StopIteration: | |
845 return self._cast('') | |
846 except HTTPResponse: | |
847 first = _e() | |
848 except (KeyboardInterrupt, SystemExit, MemoryError): | |
849 raise | |
850 except Exception: | |
851 if not self.catchall: raise | |
852 first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) | |
853 | |
854 # These are the inner types allowed in iterator or generator objects. | |
855 if isinstance(first, HTTPResponse): | |
856 return self._cast(first) | |
857 if isinstance(first, bytes): | |
858 return itertools.chain([first], out) | |
859 if isinstance(first, unicode): | |
860 return imap(lambda x: x.encode(response.charset), | |
861 itertools.chain([first], out)) | |
862 return self._cast(HTTPError(500, 'Unsupported response type: %s'\ | |
863 % type(first))) | |
864 | |
865 def wsgi(self, environ, start_response): | |
866 """ The bottle WSGI-interface. """ | |
867 try: | |
868 out = self._cast(self._handle(environ)) | |
869 # rfc2616 section 4.3 | |
870 if response._status_code in (100, 101, 204, 304)\ | |
871 or request.method == 'HEAD': | |
872 if hasattr(out, 'close'): out.close() | |
873 out = [] | |
874 if isinstance(response._status_line, unicode): | |
875 response._status_line = str(response._status_line) | |
876 start_response(response._status_line, list(response.iter_headers())) | |
877 return out | |
878 except (KeyboardInterrupt, SystemExit, MemoryError): | |
879 raise | |
880 except Exception: | |
881 if not self.catchall: raise | |
882 err = '<h1>Critical error while processing request: %s</h1>' \ | |
883 % html_escape(environ.get('PATH_INFO', '/')) | |
884 if DEBUG: | |
885 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \ | |
886 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \ | |
887 % (html_escape(repr(_e())), html_escape(format_exc())) | |
888 environ['wsgi.errors'].write(err) | |
889 headers = [('Content-Type', 'text/html; charset=UTF-8')] | |
890 start_response('500 INTERNAL SERVER ERROR', headers) | |
891 return [tob(err)] | |
892 | |
893 def __call__(self, environ, start_response): | |
894 ''' Each instance of :class:'Bottle' is a WSGI application. ''' | |
895 return self.wsgi(environ, start_response) | |
896 | |
897 | |
898 | |
899 | |
900 | |
901 | |
902 ############################################################################### | |
903 # HTTP and WSGI Tools ########################################################## | |
904 ############################################################################### | |
905 | |
906 | |
907 class BaseRequest(object): | |
908 """ A wrapper for WSGI environment dictionaries that adds a lot of | |
909 convenient access methods and properties. Most of them are read-only. | |
910 | |
911 Adding new attributes to a request actually adds them to the environ | |
912 dictionary (as 'bottle.request.ext.<name>'). This is the recommended | |
913 way to store and access request-specific data. | |
914 """ | |
915 | |
916 __slots__ = ('environ') | |
917 | |
918 #: Maximum size of memory buffer for :attr:`body` in bytes. | |
919 MEMFILE_MAX = 102400 | |
920 #: Maximum number pr GET or POST parameters per request | |
921 MAX_PARAMS = 100 | |
922 | |
923 def __init__(self, environ=None): | |
924 """ Wrap a WSGI environ dictionary. """ | |
925 #: The wrapped WSGI environ dictionary. This is the only real attribute. | |
926 #: All other attributes actually are read-only properties. | |
927 self.environ = {} if environ is None else environ | |
928 self.environ['bottle.request'] = self | |
929 | |
930 @DictProperty('environ', 'bottle.app', read_only=True) | |
931 def app(self): | |
932 ''' Bottle application handling this request. ''' | |
933 raise RuntimeError('This request is not connected to an application.') | |
934 | |
935 @property | |
936 def path(self): | |
937 ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix | |
938 broken clients and avoid the "empty path" edge case). ''' | |
939 return '/' + self.environ.get('PATH_INFO','').lstrip('/') | |
940 | |
941 @property | |
942 def method(self): | |
943 ''' The ``REQUEST_METHOD`` value as an uppercase string. ''' | |
944 return self.environ.get('REQUEST_METHOD', 'GET').upper() | |
945 | |
946 @DictProperty('environ', 'bottle.request.headers', read_only=True) | |
947 def headers(self): | |
948 ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to | |
949 HTTP request headers. ''' | |
950 return WSGIHeaderDict(self.environ) | |
951 | |
952 def get_header(self, name, default=None): | |
953 ''' Return the value of a request header, or a given default value. ''' | |
954 return self.headers.get(name, default) | |
955 | |
956 @DictProperty('environ', 'bottle.request.cookies', read_only=True) | |
957 def cookies(self): | |
958 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT | |
959 decoded. Use :meth:`get_cookie` if you expect signed cookies. """ | |
960 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) | |
961 cookies = list(cookies.values())[:self.MAX_PARAMS] | |
962 return FormsDict((c.key, c.value) for c in cookies) | |
963 | |
964 def get_cookie(self, key, default=None, secret=None): | |
965 """ Return the content of a cookie. To read a `Signed Cookie`, the | |
966 `secret` must match the one used to create the cookie (see | |
967 :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing | |
968 cookie or wrong signature), return a default value. """ | |
969 value = self.cookies.get(key) | |
970 if secret and value: | |
971 dec = cookie_decode(value, secret) # (key, value) tuple or None | |
972 return dec[1] if dec and dec[0] == key else default | |
973 return value or default | |
974 | |
975 @DictProperty('environ', 'bottle.request.query', read_only=True) | |
976 def query(self): | |
977 ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These | |
978 values are sometimes called "URL arguments" or "GET parameters", but | |
979 not to be confused with "URL wildcards" as they are provided by the | |
980 :class:`Router`. ''' | |
981 pairs = parse_qsl(self.query_string, keep_blank_values=True) | |
982 get = self.environ['bottle.get'] = FormsDict() | |
983 for key, value in pairs[:self.MAX_PARAMS]: | |
984 get[key] = value | |
985 return get | |
986 | |
987 @DictProperty('environ', 'bottle.request.forms', read_only=True) | |
988 def forms(self): | |
989 """ Form values parsed from an `url-encoded` or `multipart/form-data` | |
990 encoded POST or PUT request body. The result is retuned as a | |
991 :class:`FormsDict`. All keys and values are strings. File uploads | |
992 are stored separately in :attr:`files`. """ | |
993 forms = FormsDict() | |
994 for name, item in self.POST.allitems(): | |
995 if not hasattr(item, 'filename'): | |
996 forms[name] = item | |
997 return forms | |
998 | |
999 @DictProperty('environ', 'bottle.request.params', read_only=True) | |
1000 def params(self): | |
1001 """ A :class:`FormsDict` with the combined values of :attr:`query` and | |
1002 :attr:`forms`. File uploads are stored in :attr:`files`. """ | |
1003 params = FormsDict() | |
1004 for key, value in self.query.allitems(): | |
1005 params[key] = value | |
1006 for key, value in self.forms.allitems(): | |
1007 params[key] = value | |
1008 return params | |
1009 | |
1010 @DictProperty('environ', 'bottle.request.files', read_only=True) | |
1011 def files(self): | |
1012 """ File uploads parsed from an `url-encoded` or `multipart/form-data` | |
1013 encoded POST or PUT request body. The values are instances of | |
1014 :class:`cgi.FieldStorage`. The most important attributes are: | |
1015 | |
1016 filename | |
1017 The filename, if specified; otherwise None; this is the client | |
1018 side filename, *not* the file name on which it is stored (that's | |
1019 a temporary file you don't deal with) | |
1020 file | |
1021 The file(-like) object from which you can read the data. | |
1022 value | |
1023 The value as a *string*; for file uploads, this transparently | |
1024 reads the file every time you request the value. Do not do this | |
1025 on big files. | |
1026 """ | |
1027 files = FormsDict() | |
1028 for name, item in self.POST.allitems(): | |
1029 if hasattr(item, 'filename'): | |
1030 files[name] = item | |
1031 return files | |
1032 | |
1033 @DictProperty('environ', 'bottle.request.json', read_only=True) | |
1034 def json(self): | |
1035 ''' If the ``Content-Type`` header is ``application/json``, this | |
1036 property holds the parsed content of the request body. Only requests | |
1037 smaller than :attr:`MEMFILE_MAX` are processed to avoid memory | |
1038 exhaustion. ''' | |
1039 if 'application/json' in self.environ.get('CONTENT_TYPE', '') \ | |
1040 and 0 < self.content_length < self.MEMFILE_MAX: | |
1041 return json_loads(self.body.read(self.MEMFILE_MAX)) | |
1042 return None | |
1043 | |
1044 @DictProperty('environ', 'bottle.request.body', read_only=True) | |
1045 def _body(self): | |
1046 maxread = max(0, self.content_length) | |
1047 stream = self.environ['wsgi.input'] | |
1048 body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b') | |
1049 while maxread > 0: | |
1050 part = stream.read(min(maxread, self.MEMFILE_MAX)) | |
1051 if not part: break | |
1052 body.write(part) | |
1053 maxread -= len(part) | |
1054 self.environ['wsgi.input'] = body | |
1055 body.seek(0) | |
1056 return body | |
1057 | |
1058 @property | |
1059 def body(self): | |
1060 """ The HTTP request body as a seek-able file-like object. Depending on | |
1061 :attr:`MEMFILE_MAX`, this is either a temporary file or a | |
1062 :class:`io.BytesIO` instance. Accessing this property for the first | |
1063 time reads and replaces the ``wsgi.input`` environ variable. | |
1064 Subsequent accesses just do a `seek(0)` on the file object. """ | |
1065 self._body.seek(0) | |
1066 return self._body | |
1067 | |
1068 #: An alias for :attr:`query`. | |
1069 GET = query | |
1070 | |
1071 @DictProperty('environ', 'bottle.request.post', read_only=True) | |
1072 def POST(self): | |
1073 """ The values of :attr:`forms` and :attr:`files` combined into a single | |
1074 :class:`FormsDict`. Values are either strings (form values) or | |
1075 instances of :class:`cgi.FieldStorage` (file uploads). | |
1076 """ | |
1077 post = FormsDict() | |
1078 safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi | |
1079 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): | |
1080 if key in self.environ: safe_env[key] = self.environ[key] | |
1081 if NCTextIOWrapper: | |
1082 fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') | |
1083 else: | |
1084 fb = self.body | |
1085 data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) | |
1086 for item in (data.list or [])[:self.MAX_PARAMS]: | |
1087 post[item.name] = item if item.filename else item.value | |
1088 return post | |
1089 | |
1090 @property | |
1091 def COOKIES(self): | |
1092 ''' Alias for :attr:`cookies` (deprecated). ''' | |
1093 depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).') | |
1094 return self.cookies | |
1095 | |
1096 @property | |
1097 def url(self): | |
1098 """ The full request URI including hostname and scheme. If your app | |
1099 lives behind a reverse proxy or load balancer and you get confusing | |
1100 results, make sure that the ``X-Forwarded-Host`` header is set | |
1101 correctly. """ | |
1102 return self.urlparts.geturl() | |
1103 | |
1104 @DictProperty('environ', 'bottle.request.urlparts', read_only=True) | |
1105 def urlparts(self): | |
1106 ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. | |
1107 The tuple contains (scheme, host, path, query_string and fragment), | |
1108 but the fragment is always empty because it is not visible to the | |
1109 server. ''' | |
1110 env = self.environ | |
1111 http = env.get('wsgi.url_scheme', 'http') | |
1112 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') | |
1113 if not host: | |
1114 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. | |
1115 host = env.get('SERVER_NAME', '127.0.0.1') | |
1116 port = env.get('SERVER_PORT') | |
1117 if port and port != ('80' if http == 'http' else '443'): | |
1118 host += ':' + port | |
1119 path = urlquote(self.fullpath) | |
1120 return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') | |
1121 | |
1122 @property | |
1123 def fullpath(self): | |
1124 """ Request path including :attr:`script_name` (if present). """ | |
1125 return urljoin(self.script_name, self.path.lstrip('/')) | |
1126 | |
1127 @property | |
1128 def query_string(self): | |
1129 """ The raw :attr:`query` part of the URL (everything in between ``?`` | |
1130 and ``#``) as a string. """ | |
1131 return self.environ.get('QUERY_STRING', '') | |
1132 | |
1133 @property | |
1134 def script_name(self): | |
1135 ''' The initial portion of the URL's `path` that was removed by a higher | |
1136 level (server or routing middleware) before the application was | |
1137 called. This script path is returned with leading and tailing | |
1138 slashes. ''' | |
1139 script_name = self.environ.get('SCRIPT_NAME', '').strip('/') | |
1140 return '/' + script_name + '/' if script_name else '/' | |
1141 | |
1142 def path_shift(self, shift=1): | |
1143 ''' Shift path segments from :attr:`path` to :attr:`script_name` and | |
1144 vice versa. | |
1145 | |
1146 :param shift: The number of path segments to shift. May be negative | |
1147 to change the shift direction. (default: 1) | |
1148 ''' | |
1149 script = self.environ.get('SCRIPT_NAME','/') | |
1150 self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift) | |
1151 | |
1152 @property | |
1153 def content_length(self): | |
1154 ''' The request body length as an integer. The client is responsible to | |
1155 set this header. Otherwise, the real length of the body is unknown | |
1156 and -1 is returned. In this case, :attr:`body` will be empty. ''' | |
1157 return int(self.environ.get('CONTENT_LENGTH') or -1) | |
1158 | |
1159 @property | |
1160 def is_xhr(self): | |
1161 ''' True if the request was triggered by a XMLHttpRequest. This only | |
1162 works with JavaScript libraries that support the `X-Requested-With` | |
1163 header (most of the popular libraries do). ''' | |
1164 requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','') | |
1165 return requested_with.lower() == 'xmlhttprequest' | |
1166 | |
1167 @property | |
1168 def is_ajax(self): | |
1169 ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. ''' | |
1170 return self.is_xhr | |
1171 | |
1172 @property | |
1173 def auth(self): | |
1174 """ HTTP authentication data as a (user, password) tuple. This | |
1175 implementation currently supports basic (not digest) authentication | |
1176 only. If the authentication happened at a higher level (e.g. in the | |
1177 front web-server or a middleware), the password field is None, but | |
1178 the user field is looked up from the ``REMOTE_USER`` environ | |
1179 variable. On any errors, None is returned. """ | |
1180 basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION','')) | |
1181 if basic: return basic | |
1182 ruser = self.environ.get('REMOTE_USER') | |
1183 if ruser: return (ruser, None) | |
1184 return None | |
1185 | |
1186 @property | |
1187 def remote_route(self): | |
1188 """ A list of all IPs that were involved in this request, starting with | |
1189 the client IP and followed by zero or more proxies. This does only | |
1190 work if all proxies support the ```X-Forwarded-For`` header. Note | |
1191 that this information can be forged by malicious clients. """ | |
1192 proxy = self.environ.get('HTTP_X_FORWARDED_FOR') | |
1193 if proxy: return [ip.strip() for ip in proxy.split(',')] | |
1194 remote = self.environ.get('REMOTE_ADDR') | |
1195 return [remote] if remote else [] | |
1196 | |
1197 @property | |
1198 def remote_addr(self): | |
1199 """ The client IP as a string. Note that this information can be forged | |
1200 by malicious clients. """ | |
1201 route = self.remote_route | |
1202 return route[0] if route else None | |
1203 | |
1204 def copy(self): | |
1205 """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ | |
1206 return Request(self.environ.copy()) | |
1207 | |
1208 def get(self, value, default=None): return self.environ.get(value, default) | |
1209 def __getitem__(self, key): return self.environ[key] | |
1210 def __delitem__(self, key): self[key] = ""; del(self.environ[key]) | |
1211 def __iter__(self): return iter(self.environ) | |
1212 def __len__(self): return len(self.environ) | |
1213 def keys(self): return self.environ.keys() | |
1214 def __setitem__(self, key, value): | |
1215 """ Change an environ value and clear all caches that depend on it. """ | |
1216 | |
1217 if self.environ.get('bottle.request.readonly'): | |
1218 raise KeyError('The environ dictionary is read-only.') | |
1219 | |
1220 self.environ[key] = value | |
1221 todelete = () | |
1222 | |
1223 if key == 'wsgi.input': | |
1224 todelete = ('body', 'forms', 'files', 'params', 'post', 'json') | |
1225 elif key == 'QUERY_STRING': | |
1226 todelete = ('query', 'params') | |
1227 elif key.startswith('HTTP_'): | |
1228 todelete = ('headers', 'cookies') | |
1229 | |
1230 for key in todelete: | |
1231 self.environ.pop('bottle.request.'+key, None) | |
1232 | |
1233 def __repr__(self): | |
1234 return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) | |
1235 | |
1236 def __getattr__(self, name): | |
1237 ''' Search in self.environ for additional user defined attributes. ''' | |
1238 try: | |
1239 var = self.environ['bottle.request.ext.%s'%name] | |
1240 return var.__get__(self) if hasattr(var, '__get__') else var | |
1241 except KeyError: | |
1242 raise AttributeError('Attribute %r not defined.' % name) | |
1243 | |
1244 def __setattr__(self, name, value): | |
1245 if name == 'environ': return object.__setattr__(self, name, value) | |
1246 self.environ['bottle.request.ext.%s'%name] = value | |
1247 | |
1248 | |
1249 | |
1250 | |
1251 def _hkey(s): | |
1252 return s.title().replace('_','-') | |
1253 | |
1254 | |
1255 class HeaderProperty(object): | |
1256 def __init__(self, name, reader=None, writer=str, default=''): | |
1257 self.name, self.default = name, default | |
1258 self.reader, self.writer = reader, writer | |
1259 self.__doc__ = 'Current value of the %r header.' % name.title() | |
1260 | |
1261 def __get__(self, obj, cls): | |
1262 if obj is None: return self | |
1263 value = obj.headers.get(self.name, self.default) | |
1264 return self.reader(value) if self.reader else value | |
1265 | |
1266 def __set__(self, obj, value): | |
1267 obj.headers[self.name] = self.writer(value) | |
1268 | |
1269 def __delete__(self, obj): | |
1270 del obj.headers[self.name] | |
1271 | |
1272 | |
1273 class BaseResponse(object): | |
1274 """ Storage class for a response body as well as headers and cookies. | |
1275 | |
1276 This class does support dict-like case-insensitive item-access to | |
1277 headers, but is NOT a dict. Most notably, iterating over a response | |
1278 yields parts of the body and not the headers. | |
1279 """ | |
1280 | |
1281 default_status = 200 | |
1282 default_content_type = 'text/html; charset=UTF-8' | |
1283 | |
1284 # Header blacklist for specific response codes | |
1285 # (rfc2616 section 10.2.3 and 10.3.5) | |
1286 bad_headers = { | |
1287 204: set(('Content-Type',)), | |
1288 304: set(('Allow', 'Content-Encoding', 'Content-Language', | |
1289 'Content-Length', 'Content-Range', 'Content-Type', | |
1290 'Content-Md5', 'Last-Modified'))} | |
1291 | |
1292 def __init__(self, body='', status=None, **headers): | |
1293 self._status_line = None | |
1294 self._status_code = None | |
1295 self._cookies = None | |
1296 self._headers = {'Content-Type': [self.default_content_type]} | |
1297 self.body = body | |
1298 self.status = status or self.default_status | |
1299 if headers: | |
1300 for name, value in headers.items(): | |
1301 self[name] = value | |
1302 | |
1303 def copy(self): | |
1304 ''' Returns a copy of self. ''' | |
1305 copy = Response() | |
1306 copy.status = self.status | |
1307 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) | |
1308 return copy | |
1309 | |
1310 def __iter__(self): | |
1311 return iter(self.body) | |
1312 | |
1313 def close(self): | |
1314 if hasattr(self.body, 'close'): | |
1315 self.body.close() | |
1316 | |
1317 @property | |
1318 def status_line(self): | |
1319 ''' The HTTP status line as a string (e.g. ``404 Not Found``).''' | |
1320 return self._status_line | |
1321 | |
1322 @property | |
1323 def status_code(self): | |
1324 ''' The HTTP status code as an integer (e.g. 404).''' | |
1325 return self._status_code | |
1326 | |
1327 def _set_status(self, status): | |
1328 if isinstance(status, int): | |
1329 code, status = status, _HTTP_STATUS_LINES.get(status) | |
1330 elif ' ' in status: | |
1331 status = status.strip() | |
1332 code = int(status.split()[0]) | |
1333 else: | |
1334 raise ValueError('String status line without a reason phrase.') | |
1335 if not 100 <= code <= 999: raise ValueError('Status code out of range.') | |
1336 self._status_code = code | |
1337 self._status_line = status or ('%d Unknown' % code) | |
1338 | |
1339 def _get_status(self): | |
1340 return self._status_line | |
1341 | |
1342 status = property(_get_status, _set_status, None, | |
1343 ''' A writeable property to change the HTTP response status. It accepts | |
1344 either a numeric code (100-999) or a string with a custom reason | |
1345 phrase (e.g. "404 Brain not found"). Both :data:`status_line` and | |
1346 :data:`status_code` are updated accordingly. The return value is | |
1347 always a status string. ''') | |
1348 del _get_status, _set_status | |
1349 | |
1350 @property | |
1351 def headers(self): | |
1352 ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like | |
1353 view on the response headers. ''' | |
1354 self.__dict__['headers'] = hdict = HeaderDict() | |
1355 hdict.dict = self._headers | |
1356 return hdict | |
1357 | |
1358 def __contains__(self, name): return _hkey(name) in self._headers | |
1359 def __delitem__(self, name): del self._headers[_hkey(name)] | |
1360 def __getitem__(self, name): return self._headers[_hkey(name)][-1] | |
1361 def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)] | |
1362 | |
1363 def get_header(self, name, default=None): | |
1364 ''' Return the value of a previously defined header. If there is no | |
1365 header with that name, return a default value. ''' | |
1366 return self._headers.get(_hkey(name), [default])[-1] | |
1367 | |
1368 def set_header(self, name, value, append=False): | |
1369 ''' Create a new response header, replacing any previously defined | |
1370 headers with the same name. ''' | |
1371 if append: | |
1372 self.add_header(name, value) | |
1373 else: | |
1374 self._headers[_hkey(name)] = [str(value)] | |
1375 | |
1376 def add_header(self, name, value): | |
1377 ''' Add an additional response header, not removing duplicates. ''' | |
1378 self._headers.setdefault(_hkey(name), []).append(str(value)) | |
1379 | |
1380 def iter_headers(self): | |
1381 ''' Yield (header, value) tuples, skipping headers that are not | |
1382 allowed with the current response status code. ''' | |
1383 headers = self._headers.items() | |
1384 bad_headers = self.bad_headers.get(self._status_code) | |
1385 if bad_headers: | |
1386 headers = [h for h in headers if h[0] not in bad_headers] | |
1387 for name, values in headers: | |
1388 for value in values: | |
1389 yield name, value | |
1390 if self._cookies: | |
1391 for c in self._cookies.values(): | |
1392 yield 'Set-Cookie', c.OutputString() | |
1393 | |
1394 def wsgiheader(self): | |
1395 depr('The wsgiheader method is deprecated. See headerlist.') #0.10 | |
1396 return self.headerlist | |
1397 | |
1398 @property | |
1399 def headerlist(self): | |
1400 ''' WSGI conform list of (header, value) tuples. ''' | |
1401 return list(self.iter_headers()) | |
1402 | |
1403 content_type = HeaderProperty('Content-Type') | |
1404 content_length = HeaderProperty('Content-Length', reader=int) | |
1405 | |
1406 @property | |
1407 def charset(self): | |
1408 """ Return the charset specified in the content-type header (default: utf8). """ | |
1409 if 'charset=' in self.content_type: | |
1410 return self.content_type.split('charset=')[-1].split(';')[0].strip() | |
1411 return 'UTF-8' | |
1412 | |
1413 @property | |
1414 def COOKIES(self): | |
1415 """ A dict-like SimpleCookie instance. This should not be used directly. | |
1416 See :meth:`set_cookie`. """ | |
1417 depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10 | |
1418 if not self._cookies: | |
1419 self._cookies = SimpleCookie() | |
1420 return self._cookies | |
1421 | |
1422 def set_cookie(self, name, value, secret=None, **options): | |
1423 ''' Create a new cookie or replace an old one. If the `secret` parameter is | |
1424 set, create a `Signed Cookie` (described below). | |
1425 | |
1426 :param name: the name of the cookie. | |
1427 :param value: the value of the cookie. | |
1428 :param secret: a signature key required for signed cookies. | |
1429 | |
1430 Additionally, this method accepts all RFC 2109 attributes that are | |
1431 supported by :class:`cookie.Morsel`, including: | |
1432 | |
1433 :param max_age: maximum age in seconds. (default: None) | |
1434 :param expires: a datetime object or UNIX timestamp. (default: None) | |
1435 :param domain: the domain that is allowed to read the cookie. | |
1436 (default: current domain) | |
1437 :param path: limits the cookie to a given path (default: current path) | |
1438 :param secure: limit the cookie to HTTPS connections (default: off). | |
1439 :param httponly: prevents client-side javascript to read this cookie | |
1440 (default: off, requires Python 2.6 or newer). | |
1441 | |
1442 If neither `expires` nor `max_age` is set (default), the cookie will | |
1443 expire at the end of the browser session (as soon as the browser | |
1444 window is closed). | |
1445 | |
1446 Signed cookies may store any pickle-able object and are | |
1447 cryptographically signed to prevent manipulation. Keep in mind that | |
1448 cookies are limited to 4kb in most browsers. | |
1449 | |
1450 Warning: Signed cookies are not encrypted (the client can still see | |
1451 the content) and not copy-protected (the client can restore an old | |
1452 cookie). The main intention is to make pickling and unpickling | |
1453 save, not to store secret information at client side. | |
1454 ''' | |
1455 if not self._cookies: | |
1456 self._cookies = SimpleCookie() | |
1457 | |
1458 if secret: | |
1459 value = touni(cookie_encode((name, value), secret)) | |
1460 elif not isinstance(value, basestring): | |
1461 raise TypeError('Secret key missing for non-string Cookie.') | |
1462 | |
1463 if len(value) > 4096: raise ValueError('Cookie value to long.') | |
1464 self._cookies[name] = value | |
1465 | |
1466 for key, value in options.items(): | |
1467 if key == 'max_age': | |
1468 if isinstance(value, timedelta): | |
1469 value = value.seconds + value.days * 24 * 3600 | |
1470 if key == 'expires': | |
1471 if isinstance(value, (datedate, datetime)): | |
1472 value = value.timetuple() | |
1473 elif isinstance(value, (int, float)): | |
1474 value = time.gmtime(value) | |
1475 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) | |
1476 self._cookies[name][key.replace('_', '-')] = value | |
1477 | |
1478 def delete_cookie(self, key, **kwargs): | |
1479 ''' Delete a cookie. Be sure to use the same `domain` and `path` | |
1480 settings as used to create the cookie. ''' | |
1481 kwargs['max_age'] = -1 | |
1482 kwargs['expires'] = 0 | |
1483 self.set_cookie(key, '', **kwargs) | |
1484 | |
1485 def __repr__(self): | |
1486 out = '' | |
1487 for name, value in self.headerlist: | |
1488 out += '%s: %s\n' % (name.title(), value.strip()) | |
1489 return out | |
1490 | |
1491 #: Thread-local storage for :class:`LocalRequest` and :class:`LocalResponse` | |
1492 #: attributes. | |
1493 _lctx = threading.local() | |
1494 | |
1495 def local_property(name): | |
1496 def fget(self): | |
1497 try: | |
1498 return getattr(_lctx, name) | |
1499 except AttributeError: | |
1500 raise RuntimeError("Request context not initialized.") | |
1501 def fset(self, value): setattr(_lctx, name, value) | |
1502 def fdel(self): delattr(_lctx, name) | |
1503 return property(fget, fset, fdel, | |
1504 'Thread-local property stored in :data:`_lctx.%s`' % name) | |
1505 | |
1506 | |
1507 class LocalRequest(BaseRequest): | |
1508 ''' A thread-local subclass of :class:`BaseRequest` with a different | |
1509 set of attribues for each thread. There is usually only one global | |
1510 instance of this class (:data:`request`). If accessed during a | |
1511 request/response cycle, this instance always refers to the *current* | |
1512 request (even on a multithreaded server). ''' | |
1513 bind = BaseRequest.__init__ | |
1514 environ = local_property('request_environ') | |
1515 | |
1516 | |
1517 class LocalResponse(BaseResponse): | |
1518 ''' A thread-local subclass of :class:`BaseResponse` with a different | |
1519 set of attribues for each thread. There is usually only one global | |
1520 instance of this class (:data:`response`). Its attributes are used | |
1521 to build the HTTP response at the end of the request/response cycle. | |
1522 ''' | |
1523 bind = BaseResponse.__init__ | |
1524 _status_line = local_property('response_status_line') | |
1525 _status_code = local_property('response_status_code') | |
1526 _cookies = local_property('response_cookies') | |
1527 _headers = local_property('response_headers') | |
1528 body = local_property('response_body') | |
1529 | |
1530 Response = LocalResponse # BC 0.9 | |
1531 Request = LocalRequest # BC 0.9 | |
1532 | |
1533 | |
1534 | |
1535 | |
1536 | |
1537 | |
1538 ############################################################################### | |
1539 # Plugins ###################################################################### | |
1540 ############################################################################### | |
1541 | |
1542 class PluginError(BottleException): pass | |
1543 | |
1544 class JSONPlugin(object): | |
1545 name = 'json' | |
1546 api = 2 | |
1547 | |
1548 def __init__(self, json_dumps=json_dumps): | |
1549 self.json_dumps = json_dumps | |
1550 | |
1551 def apply(self, callback, context): | |
1552 dumps = self.json_dumps | |
1553 if not dumps: return callback | |
1554 def wrapper(*a, **ka): | |
1555 rv = callback(*a, **ka) | |
1556 if isinstance(rv, dict): | |
1557 #Attempt to serialize, raises exception on failure | |
1558 json_response = dumps(rv) | |
1559 #Set content type only if serialization succesful | |
1560 response.content_type = 'application/json' | |
1561 return json_response | |
1562 return rv | |
1563 return wrapper | |
1564 | |
1565 | |
1566 class HooksPlugin(object): | |
1567 name = 'hooks' | |
1568 api = 2 | |
1569 | |
1570 _names = 'before_request', 'after_request', 'app_reset' | |
1571 | |
1572 def __init__(self): | |
1573 self.hooks = dict((name, []) for name in self._names) | |
1574 self.app = None | |
1575 | |
1576 def _empty(self): | |
1577 return not (self.hooks['before_request'] or self.hooks['after_request']) | |
1578 | |
1579 def setup(self, app): | |
1580 self.app = app | |
1581 | |
1582 def add(self, name, func): | |
1583 ''' Attach a callback to a hook. ''' | |
1584 was_empty = self._empty() | |
1585 self.hooks.setdefault(name, []).append(func) | |
1586 if self.app and was_empty and not self._empty(): self.app.reset() | |
1587 | |
1588 def remove(self, name, func): | |
1589 ''' Remove a callback from a hook. ''' | |
1590 was_empty = self._empty() | |
1591 if name in self.hooks and func in self.hooks[name]: | |
1592 self.hooks[name].remove(func) | |
1593 if self.app and not was_empty and self._empty(): self.app.reset() | |
1594 | |
1595 def trigger(self, name, *a, **ka): | |
1596 ''' Trigger a hook and return a list of results. ''' | |
1597 hooks = self.hooks[name] | |
1598 if ka.pop('reversed', False): hooks = hooks[::-1] | |
1599 return [hook(*a, **ka) for hook in hooks] | |
1600 | |
1601 def apply(self, callback, context): | |
1602 if self._empty(): return callback | |
1603 def wrapper(*a, **ka): | |
1604 self.trigger('before_request') | |
1605 rv = callback(*a, **ka) | |
1606 self.trigger('after_request', reversed=True) | |
1607 return rv | |
1608 return wrapper | |
1609 | |
1610 | |
1611 class TemplatePlugin(object): | |
1612 ''' This plugin applies the :func:`view` decorator to all routes with a | |
1613 `template` config parameter. If the parameter is a tuple, the second | |
1614 element must be a dict with additional options (e.g. `template_engine`) | |
1615 or default variables for the template. ''' | |
1616 name = 'template' | |
1617 api = 2 | |
1618 | |
1619 def apply(self, callback, route): | |
1620 conf = route.config.get('template') | |
1621 if isinstance(conf, (tuple, list)) and len(conf) == 2: | |
1622 return view(conf[0], **conf[1])(callback) | |
1623 elif isinstance(conf, str) and 'template_opts' in route.config: | |
1624 depr('The `template_opts` parameter is deprecated.') #0.9 | |
1625 return view(conf, **route.config['template_opts'])(callback) | |
1626 elif isinstance(conf, str): | |
1627 return view(conf)(callback) | |
1628 else: | |
1629 return callback | |
1630 | |
1631 | |
1632 #: Not a plugin, but part of the plugin API. TODO: Find a better place. | |
1633 class _ImportRedirect(object): | |
1634 def __init__(self, name, impmask): | |
1635 ''' Create a virtual package that redirects imports (see PEP 302). ''' | |
1636 self.name = name | |
1637 self.impmask = impmask | |
1638 self.module = sys.modules.setdefault(name, imp.new_module(name)) | |
1639 self.module.__dict__.update({'__file__': __file__, '__path__': [], | |
1640 '__all__': [], '__loader__': self}) | |
1641 sys.meta_path.append(self) | |
1642 | |
1643 def find_module(self, fullname, path=None): | |
1644 if '.' not in fullname: return | |
1645 packname, modname = fullname.rsplit('.', 1) | |
1646 if packname != self.name: return | |
1647 return self | |
1648 | |
1649 def load_module(self, fullname): | |
1650 if fullname in sys.modules: return sys.modules[fullname] | |
1651 packname, modname = fullname.rsplit('.', 1) | |
1652 realname = self.impmask % modname | |
1653 __import__(realname) | |
1654 module = sys.modules[fullname] = sys.modules[realname] | |
1655 setattr(self.module, modname, module) | |
1656 module.__loader__ = self | |
1657 return module | |
1658 | |
1659 | |
1660 | |
1661 | |
1662 | |
1663 | |
1664 ############################################################################### | |
1665 # Common Utilities ############################################################# | |
1666 ############################################################################### | |
1667 | |
1668 | |
1669 class MultiDict(DictMixin): | |
1670 """ This dict stores multiple values per key, but behaves exactly like a | |
1671 normal dict in that it returns only the newest value for any given key. | |
1672 There are special methods available to access the full list of values. | |
1673 """ | |
1674 | |
1675 def __init__(self, *a, **k): | |
1676 self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) | |
1677 | |
1678 def __len__(self): return len(self.dict) | |
1679 def __iter__(self): return iter(self.dict) | |
1680 def __contains__(self, key): return key in self.dict | |
1681 def __delitem__(self, key): del self.dict[key] | |
1682 def __getitem__(self, key): return self.dict[key][-1] | |
1683 def __setitem__(self, key, value): self.append(key, value) | |
1684 def keys(self): return self.dict.keys() | |
1685 | |
1686 if py3k: | |
1687 def values(self): return (v[-1] for v in self.dict.values()) | |
1688 def items(self): return ((k, v[-1]) for k, v in self.dict.items()) | |
1689 def allitems(self): | |
1690 return ((k, v) for k, vl in self.dict.items() for v in vl) | |
1691 iterkeys = keys | |
1692 itervalues = values | |
1693 iteritems = items | |
1694 iterallitems = allitems | |
1695 | |
1696 else: | |
1697 def values(self): return [v[-1] for v in self.dict.values()] | |
1698 def items(self): return [(k, v[-1]) for k, v in self.dict.items()] | |
1699 def iterkeys(self): return self.dict.iterkeys() | |
1700 def itervalues(self): return (v[-1] for v in self.dict.itervalues()) | |
1701 def iteritems(self): | |
1702 return ((k, v[-1]) for k, v in self.dict.iteritems()) | |
1703 def iterallitems(self): | |
1704 return ((k, v) for k, vl in self.dict.iteritems() for v in vl) | |
1705 def allitems(self): | |
1706 return [(k, v) for k, vl in self.dict.iteritems() for v in vl] | |
1707 | |
1708 def get(self, key, default=None, index=-1, type=None): | |
1709 ''' Return the most recent value for a key. | |
1710 | |
1711 :param default: The default value to be returned if the key is not | |
1712 present or the type conversion fails. | |
1713 :param index: An index for the list of available values. | |
1714 :param type: If defined, this callable is used to cast the value | |
1715 into a specific type. Exception are suppressed and result in | |
1716 the default value to be returned. | |
1717 ''' | |
1718 try: | |
1719 val = self.dict[key][index] | |
1720 return type(val) if type else val | |
1721 except Exception: | |
1722 pass | |
1723 return default | |
1724 | |
1725 def append(self, key, value): | |
1726 ''' Add a new value to the list of values for this key. ''' | |
1727 self.dict.setdefault(key, []).append(value) | |
1728 | |
1729 def replace(self, key, value): | |
1730 ''' Replace the list of values with a single value. ''' | |
1731 self.dict[key] = [value] | |
1732 | |
1733 def getall(self, key): | |
1734 ''' Return a (possibly empty) list of values for a key. ''' | |
1735 return self.dict.get(key) or [] | |
1736 | |
1737 #: Aliases for WTForms to mimic other multi-dict APIs (Django) | |
1738 getone = get | |
1739 getlist = getall | |
1740 | |
1741 | |
1742 | |
1743 class FormsDict(MultiDict): | |
1744 ''' This :class:`MultiDict` subclass is used to store request form data. | |
1745 Additionally to the normal dict-like item access methods (which return | |
1746 unmodified data as native strings), this container also supports | |
1747 attribute-like access to its values. Attributes are automatically de- | |
1748 or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing | |
1749 attributes default to an empty string. ''' | |
1750 | |
1751 #: Encoding used for attribute values. | |
1752 input_encoding = 'utf8' | |
1753 #: If true (default), unicode strings are first encoded with `latin1` | |
1754 #: and then decoded to match :attr:`input_encoding`. | |
1755 recode_unicode = True | |
1756 | |
1757 def _fix(self, s, encoding=None): | |
1758 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI | |
1759 s = s.encode('latin1') | |
1760 if isinstance(s, bytes): # Python 2 WSGI | |
1761 return s.decode(encoding or self.input_encoding) | |
1762 return s | |
1763 | |
1764 def decode(self, encoding=None): | |
1765 ''' Returns a copy with all keys and values de- or recoded to match | |
1766 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a | |
1767 unicode dictionary. ''' | |
1768 copy = FormsDict() | |
1769 enc = copy.input_encoding = encoding or self.input_encoding | |
1770 copy.recode_unicode = False | |
1771 for key, value in self.allitems(): | |
1772 copy.append(self._fix(key, enc), self._fix(value, enc)) | |
1773 return copy | |
1774 | |
1775 def getunicode(self, name, default=None, encoding=None): | |
1776 try: | |
1777 return self._fix(self[name], encoding) | |
1778 except (UnicodeError, KeyError): | |
1779 return default | |
1780 | |
1781 def __getattr__(self, name, default=unicode()): | |
1782 # Without this guard, pickle generates a cryptic TypeError: | |
1783 if name.startswith('__') and name.endswith('__'): | |
1784 return super(FormsDict, self).__getattr__(name) | |
1785 return self.getunicode(name, default=default) | |
1786 | |
1787 | |
1788 class HeaderDict(MultiDict): | |
1789 """ A case-insensitive version of :class:`MultiDict` that defaults to | |
1790 replace the old value instead of appending it. """ | |
1791 | |
1792 def __init__(self, *a, **ka): | |
1793 self.dict = {} | |
1794 if a or ka: self.update(*a, **ka) | |
1795 | |
1796 def __contains__(self, key): return _hkey(key) in self.dict | |
1797 def __delitem__(self, key): del self.dict[_hkey(key)] | |
1798 def __getitem__(self, key): return self.dict[_hkey(key)][-1] | |
1799 def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)] | |
1800 def append(self, key, value): | |
1801 self.dict.setdefault(_hkey(key), []).append(str(value)) | |
1802 def replace(self, key, value): self.dict[_hkey(key)] = [str(value)] | |
1803 def getall(self, key): return self.dict.get(_hkey(key)) or [] | |
1804 def get(self, key, default=None, index=-1): | |
1805 return MultiDict.get(self, _hkey(key), default, index) | |
1806 def filter(self, names): | |
1807 for name in [_hkey(n) for n in names]: | |
1808 if name in self.dict: | |
1809 del self.dict[name] | |
1810 | |
1811 | |
1812 class WSGIHeaderDict(DictMixin): | |
1813 ''' This dict-like class wraps a WSGI environ dict and provides convenient | |
1814 access to HTTP_* fields. Keys and values are native strings | |
1815 (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI | |
1816 environment contains non-native string values, these are de- or encoded | |
1817 using a lossless 'latin1' character set. | |
1818 | |
1819 The API will remain stable even on changes to the relevant PEPs. | |
1820 Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one | |
1821 that uses non-native strings.) | |
1822 ''' | |
1823 #: List of keys that do not have a 'HTTP_' prefix. | |
1824 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') | |
1825 | |
1826 def __init__(self, environ): | |
1827 self.environ = environ | |
1828 | |
1829 def _ekey(self, key): | |
1830 ''' Translate header field name to CGI/WSGI environ key. ''' | |
1831 key = key.replace('-','_').upper() | |
1832 if key in self.cgikeys: | |
1833 return key | |
1834 return 'HTTP_' + key | |
1835 | |
1836 def raw(self, key, default=None): | |
1837 ''' Return the header value as is (may be bytes or unicode). ''' | |
1838 return self.environ.get(self._ekey(key), default) | |
1839 | |
1840 def __getitem__(self, key): | |
1841 return tonat(self.environ[self._ekey(key)], 'latin1') | |
1842 | |
1843 def __setitem__(self, key, value): | |
1844 raise TypeError("%s is read-only." % self.__class__) | |
1845 | |
1846 def __delitem__(self, key): | |
1847 raise TypeError("%s is read-only." % self.__class__) | |
1848 | |
1849 def __iter__(self): | |
1850 for key in self.environ: | |
1851 if key[:5] == 'HTTP_': | |
1852 yield key[5:].replace('_', '-').title() | |
1853 elif key in self.cgikeys: | |
1854 yield key.replace('_', '-').title() | |
1855 | |
1856 def keys(self): return [x for x in self] | |
1857 def __len__(self): return len(self.keys()) | |
1858 def __contains__(self, key): return self._ekey(key) in self.environ | |
1859 | |
1860 | |
1861 class ConfigDict(dict): | |
1862 ''' A dict-subclass with some extras: You can access keys like attributes. | |
1863 Uppercase attributes create new ConfigDicts and act as name-spaces. | |
1864 Other missing attributes return None. Calling a ConfigDict updates its | |
1865 values and returns itself. | |
1866 | |
1867 >>> cfg = ConfigDict() | |
1868 >>> cfg.Namespace.value = 5 | |
1869 >>> cfg.OtherNamespace(a=1, b=2) | |
1870 >>> cfg | |
1871 {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} | |
1872 ''' | |
1873 | |
1874 def __getattr__(self, key): | |
1875 if key not in self and key[0].isupper(): | |
1876 self[key] = ConfigDict() | |
1877 return self.get(key) | |
1878 | |
1879 def __setattr__(self, key, value): | |
1880 if hasattr(dict, key): | |
1881 raise AttributeError('Read-only attribute.') | |
1882 if key in self and self[key] and isinstance(self[key], ConfigDict): | |
1883 raise AttributeError('Non-empty namespace attribute.') | |
1884 self[key] = value | |
1885 | |
1886 def __delattr__(self, key): | |
1887 if key in self: del self[key] | |
1888 | |
1889 def __call__(self, *a, **ka): | |
1890 for key, value in dict(*a, **ka).items(): setattr(self, key, value) | |
1891 return self | |
1892 | |
1893 | |
1894 class AppStack(list): | |
1895 """ A stack-like list. Calling it returns the head of the stack. """ | |
1896 | |
1897 def __call__(self): | |
1898 """ Return the current default application. """ | |
1899 return self[-1] | |
1900 | |
1901 def push(self, value=None): | |
1902 """ Add a new :class:`Bottle` instance to the stack """ | |
1903 if not isinstance(value, Bottle): | |
1904 value = Bottle() | |
1905 self.append(value) | |
1906 return value | |
1907 | |
1908 | |
1909 class WSGIFileWrapper(object): | |
1910 | |
1911 def __init__(self, fp, buffer_size=1024*64): | |
1912 self.fp, self.buffer_size = fp, buffer_size | |
1913 for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): | |
1914 if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) | |
1915 | |
1916 def __iter__(self): | |
1917 buff, read = self.buffer_size, self.read | |
1918 while True: | |
1919 part = read(buff) | |
1920 if not part: return | |
1921 yield part | |
1922 | |
1923 | |
1924 class ResourceManager(object): | |
1925 ''' This class manages a list of search paths and helps to find and open | |
1926 aplication-bound resources (files). | |
1927 | |
1928 :param base: default value for same-named :meth:`add_path` parameter. | |
1929 :param opener: callable used to open resources. | |
1930 :param cachemode: controls which lookups are cached. One of 'all', | |
1931 'found' or 'none'. | |
1932 ''' | |
1933 | |
1934 def __init__(self, base='./', opener=open, cachemode='all'): | |
1935 self.opener = open | |
1936 self.base = base | |
1937 self.cachemode = cachemode | |
1938 | |
1939 #: A list of search paths. See :meth:`add_path` for details. | |
1940 self.path = [] | |
1941 #: A cache for resolved paths. `res.cache.clear()`` clears the cache. | |
1942 self.cache = {} | |
1943 | |
1944 def add_path(self, path, base=None, index=None, create=False): | |
1945 ''' Add a new path to the list of search paths. Return False if it does | |
1946 not exist. | |
1947 | |
1948 :param path: The new search path. Relative paths are turned into an | |
1949 absolute and normalized form. If the path looks like a file (not | |
1950 ending in `/`), the filename is stripped off. | |
1951 :param base: Path used to absolutize relative search paths. | |
1952 Defaults to `:attr:base` which defaults to ``./``. | |
1953 :param index: Position within the list of search paths. Defaults to | |
1954 last index (appends to the list). | |
1955 :param create: Create non-existent search paths. Off by default. | |
1956 | |
1957 The `base` parameter makes it easy to reference files installed | |
1958 along with a python module or package:: | |
1959 | |
1960 res.add_path('./resources/', __file__) | |
1961 ''' | |
1962 base = os.path.abspath(os.path.dirname(base or self.base)) | |
1963 path = os.path.abspath(os.path.join(base, os.path.dirname(path))) | |
1964 path += os.sep | |
1965 if path in self.path: | |
1966 self.path.remove(path) | |
1967 if create and not os.path.isdir(path): | |
1968 os.mkdirs(path) | |
1969 if index is None: | |
1970 self.path.append(path) | |
1971 else: | |
1972 self.path.insert(index, path) | |
1973 self.cache.clear() | |
1974 | |
1975 def __iter__(self): | |
1976 ''' Iterate over all existing files in all registered paths. ''' | |
1977 search = self.path[:] | |
1978 while search: | |
1979 path = search.pop() | |
1980 if not os.path.isdir(path): continue | |
1981 for name in os.listdir(path): | |
1982 full = os.path.join(path, name) | |
1983 if os.path.isdir(full): search.append(full) | |
1984 else: yield full | |
1985 | |
1986 def lookup(self, name): | |
1987 ''' Search for a resource and return an absolute file path, or `None`. | |
1988 | |
1989 The :attr:`path` list is searched in order. The first match is | |
1990 returend. Symlinks are followed. The result is cached to speed up | |
1991 future lookups. ''' | |
1992 if name not in self.cache or DEBUG: | |
1993 for path in self.path: | |
1994 fpath = os.path.join(path, name) | |
1995 if os.path.isfile(fpath): | |
1996 if self.cachemode in ('all', 'found'): | |
1997 self.cache[name] = fpath | |
1998 return fpath | |
1999 if self.cachemode == 'all': | |
2000 self.cache[name] = None | |
2001 return self.cache[name] | |
2002 | |
2003 def open(self, name, mode='r', *args, **kwargs): | |
2004 ''' Find a resource and return a file object, or raise IOError. ''' | |
2005 fname = self.lookup(name) | |
2006 if not fname: raise IOError("Resource %r not found." % name) | |
2007 return self.opener(name, mode=mode, *args, **kwargs) | |
2008 | |
2009 | |
2010 | |
2011 | |
2012 | |
2013 | |
2014 ############################################################################### | |
2015 # Application Helper ########################################################### | |
2016 ############################################################################### | |
2017 | |
2018 | |
2019 def abort(code=500, text='Unknown Error: Application stopped.'): | |
2020 """ Aborts execution and causes a HTTP error. """ | |
2021 raise HTTPError(code, text) | |
2022 | |
2023 | |
2024 def redirect(url, code=None): | |
2025 """ Aborts execution and causes a 303 or 302 redirect, depending on | |
2026 the HTTP protocol version. """ | |
2027 if code is None: | |
2028 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 | |
2029 location = urljoin(request.url, url) | |
2030 raise HTTPResponse("", status=code, header=dict(Location=location)) | |
2031 | |
2032 | |
2033 def _file_iter_range(fp, offset, bytes, maxread=1024*1024): | |
2034 ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' | |
2035 fp.seek(offset) | |
2036 while bytes > 0: | |
2037 part = fp.read(min(bytes, maxread)) | |
2038 if not part: break | |
2039 bytes -= len(part) | |
2040 yield part | |
2041 | |
2042 | |
2043 def static_file(filename, root, mimetype='auto', download=False): | |
2044 """ Open a file in a safe way and return :exc:`HTTPResponse` with status | |
2045 code 200, 305, 401 or 404. Set Content-Type, Content-Encoding, | |
2046 Content-Length and Last-Modified header. Obey If-Modified-Since header | |
2047 and HEAD requests. | |
2048 """ | |
2049 root = os.path.abspath(root) + os.sep | |
2050 filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) | |
2051 header = dict() | |
2052 | |
2053 if not filename.startswith(root): | |
2054 return HTTPError(403, "Access denied.") | |
2055 if not os.path.exists(filename) or not os.path.isfile(filename): | |
2056 return HTTPError(404, "File does not exist.") | |
2057 if not os.access(filename, os.R_OK): | |
2058 return HTTPError(403, "You do not have permission to access this file.") | |
2059 | |
2060 if mimetype == 'auto': | |
2061 mimetype, encoding = mimetypes.guess_type(filename) | |
2062 if mimetype: header['Content-Type'] = mimetype | |
2063 if encoding: header['Content-Encoding'] = encoding | |
2064 elif mimetype: | |
2065 header['Content-Type'] = mimetype | |
2066 | |
2067 if download: | |
2068 download = os.path.basename(filename if download == True else download) | |
2069 header['Content-Disposition'] = 'attachment; filename="%s"' % download | |
2070 | |
2071 stats = os.stat(filename) | |
2072 header['Content-Length'] = clen = stats.st_size | |
2073 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) | |
2074 header['Last-Modified'] = lm | |
2075 | |
2076 ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') | |
2077 if ims: | |
2078 ims = parse_date(ims.split(";")[0].strip()) | |
2079 if ims is not None and ims >= int(stats.st_mtime): | |
2080 header['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) | |
2081 return HTTPResponse(status=304, header=header) | |
2082 | |
2083 body = '' if request.method == 'HEAD' else open(filename, 'rb') | |
2084 | |
2085 header["Accept-Ranges"] = "bytes" | |
2086 ranges = request.environ.get('HTTP_RANGE') | |
2087 if 'HTTP_RANGE' in request.environ: | |
2088 ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) | |
2089 if not ranges: | |
2090 return HTTPError(416, "Requested Range Not Satisfiable") | |
2091 offset, end = ranges[0] | |
2092 header["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) | |
2093 header["Content-Length"] = str(end-offset) | |
2094 if body: body = _file_iter_range(body, offset, end-offset) | |
2095 return HTTPResponse(body, header=header, status=206) | |
2096 return HTTPResponse(body, header=header) | |
2097 | |
2098 | |
2099 | |
2100 | |
2101 | |
2102 | |
2103 ############################################################################### | |
2104 # HTTP Utilities and MISC (TODO) ############################################### | |
2105 ############################################################################### | |
2106 | |
2107 | |
2108 def debug(mode=True): | |
2109 """ Change the debug level. | |
2110 There is only one debug level supported at the moment.""" | |
2111 global DEBUG | |
2112 DEBUG = bool(mode) | |
2113 | |
2114 | |
2115 def parse_date(ims): | |
2116 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ | |
2117 try: | |
2118 ts = email.utils.parsedate_tz(ims) | |
2119 return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone | |
2120 except (TypeError, ValueError, IndexError, OverflowError): | |
2121 return None | |
2122 | |
2123 | |
2124 def parse_auth(header): | |
2125 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" | |
2126 try: | |
2127 method, data = header.split(None, 1) | |
2128 if method.lower() == 'basic': | |
2129 user, pwd = touni(base64.b64decode(tob(data))).split(':',1) | |
2130 return user, pwd | |
2131 except (KeyError, ValueError): | |
2132 return None | |
2133 | |
2134 def parse_range_header(header, maxlen=0): | |
2135 ''' Yield (start, end) ranges parsed from a HTTP Range header. Skip | |
2136 unsatisfiable ranges. The end index is non-inclusive.''' | |
2137 if not header or header[:6] != 'bytes=': return | |
2138 ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] | |
2139 for start, end in ranges: | |
2140 try: | |
2141 if not start: # bytes=-100 -> last 100 bytes | |
2142 start, end = max(0, maxlen-int(end)), maxlen | |
2143 elif not end: # bytes=100- -> all but the first 99 bytes | |
2144 start, end = int(start), maxlen | |
2145 else: # bytes=100-200 -> bytes 100-200 (inclusive) | |
2146 start, end = int(start), min(int(end)+1, maxlen) | |
2147 if 0 <= start < end <= maxlen: | |
2148 yield start, end | |
2149 except ValueError: | |
2150 pass | |
2151 | |
2152 def _lscmp(a, b): | |
2153 ''' Compares two strings in a cryptographically safe way: | |
2154 Runtime is not affected by length of common prefix. ''' | |
2155 return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) | |
2156 | |
2157 | |
2158 def cookie_encode(data, key): | |
2159 ''' Encode and sign a pickle-able object. Return a (byte) string ''' | |
2160 msg = base64.b64encode(pickle.dumps(data, -1)) | |
2161 sig = base64.b64encode(hmac.new(tob(key), msg).digest()) | |
2162 return tob('!') + sig + tob('?') + msg | |
2163 | |
2164 | |
2165 def cookie_decode(data, key): | |
2166 ''' Verify and decode an encoded string. Return an object or None.''' | |
2167 data = tob(data) | |
2168 if cookie_is_encoded(data): | |
2169 sig, msg = data.split(tob('?'), 1) | |
2170 if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): | |
2171 return pickle.loads(base64.b64decode(msg)) | |
2172 return None | |
2173 | |
2174 | |
2175 def cookie_is_encoded(data): | |
2176 ''' Return True if the argument looks like a encoded cookie.''' | |
2177 return bool(data.startswith(tob('!')) and tob('?') in data) | |
2178 | |
2179 | |
2180 def html_escape(string): | |
2181 ''' Escape HTML special characters ``&<>`` and quotes ``'"``. ''' | |
2182 return string.replace('&','&').replace('<','<').replace('>','>')\ | |
2183 .replace('"','"').replace("'",''') | |
2184 | |
2185 | |
2186 def html_quote(string): | |
2187 ''' Escape and quote a string to be used as an HTTP attribute.''' | |
2188 return '"%s"' % html_escape(string).replace('\n','%#10;')\ | |
2189 .replace('\r',' ').replace('\t','	') | |
2190 | |
2191 | |
2192 def yieldroutes(func): | |
2193 """ Return a generator for routes that match the signature (name, args) | |
2194 of the func parameter. This may yield more than one route if the function | |
2195 takes optional keyword arguments. The output is best described by example:: | |
2196 | |
2197 a() -> '/a' | |
2198 b(x, y) -> '/b/:x/:y' | |
2199 c(x, y=5) -> '/c/:x' and '/c/:x/:y' | |
2200 d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' | |
2201 """ | |
2202 import inspect # Expensive module. Only import if necessary. | |
2203 path = '/' + func.__name__.replace('__','/').lstrip('/') | |
2204 spec = inspect.getargspec(func) | |
2205 argc = len(spec[0]) - len(spec[3] or []) | |
2206 path += ('/:%s' * argc) % tuple(spec[0][:argc]) | |
2207 yield path | |
2208 for arg in spec[0][argc:]: | |
2209 path += '/:%s' % arg | |
2210 yield path | |
2211 | |
2212 | |
2213 def path_shift(script_name, path_info, shift=1): | |
2214 ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. | |
2215 | |
2216 :return: The modified paths. | |
2217 :param script_name: The SCRIPT_NAME path. | |
2218 :param script_name: The PATH_INFO path. | |
2219 :param shift: The number of path fragments to shift. May be negative to | |
2220 change the shift direction. (default: 1) | |
2221 ''' | |
2222 if shift == 0: return script_name, path_info | |
2223 pathlist = path_info.strip('/').split('/') | |
2224 scriptlist = script_name.strip('/').split('/') | |
2225 if pathlist and pathlist[0] == '': pathlist = [] | |
2226 if scriptlist and scriptlist[0] == '': scriptlist = [] | |
2227 if shift > 0 and shift <= len(pathlist): | |
2228 moved = pathlist[:shift] | |
2229 scriptlist = scriptlist + moved | |
2230 pathlist = pathlist[shift:] | |
2231 elif shift < 0 and shift >= -len(scriptlist): | |
2232 moved = scriptlist[shift:] | |
2233 pathlist = moved + pathlist | |
2234 scriptlist = scriptlist[:shift] | |
2235 else: | |
2236 empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' | |
2237 raise AssertionError("Cannot shift. Nothing left from %s" % empty) | |
2238 new_script_name = '/' + '/'.join(scriptlist) | |
2239 new_path_info = '/' + '/'.join(pathlist) | |
2240 if path_info.endswith('/') and pathlist: new_path_info += '/' | |
2241 return new_script_name, new_path_info | |
2242 | |
2243 | |
2244 def validate(**vkargs): | |
2245 """ | |
2246 Validates and manipulates keyword arguments by user defined callables. | |
2247 Handles ValueError and missing arguments by raising HTTPError(403). | |
2248 """ | |
2249 depr('Use route wildcard filters instead.') | |
2250 def decorator(func): | |
2251 @functools.wraps(func) | |
2252 def wrapper(*args, **kargs): | |
2253 for key, value in vkargs.items(): | |
2254 if key not in kargs: | |
2255 abort(403, 'Missing parameter: %s' % key) | |
2256 try: | |
2257 kargs[key] = value(kargs[key]) | |
2258 except ValueError: | |
2259 abort(403, 'Wrong parameter format for: %s' % key) | |
2260 return func(*args, **kargs) | |
2261 return wrapper | |
2262 return decorator | |
2263 | |
2264 | |
2265 def auth_basic(check, realm="private", text="Access denied"): | |
2266 ''' Callback decorator to require HTTP auth (basic). | |
2267 TODO: Add route(check_auth=...) parameter. ''' | |
2268 def decorator(func): | |
2269 def wrapper(*a, **ka): | |
2270 user, password = request.auth or (None, None) | |
2271 if user is None or not check(user, password): | |
2272 response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm | |
2273 return HTTPError(401, text) | |
2274 return func(*a, **ka) | |
2275 return wrapper | |
2276 return decorator | |
2277 | |
2278 | |
2279 # Shortcuts for common Bottle methods. | |
2280 # They all refer to the current default application. | |
2281 | |
2282 def make_default_app_wrapper(name): | |
2283 ''' Return a callable that relays calls to the current default app. ''' | |
2284 @functools.wraps(getattr(Bottle, name)) | |
2285 def wrapper(*a, **ka): | |
2286 return getattr(app(), name)(*a, **ka) | |
2287 return wrapper | |
2288 | |
2289 route = make_default_app_wrapper('route') | |
2290 get = make_default_app_wrapper('get') | |
2291 post = make_default_app_wrapper('post') | |
2292 put = make_default_app_wrapper('put') | |
2293 delete = make_default_app_wrapper('delete') | |
2294 error = make_default_app_wrapper('error') | |
2295 mount = make_default_app_wrapper('mount') | |
2296 hook = make_default_app_wrapper('hook') | |
2297 install = make_default_app_wrapper('install') | |
2298 uninstall = make_default_app_wrapper('uninstall') | |
2299 url = make_default_app_wrapper('get_url') | |
2300 | |
2301 | |
2302 | |
2303 | |
2304 | |
2305 | |
2306 | |
2307 ############################################################################### | |
2308 # Server Adapter ############################################################### | |
2309 ############################################################################### | |
2310 | |
2311 | |
2312 class ServerAdapter(object): | |
2313 quiet = False | |
2314 def __init__(self, host='127.0.0.1', port=8080, **config): | |
2315 self.options = config | |
2316 self.host = host | |
2317 self.port = int(port) | |
2318 | |
2319 def run(self, handler): # pragma: no cover | |
2320 pass | |
2321 | |
2322 def __repr__(self): | |
2323 args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()]) | |
2324 return "%s(%s)" % (self.__class__.__name__, args) | |
2325 | |
2326 | |
2327 class CGIServer(ServerAdapter): | |
2328 quiet = True | |
2329 def run(self, handler): # pragma: no cover | |
2330 from wsgiref.handlers import CGIHandler | |
2331 def fixed_environ(environ, start_response): | |
2332 environ.setdefault('PATH_INFO', '') | |
2333 return handler(environ, start_response) | |
2334 CGIHandler().run(fixed_environ) | |
2335 | |
2336 | |
2337 class FlupFCGIServer(ServerAdapter): | |
2338 def run(self, handler): # pragma: no cover | |
2339 import flup.server.fcgi | |
2340 self.options.setdefault('bindAddress', (self.host, self.port)) | |
2341 flup.server.fcgi.WSGIServer(handler, **self.options).run() | |
2342 | |
2343 | |
2344 class WSGIRefServer(ServerAdapter): | |
2345 def run(self, handler): # pragma: no cover | |
2346 from wsgiref.simple_server import make_server, WSGIRequestHandler | |
2347 if self.quiet: | |
2348 class QuietHandler(WSGIRequestHandler): | |
2349 def log_request(*args, **kw): pass | |
2350 self.options['handler_class'] = QuietHandler | |
2351 srv = make_server(self.host, self.port, handler, **self.options) | |
2352 srv.serve_forever() | |
2353 | |
2354 | |
2355 class CherryPyServer(ServerAdapter): | |
2356 def run(self, handler): # pragma: no cover | |
2357 from cherrypy import wsgiserver | |
2358 server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) | |
2359 try: | |
2360 server.start() | |
2361 finally: | |
2362 server.stop() | |
2363 | |
2364 | |
2365 class WaitressServer(ServerAdapter): | |
2366 def run(self, handler): | |
2367 from waitress import serve | |
2368 serve(handler, host=self.host, port=self.port) | |
2369 | |
2370 | |
2371 class PasteServer(ServerAdapter): | |
2372 def run(self, handler): # pragma: no cover | |
2373 from paste import httpserver | |
2374 if not self.quiet: | |
2375 from paste.translogger import TransLogger | |
2376 handler = TransLogger(handler) | |
2377 httpserver.serve(handler, host=self.host, port=str(self.port), | |
2378 **self.options) | |
2379 | |
2380 | |
2381 class MeinheldServer(ServerAdapter): | |
2382 def run(self, handler): | |
2383 from meinheld import server | |
2384 server.listen((self.host, self.port)) | |
2385 server.run(handler) | |
2386 | |
2387 | |
2388 class FapwsServer(ServerAdapter): | |
2389 """ Extremely fast webserver using libev. See http://www.fapws.org/ """ | |
2390 def run(self, handler): # pragma: no cover | |
2391 import fapws._evwsgi as evwsgi | |
2392 from fapws import base, config | |
2393 port = self.port | |
2394 if float(config.SERVER_IDENT[-2:]) > 0.4: | |
2395 # fapws3 silently changed its API in 0.5 | |
2396 port = str(port) | |
2397 evwsgi.start(self.host, port) | |
2398 # fapws3 never releases the GIL. Complain upstream. I tried. No luck. | |
2399 if 'BOTTLE_CHILD' in os.environ and not self.quiet: | |
2400 _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") | |
2401 _stderr(" (Fapws3 breaks python thread support)\n") | |
2402 evwsgi.set_base_module(base) | |
2403 def app(environ, start_response): | |
2404 environ['wsgi.multiprocess'] = False | |
2405 return handler(environ, start_response) | |
2406 evwsgi.wsgi_cb(('', app)) | |
2407 evwsgi.run() | |
2408 | |
2409 | |
2410 class TornadoServer(ServerAdapter): | |
2411 """ The super hyped asynchronous server by facebook. Untested. """ | |
2412 def run(self, handler): # pragma: no cover | |
2413 import tornado.wsgi, tornado.httpserver, tornado.ioloop | |
2414 container = tornado.wsgi.WSGIContainer(handler) | |
2415 server = tornado.httpserver.HTTPServer(container) | |
2416 server.listen(port=self.port) | |
2417 tornado.ioloop.IOLoop.instance().start() | |
2418 | |
2419 | |
2420 class AppEngineServer(ServerAdapter): | |
2421 """ Adapter for Google App Engine. """ | |
2422 quiet = True | |
2423 def run(self, handler): | |
2424 from google.appengine.ext.webapp import util | |
2425 # A main() function in the handler script enables 'App Caching'. | |
2426 # Lets makes sure it is there. This _really_ improves performance. | |
2427 module = sys.modules.get('__main__') | |
2428 if module and not hasattr(module, 'main'): | |
2429 module.main = lambda: util.run_wsgi_app(handler) | |
2430 util.run_wsgi_app(handler) | |
2431 | |
2432 | |
2433 class TwistedServer(ServerAdapter): | |
2434 """ Untested. """ | |
2435 def run(self, handler): | |
2436 from twisted.web import server, wsgi | |
2437 from twisted.python.threadpool import ThreadPool | |
2438 from twisted.internet import reactor | |
2439 thread_pool = ThreadPool() | |
2440 thread_pool.start() | |
2441 reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) | |
2442 factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) | |
2443 reactor.listenTCP(self.port, factory, interface=self.host) | |
2444 reactor.run() | |
2445 | |
2446 | |
2447 class DieselServer(ServerAdapter): | |
2448 """ Untested. """ | |
2449 def run(self, handler): | |
2450 from diesel.protocols.wsgi import WSGIApplication | |
2451 app = WSGIApplication(handler, port=self.port) | |
2452 app.run() | |
2453 | |
2454 | |
2455 class GeventServer(ServerAdapter): | |
2456 """ Untested. Options: | |
2457 | |
2458 * `monkey` (default: True) fixes the stdlib to use greenthreads. | |
2459 * `fast` (default: False) uses libevent's http server, but has some | |
2460 issues: No streaming, no pipelining, no SSL. | |
2461 """ | |
2462 def run(self, handler): | |
2463 from gevent import wsgi as wsgi_fast, pywsgi, monkey, local | |
2464 if self.options.get('monkey', True): | |
2465 if not threading.local is local.local: monkey.patch_all() | |
2466 wsgi = wsgi_fast if self.options.get('fast') else pywsgi | |
2467 log = None if self.quiet else 'default' | |
2468 wsgi.WSGIServer((self.host, self.port), handler, log=log).serve_forever() | |
2469 | |
2470 | |
2471 class GunicornServer(ServerAdapter): | |
2472 """ Untested. See http://gunicorn.org/configure.html for options. """ | |
2473 def run(self, handler): | |
2474 from gunicorn.app.base import Application | |
2475 | |
2476 config = {'bind': "%s:%d" % (self.host, int(self.port))} | |
2477 config.update(self.options) | |
2478 | |
2479 class GunicornApplication(Application): | |
2480 def init(self, parser, opts, args): | |
2481 return config | |
2482 | |
2483 def load(self): | |
2484 return handler | |
2485 | |
2486 GunicornApplication().run() | |
2487 | |
2488 | |
2489 class EventletServer(ServerAdapter): | |
2490 """ Untested """ | |
2491 def run(self, handler): | |
2492 from eventlet import wsgi, listen | |
2493 try: | |
2494 wsgi.server(listen((self.host, self.port)), handler, | |
2495 log_output=(not self.quiet)) | |
2496 except TypeError: | |
2497 # Fallback, if we have old version of eventlet | |
2498 wsgi.server(listen((self.host, self.port)), handler) | |
2499 | |
2500 | |
2501 class RocketServer(ServerAdapter): | |
2502 """ Untested. """ | |
2503 def run(self, handler): | |
2504 from rocket import Rocket | |
2505 server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) | |
2506 server.start() | |
2507 | |
2508 | |
2509 class BjoernServer(ServerAdapter): | |
2510 """ Fast server written in C: https://github.com/jonashaag/bjoern """ | |
2511 def run(self, handler): | |
2512 from bjoern import run | |
2513 run(handler, self.host, self.port) | |
2514 | |
2515 | |
2516 class AutoServer(ServerAdapter): | |
2517 """ Untested. """ | |
2518 adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] | |
2519 def run(self, handler): | |
2520 for sa in self.adapters: | |
2521 try: | |
2522 return sa(self.host, self.port, **self.options).run(handler) | |
2523 except ImportError: | |
2524 pass | |
2525 | |
2526 server_names = { | |
2527 'cgi': CGIServer, | |
2528 'flup': FlupFCGIServer, | |
2529 'wsgiref': WSGIRefServer, | |
2530 'waitress': WaitressServer, | |
2531 'cherrypy': CherryPyServer, | |
2532 'paste': PasteServer, | |
2533 'fapws3': FapwsServer, | |
2534 'tornado': TornadoServer, | |
2535 'gae': AppEngineServer, | |
2536 'twisted': TwistedServer, | |
2537 'diesel': DieselServer, | |
2538 'meinheld': MeinheldServer, | |
2539 'gunicorn': GunicornServer, | |
2540 'eventlet': EventletServer, | |
2541 'gevent': GeventServer, | |
2542 'rocket': RocketServer, | |
2543 'bjoern' : BjoernServer, | |
2544 'auto': AutoServer, | |
2545 } | |
2546 | |
2547 | |
2548 | |
2549 | |
2550 | |
2551 | |
2552 ############################################################################### | |
2553 # Application Control ########################################################## | |
2554 ############################################################################### | |
2555 | |
2556 | |
2557 def load(target, **namespace): | |
2558 """ Import a module or fetch an object from a module. | |
2559 | |
2560 * ``package.module`` returns `module` as a module object. | |
2561 * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. | |
2562 * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. | |
2563 | |
2564 The last form accepts not only function calls, but any type of | |
2565 expression. Keyword arguments passed to this function are available as | |
2566 local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` | |
2567 """ | |
2568 module, target = target.split(":", 1) if ':' in target else (target, None) | |
2569 if module not in sys.modules: __import__(module) | |
2570 if not target: return sys.modules[module] | |
2571 if target.isalnum(): return getattr(sys.modules[module], target) | |
2572 package_name = module.split('.')[0] | |
2573 namespace[package_name] = sys.modules[package_name] | |
2574 return eval('%s.%s' % (module, target), namespace) | |
2575 | |
2576 | |
2577 def load_app(target): | |
2578 """ Load a bottle application from a module and make sure that the import | |
2579 does not affect the current default application, but returns a separate | |
2580 application object. See :func:`load` for the target parameter. """ | |
2581 global NORUN; NORUN, nr_old = True, NORUN | |
2582 try: | |
2583 tmp = default_app.push() # Create a new "default application" | |
2584 rv = load(target) # Import the target module | |
2585 return rv if callable(rv) else tmp | |
2586 finally: | |
2587 default_app.remove(tmp) # Remove the temporary added default application | |
2588 NORUN = nr_old | |
2589 | |
2590 _debug = debug | |
2591 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, | |
2592 interval=1, reloader=False, quiet=False, plugins=None, | |
2593 debug=False, **kargs): | |
2594 """ Start a server instance. This method blocks until the server terminates. | |
2595 | |
2596 :param app: WSGI application or target string supported by | |
2597 :func:`load_app`. (default: :func:`default_app`) | |
2598 :param server: Server adapter to use. See :data:`server_names` keys | |
2599 for valid names or pass a :class:`ServerAdapter` subclass. | |
2600 (default: `wsgiref`) | |
2601 :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on | |
2602 all interfaces including the external one. (default: 127.0.0.1) | |
2603 :param port: Server port to bind to. Values below 1024 require root | |
2604 privileges. (default: 8080) | |
2605 :param reloader: Start auto-reloading server? (default: False) | |
2606 :param interval: Auto-reloader interval in seconds (default: 1) | |
2607 :param quiet: Suppress output to stdout and stderr? (default: False) | |
2608 :param options: Options passed to the server adapter. | |
2609 """ | |
2610 if NORUN: return | |
2611 if reloader and not os.environ.get('BOTTLE_CHILD'): | |
2612 try: | |
2613 lockfile = None | |
2614 fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') | |
2615 os.close(fd) # We only need this file to exist. We never write to it | |
2616 while os.path.exists(lockfile): | |
2617 args = [sys.executable] + sys.argv | |
2618 environ = os.environ.copy() | |
2619 environ['BOTTLE_CHILD'] = 'true' | |
2620 environ['BOTTLE_LOCKFILE'] = lockfile | |
2621 p = subprocess.Popen(args, env=environ) | |
2622 while p.poll() is None: # Busy wait... | |
2623 os.utime(lockfile, None) # I am alive! | |
2624 time.sleep(interval) | |
2625 if p.poll() != 3: | |
2626 if os.path.exists(lockfile): os.unlink(lockfile) | |
2627 sys.exit(p.poll()) | |
2628 except KeyboardInterrupt: | |
2629 pass | |
2630 finally: | |
2631 if os.path.exists(lockfile): | |
2632 os.unlink(lockfile) | |
2633 return | |
2634 | |
2635 try: | |
2636 _debug(debug) | |
2637 app = app or default_app() | |
2638 if isinstance(app, basestring): | |
2639 app = load_app(app) | |
2640 if not callable(app): | |
2641 raise ValueError("Application is not callable: %r" % app) | |
2642 | |
2643 for plugin in plugins or []: | |
2644 app.install(plugin) | |
2645 | |
2646 if server in server_names: | |
2647 server = server_names.get(server) | |
2648 if isinstance(server, basestring): | |
2649 server = load(server) | |
2650 if isinstance(server, type): | |
2651 server = server(host=host, port=port, **kargs) | |
2652 if not isinstance(server, ServerAdapter): | |
2653 raise ValueError("Unknown or unsupported server: %r" % server) | |
2654 | |
2655 server.quiet = server.quiet or quiet | |
2656 if not server.quiet: | |
2657 _stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server))) | |
2658 _stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) | |
2659 _stderr("Hit Ctrl-C to quit.\n\n") | |
2660 | |
2661 if reloader: | |
2662 lockfile = os.environ.get('BOTTLE_LOCKFILE') | |
2663 bgcheck = FileCheckerThread(lockfile, interval) | |
2664 with bgcheck: | |
2665 server.run(app) | |
2666 if bgcheck.status == 'reload': | |
2667 sys.exit(3) | |
2668 else: | |
2669 server.run(app) | |
2670 except KeyboardInterrupt: | |
2671 pass | |
2672 except (SystemExit, MemoryError): | |
2673 raise | |
2674 except: | |
2675 if not reloader: raise | |
2676 if not getattr(server, 'quiet', quiet): | |
2677 print_exc() | |
2678 time.sleep(interval) | |
2679 sys.exit(3) | |
2680 | |
2681 | |
2682 | |
2683 class FileCheckerThread(threading.Thread): | |
2684 ''' Interrupt main-thread as soon as a changed module file is detected, | |
2685 the lockfile gets deleted or gets to old. ''' | |
2686 | |
2687 def __init__(self, lockfile, interval): | |
2688 threading.Thread.__init__(self) | |
2689 self.lockfile, self.interval = lockfile, interval | |
2690 #: Is one of 'reload', 'error' or 'exit' | |
2691 self.status = None | |
2692 | |
2693 def run(self): | |
2694 exists = os.path.exists | |
2695 mtime = lambda path: os.stat(path).st_mtime | |
2696 files = dict() | |
2697 | |
2698 for module in list(sys.modules.values()): | |
2699 path = getattr(module, '__file__', '') | |
2700 if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] | |
2701 if path and exists(path): files[path] = mtime(path) | |
2702 | |
2703 while not self.status: | |
2704 if not exists(self.lockfile)\ | |
2705 or mtime(self.lockfile) < time.time() - self.interval - 5: | |
2706 self.status = 'error' | |
2707 thread.interrupt_main() | |
2708 for path, lmtime in list(files.items()): | |
2709 if not exists(path) or mtime(path) > lmtime: | |
2710 self.status = 'reload' | |
2711 thread.interrupt_main() | |
2712 break | |
2713 time.sleep(self.interval) | |
2714 | |
2715 def __enter__(self): | |
2716 self.start() | |
2717 | |
2718 def __exit__(self, exc_type, exc_val, exc_tb): | |
2719 if not self.status: self.status = 'exit' # silent exit | |
2720 self.join() | |
2721 return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) | |
2722 | |
2723 | |
2724 | |
2725 | |
2726 | |
2727 ############################################################################### | |
2728 # Template Adapters ############################################################ | |
2729 ############################################################################### | |
2730 | |
2731 | |
2732 class TemplateError(HTTPError): | |
2733 def __init__(self, message): | |
2734 HTTPError.__init__(self, 500, message) | |
2735 | |
2736 | |
2737 class BaseTemplate(object): | |
2738 """ Base class and minimal API for template adapters """ | |
2739 extensions = ['tpl','html','thtml','stpl'] | |
2740 settings = {} #used in prepare() | |
2741 defaults = {} #used in render() | |
2742 | |
2743 def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings): | |
2744 """ Create a new template. | |
2745 If the source parameter (str or buffer) is missing, the name argument | |
2746 is used to guess a template filename. Subclasses can assume that | |
2747 self.source and/or self.filename are set. Both are strings. | |
2748 The lookup, encoding and settings parameters are stored as instance | |
2749 variables. | |
2750 The lookup parameter stores a list containing directory paths. | |
2751 The encoding parameter should be used to decode byte strings or files. | |
2752 The settings parameter contains a dict for engine-specific settings. | |
2753 """ | |
2754 self.name = name | |
2755 self.source = source.read() if hasattr(source, 'read') else source | |
2756 self.filename = source.filename if hasattr(source, 'filename') else None | |
2757 self.lookup = [os.path.abspath(x) for x in lookup] | |
2758 self.encoding = encoding | |
2759 self.settings = self.settings.copy() # Copy from class variable | |
2760 self.settings.update(settings) # Apply | |
2761 if not self.source and self.name: | |
2762 self.filename = self.search(self.name, self.lookup) | |
2763 if not self.filename: | |
2764 raise TemplateError('Template %s not found.' % repr(name)) | |
2765 if not self.source and not self.filename: | |
2766 raise TemplateError('No template specified.') | |
2767 self.prepare(**self.settings) | |
2768 | |
2769 @classmethod | |
2770 def search(cls, name, lookup=[]): | |
2771 """ Search name in all directories specified in lookup. | |
2772 First without, then with common extensions. Return first hit. """ | |
2773 if os.path.isfile(name): return name | |
2774 for spath in lookup: | |
2775 fname = os.path.join(spath, name) | |
2776 if os.path.isfile(fname): | |
2777 return fname | |
2778 for ext in cls.extensions: | |
2779 if os.path.isfile('%s.%s' % (fname, ext)): | |
2780 return '%s.%s' % (fname, ext) | |
2781 | |
2782 @classmethod | |
2783 def global_config(cls, key, *args): | |
2784 ''' This reads or sets the global settings stored in class.settings. ''' | |
2785 if args: | |
2786 cls.settings = cls.settings.copy() # Make settings local to class | |
2787 cls.settings[key] = args[0] | |
2788 else: | |
2789 return cls.settings[key] | |
2790 | |
2791 def prepare(self, **options): | |
2792 """ Run preparations (parsing, caching, ...). | |
2793 It should be possible to call this again to refresh a template or to | |
2794 update settings. | |
2795 """ | |
2796 raise NotImplementedError | |
2797 | |
2798 def render(self, *args, **kwargs): | |
2799 """ Render the template with the specified local variables and return | |
2800 a single byte or unicode string. If it is a byte string, the encoding | |
2801 must match self.encoding. This method must be thread-safe! | |
2802 Local variables may be provided in dictionaries (*args) | |
2803 or directly, as keywords (**kwargs). | |
2804 """ | |
2805 raise NotImplementedError | |
2806 | |
2807 | |
2808 class MakoTemplate(BaseTemplate): | |
2809 def prepare(self, **options): | |
2810 from mako.template import Template | |
2811 from mako.lookup import TemplateLookup | |
2812 options.update({'input_encoding':self.encoding}) | |
2813 options.setdefault('format_exceptions', bool(DEBUG)) | |
2814 lookup = TemplateLookup(directories=self.lookup, **options) | |
2815 if self.source: | |
2816 self.tpl = Template(self.source, lookup=lookup, **options) | |
2817 else: | |
2818 self.tpl = Template(uri=self.name, filename=self.filename, lookup=lookup, **options) | |
2819 | |
2820 def render(self, *args, **kwargs): | |
2821 for dictarg in args: kwargs.update(dictarg) | |
2822 _defaults = self.defaults.copy() | |
2823 _defaults.update(kwargs) | |
2824 return self.tpl.render(**_defaults) | |
2825 | |
2826 | |
2827 class CheetahTemplate(BaseTemplate): | |
2828 def prepare(self, **options): | |
2829 from Cheetah.Template import Template | |
2830 self.context = threading.local() | |
2831 self.context.vars = {} | |
2832 options['searchList'] = [self.context.vars] | |
2833 if self.source: | |
2834 self.tpl = Template(source=self.source, **options) | |
2835 else: | |
2836 self.tpl = Template(file=self.filename, **options) | |
2837 | |
2838 def render(self, *args, **kwargs): | |
2839 for dictarg in args: kwargs.update(dictarg) | |
2840 self.context.vars.update(self.defaults) | |
2841 self.context.vars.update(kwargs) | |
2842 out = str(self.tpl) | |
2843 self.context.vars.clear() | |
2844 return out | |
2845 | |
2846 | |
2847 class Jinja2Template(BaseTemplate): | |
2848 def prepare(self, filters=None, tests=None, **kwargs): | |
2849 from jinja2 import Environment, FunctionLoader | |
2850 if 'prefix' in kwargs: # TODO: to be removed after a while | |
2851 raise RuntimeError('The keyword argument `prefix` has been removed. ' | |
2852 'Use the full jinja2 environment name line_statement_prefix instead.') | |
2853 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) | |
2854 if filters: self.env.filters.update(filters) | |
2855 if tests: self.env.tests.update(tests) | |
2856 if self.source: | |
2857 self.tpl = self.env.from_string(self.source) | |
2858 else: | |
2859 self.tpl = self.env.get_template(self.filename) | |
2860 | |
2861 def render(self, *args, **kwargs): | |
2862 for dictarg in args: kwargs.update(dictarg) | |
2863 _defaults = self.defaults.copy() | |
2864 _defaults.update(kwargs) | |
2865 return self.tpl.render(**_defaults) | |
2866 | |
2867 def loader(self, name): | |
2868 fname = self.search(name, self.lookup) | |
2869 if not fname: return | |
2870 with open(fname, "rb") as f: | |
2871 return f.read().decode(self.encoding) | |
2872 | |
2873 | |
2874 class SimpleTALTemplate(BaseTemplate): | |
2875 ''' Deprecated, do not use. ''' | |
2876 def prepare(self, **options): | |
2877 depr('The SimpleTAL template handler is deprecated'\ | |
2878 ' and will be removed in 0.12') | |
2879 from simpletal import simpleTAL | |
2880 if self.source: | |
2881 self.tpl = simpleTAL.compileHTMLTemplate(self.source) | |
2882 else: | |
2883 with open(self.filename, 'rb') as fp: | |
2884 self.tpl = simpleTAL.compileHTMLTemplate(tonat(fp.read())) | |
2885 | |
2886 def render(self, *args, **kwargs): | |
2887 from simpletal import simpleTALES | |
2888 for dictarg in args: kwargs.update(dictarg) | |
2889 context = simpleTALES.Context() | |
2890 for k,v in self.defaults.items(): | |
2891 context.addGlobal(k, v) | |
2892 for k,v in kwargs.items(): | |
2893 context.addGlobal(k, v) | |
2894 output = StringIO() | |
2895 self.tpl.expand(context, output) | |
2896 return output.getvalue() | |
2897 | |
2898 | |
2899 class SimpleTemplate(BaseTemplate): | |
2900 blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', | |
2901 'with', 'def', 'class') | |
2902 dedent_blocks = ('elif', 'else', 'except', 'finally') | |
2903 | |
2904 @lazy_attribute | |
2905 def re_pytokens(cls): | |
2906 ''' This matches comments and all kinds of quoted strings but does | |
2907 NOT match comments (#...) within quoted strings. (trust me) ''' | |
2908 return re.compile(r''' | |
2909 (''(?!')|""(?!")|'{6}|"{6} # Empty strings (all 4 types) | |
2910 |'(?:[^\\']|\\.)+?' # Single quotes (') | |
2911 |"(?:[^\\"]|\\.)+?" # Double quotes (") | |
2912 |'{3}(?:[^\\]|\\.|\n)+?'{3} # Triple-quoted strings (') | |
2913 |"{3}(?:[^\\]|\\.|\n)+?"{3} # Triple-quoted strings (") | |
2914 |\#.* # Comments | |
2915 )''', re.VERBOSE) | |
2916 | |
2917 def prepare(self, escape_func=html_escape, noescape=False, **kwargs): | |
2918 self.cache = {} | |
2919 enc = self.encoding | |
2920 self._str = lambda x: touni(x, enc) | |
2921 self._escape = lambda x: escape_func(touni(x, enc)) | |
2922 if noescape: | |
2923 self._str, self._escape = self._escape, self._str | |
2924 | |
2925 @classmethod | |
2926 def split_comment(cls, code): | |
2927 """ Removes comments (#...) from python code. """ | |
2928 if '#' not in code: return code | |
2929 #: Remove comments only (leave quoted strings as they are) | |
2930 subf = lambda m: '' if m.group(0)[0]=='#' else m.group(0) | |
2931 return re.sub(cls.re_pytokens, subf, code) | |
2932 | |
2933 @cached_property | |
2934 def co(self): | |
2935 return compile(self.code, self.filename or '<string>', 'exec') | |
2936 | |
2937 @cached_property | |
2938 def code(self): | |
2939 stack = [] # Current Code indentation | |
2940 lineno = 0 # Current line of code | |
2941 ptrbuffer = [] # Buffer for printable strings and token tuple instances | |
2942 codebuffer = [] # Buffer for generated python code | |
2943 multiline = dedent = oneline = False | |
2944 template = self.source or open(self.filename, 'rb').read() | |
2945 | |
2946 def yield_tokens(line): | |
2947 for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): | |
2948 if i % 2: | |
2949 if part.startswith('!'): yield 'RAW', part[1:] | |
2950 else: yield 'CMD', part | |
2951 else: yield 'TXT', part | |
2952 | |
2953 def flush(): # Flush the ptrbuffer | |
2954 if not ptrbuffer: return | |
2955 cline = '' | |
2956 for line in ptrbuffer: | |
2957 for token, value in line: | |
2958 if token == 'TXT': cline += repr(value) | |
2959 elif token == 'RAW': cline += '_str(%s)' % value | |
2960 elif token == 'CMD': cline += '_escape(%s)' % value | |
2961 cline += ', ' | |
2962 cline = cline[:-2] + '\\\n' | |
2963 cline = cline[:-2] | |
2964 if cline[:-1].endswith('\\\\\\\\\\n'): | |
2965 cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr' | |
2966 cline = '_printlist([' + cline + '])' | |
2967 del ptrbuffer[:] # Do this before calling code() again | |
2968 code(cline) | |
2969 | |
2970 def code(stmt): | |
2971 for line in stmt.splitlines(): | |
2972 codebuffer.append(' ' * len(stack) + line.strip()) | |
2973 | |
2974 for line in template.splitlines(True): | |
2975 lineno += 1 | |
2976 line = touni(line, self.encoding) | |
2977 sline = line.lstrip() | |
2978 if lineno <= 2: | |
2979 m = re.match(r"%\s*#.*coding[:=]\s*([-\w.]+)", sline) | |
2980 if m: self.encoding = m.group(1) | |
2981 if m: line = line.replace('coding','coding (removed)') | |
2982 if sline and sline[0] == '%' and sline[:2] != '%%': | |
2983 line = line.split('%',1)[1].lstrip() # Full line following the % | |
2984 cline = self.split_comment(line).strip() | |
2985 cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] | |
2986 flush() # You are actually reading this? Good luck, it's a mess :) | |
2987 if cmd in self.blocks or multiline: | |
2988 cmd = multiline or cmd | |
2989 dedent = cmd in self.dedent_blocks # "else:" | |
2990 if dedent and not oneline and not multiline: | |
2991 cmd = stack.pop() | |
2992 code(line) | |
2993 oneline = not cline.endswith(':') # "if 1: pass" | |
2994 multiline = cmd if cline.endswith('\\') else False | |
2995 if not oneline and not multiline: | |
2996 stack.append(cmd) | |
2997 elif cmd == 'end' and stack: | |
2998 code('#end(%s) %s' % (stack.pop(), line.strip()[3:])) | |
2999 elif cmd == 'include': | |
3000 p = cline.split(None, 2)[1:] | |
3001 if len(p) == 2: | |
3002 code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1])) | |
3003 elif p: | |
3004 code("_=_include(%s, _stdout)" % repr(p[0])) | |
3005 else: # Empty %include -> reverse of %rebase | |
3006 code("_printlist(_base)") | |
3007 elif cmd == 'rebase': | |
3008 p = cline.split(None, 2)[1:] | |
3009 if len(p) == 2: | |
3010 code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) | |
3011 elif p: | |
3012 code("globals()['_rebase']=(%s, {})" % repr(p[0])) | |
3013 else: | |
3014 code(line) | |
3015 else: # Line starting with text (not '%') or '%%' (escaped) | |
3016 if line.strip().startswith('%%'): | |
3017 line = line.replace('%%', '%', 1) | |
3018 ptrbuffer.append(yield_tokens(line)) | |
3019 flush() | |
3020 return '\n'.join(codebuffer) + '\n' | |
3021 | |
3022 def subtemplate(self, _name, _stdout, *args, **kwargs): | |
3023 for dictarg in args: kwargs.update(dictarg) | |
3024 if _name not in self.cache: | |
3025 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) | |
3026 return self.cache[_name].execute(_stdout, kwargs) | |
3027 | |
3028 def execute(self, _stdout, *args, **kwargs): | |
3029 for dictarg in args: kwargs.update(dictarg) | |
3030 env = self.defaults.copy() | |
3031 env.update({'_stdout': _stdout, '_printlist': _stdout.extend, | |
3032 '_include': self.subtemplate, '_str': self._str, | |
3033 '_escape': self._escape, 'get': env.get, | |
3034 'setdefault': env.setdefault, 'defined': env.__contains__}) | |
3035 env.update(kwargs) | |
3036 eval(self.co, env) | |
3037 if '_rebase' in env: | |
3038 subtpl, rargs = env['_rebase'] | |
3039 rargs['_base'] = _stdout[:] #copy stdout | |
3040 del _stdout[:] # clear stdout | |
3041 return self.subtemplate(subtpl,_stdout,rargs) | |
3042 return env | |
3043 | |
3044 def render(self, *args, **kwargs): | |
3045 """ Render the template using keyword arguments as local variables. """ | |
3046 for dictarg in args: kwargs.update(dictarg) | |
3047 stdout = [] | |
3048 self.execute(stdout, kwargs) | |
3049 return ''.join(stdout) | |
3050 | |
3051 | |
3052 def template(*args, **kwargs): | |
3053 ''' | |
3054 Get a rendered template as a string iterator. | |
3055 You can use a name, a filename or a template string as first parameter. | |
3056 Template rendering arguments can be passed as dictionaries | |
3057 or directly (as keyword arguments). | |
3058 ''' | |
3059 tpl = args[0] if args else None | |
3060 template_adapter = kwargs.pop('template_adapter', SimpleTemplate) | |
3061 if tpl not in TEMPLATES or DEBUG: | |
3062 settings = kwargs.pop('template_settings', {}) | |
3063 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) | |
3064 if isinstance(tpl, template_adapter): | |
3065 TEMPLATES[tpl] = tpl | |
3066 if settings: TEMPLATES[tpl].prepare(**settings) | |
3067 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: | |
3068 TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) | |
3069 else: | |
3070 TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) | |
3071 if not TEMPLATES[tpl]: | |
3072 abort(500, 'Template (%s) not found' % tpl) | |
3073 for dictarg in args[1:]: kwargs.update(dictarg) | |
3074 return TEMPLATES[tpl].render(kwargs) | |
3075 | |
3076 mako_template = functools.partial(template, template_adapter=MakoTemplate) | |
3077 cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) | |
3078 jinja2_template = functools.partial(template, template_adapter=Jinja2Template) | |
3079 simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate) | |
3080 | |
3081 | |
3082 def view(tpl_name, **defaults): | |
3083 ''' Decorator: renders a template for a handler. | |
3084 The handler can control its behavior like that: | |
3085 | |
3086 - return a dict of template vars to fill out the template | |
3087 - return something other than a dict and the view decorator will not | |
3088 process the template, but return the handler result as is. | |
3089 This includes returning a HTTPResponse(dict) to get, | |
3090 for instance, JSON with autojson or other castfilters. | |
3091 ''' | |
3092 def decorator(func): | |
3093 @functools.wraps(func) | |
3094 def wrapper(*args, **kwargs): | |
3095 result = func(*args, **kwargs) | |
3096 if isinstance(result, (dict, DictMixin)): | |
3097 tplvars = defaults.copy() | |
3098 tplvars.update(result) | |
3099 return template(tpl_name, **tplvars) | |
3100 return result | |
3101 return wrapper | |
3102 return decorator | |
3103 | |
3104 mako_view = functools.partial(view, template_adapter=MakoTemplate) | |
3105 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) | |
3106 jinja2_view = functools.partial(view, template_adapter=Jinja2Template) | |
3107 simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate) | |
3108 | |
3109 | |
3110 | |
3111 | |
3112 | |
3113 | |
3114 ############################################################################### | |
3115 # Constants and Globals ######################################################## | |
3116 ############################################################################### | |
3117 | |
3118 | |
3119 TEMPLATE_PATH = ['./', './views/'] | |
3120 TEMPLATES = {} | |
3121 DEBUG = False | |
3122 NORUN = False # If set, run() does nothing. Used by load_app() | |
3123 | |
3124 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') | |
3125 HTTP_CODES = httplib.responses | |
3126 HTTP_CODES[418] = "I'm a teapot" # RFC 2324 | |
3127 HTTP_CODES[428] = "Precondition Required" | |
3128 HTTP_CODES[429] = "Too Many Requests" | |
3129 HTTP_CODES[431] = "Request Header Fields Too Large" | |
3130 HTTP_CODES[511] = "Network Authentication Required" | |
3131 _HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) | |
3132 | |
3133 #: The default template used for error pages. Override with @error() | |
3134 ERROR_PAGE_TEMPLATE = """ | |
3135 %%try: | |
3136 %%from %s import DEBUG, HTTP_CODES, request, touni | |
3137 %%status_name = HTTP_CODES.get(e.status, 'Unknown').title() | |
3138 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> | |
3139 <html> | |
3140 <head> | |
3141 <title>Error {{e.status}}: {{status_name}}</title> | |
3142 <style type="text/css"> | |
3143 html {background-color: #eee; font-family: sans;} | |
3144 body {background-color: #fff; border: 1px solid #ddd; | |
3145 padding: 15px; margin: 15px;} | |
3146 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} | |
3147 </style> | |
3148 </head> | |
3149 <body> | |
3150 <h1>Error {{e.status}}: {{status_name}}</h1> | |
3151 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> | |
3152 caused an error:</p> | |
3153 <pre>{{e.output}}</pre> | |
3154 %%if DEBUG and e.exception: | |
3155 <h2>Exception:</h2> | |
3156 <pre>{{repr(e.exception)}}</pre> | |
3157 %%end | |
3158 %%if DEBUG and e.traceback: | |
3159 <h2>Traceback:</h2> | |
3160 <pre>{{e.traceback}}</pre> | |
3161 %%end | |
3162 </body> | |
3163 </html> | |
3164 %%except ImportError: | |
3165 <b>ImportError:</b> Could not generate the error page. Please add bottle to | |
3166 the import path. | |
3167 %%end | |
3168 """ % __name__ | |
3169 | |
3170 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a | |
3171 #: request callback, this instance always refers to the *current* request | |
3172 #: (even on a multithreaded server). | |
3173 request = LocalRequest() | |
3174 | |
3175 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the | |
3176 #: HTTP response for the *current* request. | |
3177 response = LocalResponse() | |
3178 | |
3179 #: A thread-safe namespace. Not used by Bottle. | |
3180 local = threading.local() | |
3181 | |
3182 # Initialize app stack (create first empty Bottle app) | |
3183 # BC: 0.6.4 and needed for run() | |
3184 app = default_app = AppStack() | |
3185 app.push() | |
3186 | |
3187 #: A virtual package that redirects import statements. | |
3188 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. | |
3189 ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module | |
3190 | |
3191 if __name__ == '__main__': | |
3192 opt, args, parser = _cmd_options, _cmd_args, _cmd_parser | |
3193 if opt.version: | |
3194 _stdout('Bottle %s\n'%__version__) | |
3195 sys.exit(0) | |
3196 if not args: | |
3197 parser.print_help() | |
3198 _stderr('\nError: No application specified.\n') | |
3199 sys.exit(1) | |
3200 | |
3201 sys.path.insert(0, '.') | |
3202 sys.modules.setdefault('bottle', sys.modules['__main__']) | |
3203 | |
3204 host, port = (opt.bind or 'localhost'), 8080 | |
3205 if ':' in host: | |
3206 host, port = host.rsplit(':', 1) | |
3207 | |
3208 run(args[0], host=host, port=port, server=opt.server, | |
3209 reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) | |
3210 | |
3211 | |
3212 | |
3213 | |
3214 # THE END |