##// END OF EJS Templates
Task #715: API del ABS, abs-module scripts...
Fiorella Quino -
r170:3cd43b7dc4dd
parent child
Show More
@@ -0,0 +1,87
1 """
2 This script should run in the abs module embedded system
3 It creates a Web Application with API Restful Bottle to connect to SIR server.
4 It needs the following scripts: abs_gpio.py and bottle.py
5 """
6 from bottle import route, run, request
7 from bottle import error
8 from abs_gpio import abs_read
9
10 #Sockets
11 import socket
12 import sys
13
14 import json
15
16 module_ip = '192.168.1.xx'
17 module_num = 'xx'
18 module_port = 5500
19 module_xx = ('192.168.1.xx',5500)
20
21 @route('/')
22 @route('/hello')
23 def hello():
24 return "Hello World!"
25
26
27 #---- Get Bits Function ----
28 @route('/read', method='GET')
29 def readbits():
30
31 """
32 This function reads the real values from the embedded system pins
33 with gpio class
34 """
35
36 #This function reads sent bits.
37
38 #------Get Monitoring Values------
39 #----------------UP---------------
40 um2_value = abs_read(80) #(++)
41 um1_value = abs_read(82)
42 um0_value = abs_read(84) #(--)
43 #--------------DOWN---------------
44 dm2_value = abs_read(94) #(++)
45 dm1_value = abs_read(88)
46 dm0_value = abs_read(86) #(--)
47
48 allbits = [um2_value, um1_value, um0_value, dm2_value, dm1_value, dm0_value]
49 if "" not in allbits:
50 allbits = {"um2":int(um2_value), "um1": int(um1_value), "um0": int(um0_value), "dm2": int(dm2_value), "dm1": int(dm1_value), "dm0": int(dm0_value)}
51 #allbits = {"ub0":0, "ub1":0, "ub2":0, "db0":0, "db1":0, "db2":0}
52 return {"status": 1, "message": "Bits were successfully read", "allbits" : allbits}
53 else:
54 return {"status": 0, "message": "There's a problem reading bits", "allbits" : ""}
55
56 @route('/configure', method='POST')
57 def configurebits():
58 """
59 This function sends configurations to the module tcp_
60 """
61 try:
62 header_rx = request.forms.get('header')
63 module_rx = request.forms.get('module')
64 beams_rx = request.forms.get('beams')
65 beams_rx = json.loads(beams_rx)
66 except:
67 return {"status":0, "message": "Could not accept configuration"}
68 #print header_rx, module_rx, beams_rx
69 message_tx = header_rx+"\n"+module_rx+"\n"
70 for i in range(1,len(beams_rx)+1):
71 message_tx = message_tx+beams_rx[str(i)]+"\n"
72 message_tx = message_tx+"0"
73
74 # Create the datagram socket
75 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
76 print >>sys.stderr, 'sending "%s"' % message_tx
77 sock.connect(module_63)
78 sock.send(message_tx)
79 sock.close()
80
81 return {"status":1, "message": "Configuration has been successfully sent"}
82
83 @error(404)
84 def error404(error):
85 return "^^' Nothing here, try again."
86
87 run(host=module_ip, port=8080, debug=True)
@@ -0,0 +1,137
1 """
2 This script should run in the abs module embedded system.
3 It uses gpio class to write or read the pins values.
4 """
5
6 import sys
7 import time
8
9
10 try:
11 #------------------Write-OUT----------------
12 #----------------DOWN-------------------
13 #GPIO116-PIN37-PC20 (--)
14 pin = open("/sys/class/gpio/export","w")
15 pin.write(str(116))
16 pin.close()
17 #GPIO118-PIN39-PC22
18 pin = open("/sys/class/gpio/export","w")
19 pin.write(str(118))
20 pin.close()
21 #GPIO120-PIN41-PC24 (++)
22 pin = open("/sys/class/gpio/export","w")
23 pin.write(str(120))
24 pin.close()
25
26 #-----------------UP--------------------
27 #GPIO122-PIN43-PC26 (--)
28 pin = open("/sys/class/gpio/export","w")
29 pin.write(str(122))
30 pin.close()
31 #GPIO124-PIN45-PC28
32 pin = open("/sys/class/gpio/export","w")
33 pin.write(str(124))
34 pin.close()
35 #GPIO126-PIN47-PC30 (++)
36 pin = open("/sys/class/gpio/export","w")
37 pin.write(str(126))
38 pin.close()
39 #--------------DIRECTION----------------
40 #----------------DOWN-------------------
41 pin_direct = open("/sys/class/gpio/gpio116/direction","w")
42 pin_direct.write("out")
43 pin_direct.close()
44
45 pin_direct = open("/sys/class/gpio/gpio118/direction","w")
46 pin_direct.write("out")
47 pin_direct.close()
48
49 pin_direct = open("/sys/class/gpio/gpio120/direction","w")
50 pin_direct.write("out")
51 pin_direct.close()
52 #-----------------UP--------------------
53 pin_direct = open("/sys/class/gpio/gpio122/direction","w")
54 pin_direct.write("out")
55 pin_direct.close()
56
57 pin_direct = open("/sys/class/gpio/gpio124/direction","w")
58 pin_direct.write("out")
59 pin_direct.close()
60
61 pin_direct = open("/sys/class/gpio/gpio126/direction","w")
62 pin_direct.write("out")
63 pin_direct.close()
64 #------------------Read-IN------------------
65 #----------------DOWN-------------------
66 #GPIO86-PIN17-PB22 (--)
67 pin = open("/sys/class/gpio/export","w")
68 pin.write(str(86))
69 pin.close()
70 #GPIO88-PIN19-PB24
71 pin = open("/sys/class/gpio/export","w")
72 pin.write(str(88))
73 pin.close()
74 #GPIO94-PIN21-PB30 (++)
75 pin = open("/sys/class/gpio/export","w")
76 pin.write(str(94))
77 pin.close()
78 #-----------------UP--------------------
79 #GPIO84-PIN15-PB20 (--)
80 pin = open("/sys/class/gpio/export","w")
81 pin.write(str(84))
82 pin.close()
83 #GPIO82-PIN13-PB18
84 pin = open("/sys/class/gpio/export","w")
85 pin.write(str(82))
86 pin.close()
87 #GPIO80-PIN11-PB16 (++)
88 pin = open("/sys/class/gpio/export","w")
89 pin.write(str(80))
90 pin.close()
91 #--------------DIRECTION----------------
92 #----------------DOWN-------------------
93 pin_direct = open("/sys/class/gpio/gpio86/direction","w")
94 pin_direct.write("in")
95 pin_direct.close()
96
97 pin_direct = open("/sys/class/gpio/gpio88/direction","w")
98 pin_direct.write("in")
99 pin_direct.close()
100
101 pin_direct = open("/sys/class/gpio/gpio94/direction","w")
102 pin_direct.write("in")
103 pin_direct.close()
104 #-----------------UP--------------------
105 pin_direct = open("/sys/class/gpio/gpio84/direction","w")
106 pin_direct.write("in")
107 pin_direct.close()
108
109 pin_direct = open("/sys/class/gpio/gpio82/direction","w")
110 pin_direct.write("in")
111 pin_direct.close()
112
113 pin_direct = open("/sys/class/gpio/gpio80/direction","w")
114 pin_direct.write("in")
115 pin_direct.close()
116
117 except:
118 pass
119
120 def abs_write(address,value):
121 try:
122 pin_value = open("/sys/class/gpio/gpio"+str(address)+"/value","w")
123 pin_value.write(str(value))
124 pin_value.close()
125 return 1
126 except:
127 return 0
128 #return 1
129
130 def abs_read(address):
131 try:
132 pin_value = open("/sys/class/gpio/gpio"+str(address)+"/value","r")
133 valor = pin_value.read()
134 pin_value.close()
135 return valor
136 except:
137 return ""
This diff has been collapsed as it changes many lines, (4179 lines changed) Show them Hide them
@@ -0,0 +1,4179
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) 2015, Marcel Hellkamp.
13 License: MIT (see LICENSE for details)
14 """
15
16 from __future__ import with_statement
17 import sys
18
19 __author__ = 'Marcel Hellkamp'
20 __version__ = '0.13-dev'
21 __license__ = 'MIT'
22
23 ###############################################################################
24 # Command-line interface ########################################################
25 ###############################################################################
26 # INFO: Some server adapters need to monkey-patch std-lib modules before they
27 # are imported. This is why some of the command-line handling is done here, but
28 # the actual call to main() is at the end of the file.
29
30
31 def _cli_parse(args):
32 from optparse import OptionParser
33 parser = OptionParser(
34 usage="usage: %prog [options] package.module:app")
35 opt = parser.add_option
36 opt("--version", action="store_true", help="show version number.")
37 opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
38 opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
39 opt("-p", "--plugin", action="append", help="install additional plugin/s.")
40 opt("-c", "--conf", action="append", metavar="FILE",
41 help="load config values from FILE.")
42 opt("-C", "--param", action="append", metavar="NAME=VALUE",
43 help="override config values.")
44 opt("--debug", action="store_true", help="start server in debug mode.")
45 opt("--reload", action="store_true", help="auto-reload on file changes.")
46 opts, args = parser.parse_args(args[1:])
47
48 return opts, args, parser
49
50
51 def _cli_patch(args):
52 opts, _, _ = _cli_parse(args)
53 if opts.server:
54 if opts.server.startswith('gevent'):
55 import gevent.monkey
56 gevent.monkey.patch_all()
57 elif opts.server.startswith('eventlet'):
58 import eventlet
59 eventlet.monkey_patch()
60
61
62 if __name__ == '__main__':
63 _cli_patch(sys.argv)
64
65 ###############################################################################
66 # Imports and Python 2/3 unification ###########################################
67 ###############################################################################
68
69
70 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\
71 os, re, tempfile, threading, time, warnings, hashlib
72
73 from types import FunctionType
74 from datetime import date as datedate, datetime, timedelta
75 from tempfile import TemporaryFile
76 from traceback import format_exc, print_exc
77 from unicodedata import normalize
78
79 # inspect.getargspec was removed in Python 3.6, use
80 # Signature-based version where we can (Python 3.3+)
81 try:
82 from inspect import signature
83 def getargspec(func):
84 params = signature(func).parameters
85 args, varargs, keywords, defaults = [], None, None, []
86 for name, param in params.items():
87 if param.kind == param.VAR_POSITIONAL:
88 varargs = name
89 elif param.kind == param.VAR_KEYWORD:
90 keywords = name
91 else:
92 args.append(name)
93 if param.default is not param.empty:
94 defaults.append(param.default)
95 return (args, varargs, keywords, tuple(defaults) or None)
96 except ImportError:
97 try:
98 from inspect import getfullargspec
99 def getargspec(func):
100 spec = getfullargspec(func)
101 kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs)
102 return kwargs, spec[1], spec[2], spec[3]
103 except ImportError:
104 from inspect import getargspec
105
106 try:
107 from simplejson import dumps as json_dumps, loads as json_lds
108 except ImportError: # pragma: no cover
109 try:
110 from json import dumps as json_dumps, loads as json_lds
111 except ImportError:
112 try:
113 from django.utils.simplejson import dumps as json_dumps, loads as json_lds
114 except ImportError:
115
116 def json_dumps(data):
117 raise ImportError(
118 "JSON support requires Python 2.6 or simplejson.")
119
120 json_lds = json_dumps
121
122 # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities.
123 # It ain't pretty but it works... Sorry for the mess.
124
125 py = sys.version_info
126 py3k = py >= (3, 0, 0)
127 py25 = py < (2, 6, 0)
128 py31 = (3, 1, 0) <= py < (3, 2, 0)
129
130 # Workaround for the missing "as" keyword in py3k.
131 def _e():
132 return sys.exc_info()[1]
133
134 # Workaround for the "print is a keyword/function" Python 2/3 dilemma
135 # and a fallback for mod_wsgi (resticts stdout/err attribute access)
136 try:
137 _stdout, _stderr = sys.stdout.write, sys.stderr.write
138 except IOError:
139 _stdout = lambda x: sys.stdout.write(x)
140 _stderr = lambda x: sys.stderr.write(x)
141
142 # Lots of stdlib and builtin differences.
143 if py3k:
144 import http.client as httplib
145 import _thread as thread
146 from urllib.parse import urljoin, SplitResult as UrlSplitResult
147 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
148 urlunquote = functools.partial(urlunquote, encoding='latin1')
149 from http.cookies import SimpleCookie
150 from collections import MutableMapping as DictMixin
151 import pickle
152 from io import BytesIO
153 from configparser import ConfigParser, Error as ConfigParserError
154 basestring = str
155 unicode = str
156 json_loads = lambda s: json_lds(touni(s))
157 callable = lambda x: hasattr(x, '__call__')
158 imap = map
159
160 def _raise(*a):
161 raise a[0](a[1]).with_traceback(a[2])
162 else: # 2.x
163 import httplib
164 import thread
165 from urlparse import urljoin, SplitResult as UrlSplitResult
166 from urllib import urlencode, quote as urlquote, unquote as urlunquote
167 from Cookie import SimpleCookie
168 from itertools import imap
169 import cPickle as pickle
170 from StringIO import StringIO as BytesIO
171 from ConfigParser import SafeConfigParser as ConfigParser, \
172 Error as ConfigParserError
173 if py25:
174 from UserDict import DictMixin
175
176 def next(it):
177 return it.next()
178
179 bytes = str
180 else: # 2.6, 2.7
181 from collections import MutableMapping as DictMixin
182 unicode = unicode
183 json_loads = json_lds
184 eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
185
186 if py25 or py31:
187 msg = "Python 2.5 and 3.1 support will be dropped in future versions of Bottle."
188 warnings.warn(msg, DeprecationWarning)
189
190 # Some helpers for string/byte handling
191 def tob(s, enc='utf8'):
192 return s.encode(enc) if isinstance(s, unicode) else bytes(s)
193
194
195 def touni(s, enc='utf8', err='strict'):
196 if isinstance(s, bytes):
197 return s.decode(enc, err)
198 else:
199 return unicode(s or ("" if s is None else s))
200
201
202 tonat = touni if py3k else tob
203
204 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense).
205 # 3.1 needs a workaround.
206 if py31:
207 from io import TextIOWrapper
208
209 class NCTextIOWrapper(TextIOWrapper):
210 def close(self):
211 pass # Keep wrapped buffer open.
212
213
214 # A bug in functools causes it to break if the wrapper is an instance method
215 def update_wrapper(wrapper, wrapped, *a, **ka):
216 try:
217 functools.update_wrapper(wrapper, wrapped, *a, **ka)
218 except AttributeError:
219 pass
220
221 # These helpers are used at module level and need to be defined first.
222 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
223
224
225 def depr(major, minor, cause, fix):
226 text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\
227 "Cause: %s\n"\
228 "Fix: %s\n" % (major, minor, cause, fix)
229 if DEBUG == 'strict':
230 raise DeprecationWarning(text)
231 warnings.warn(text, DeprecationWarning, stacklevel=3)
232 return DeprecationWarning(text)
233
234
235 def makelist(data): # This is just too handy
236 if isinstance(data, (tuple, list, set, dict)):
237 return list(data)
238 elif data:
239 return [data]
240 else:
241 return []
242
243
244 class DictProperty(object):
245 """ Property that maps to a key in a local dict-like attribute. """
246
247 def __init__(self, attr, key=None, read_only=False):
248 self.attr, self.key, self.read_only = attr, key, read_only
249
250 def __call__(self, func):
251 functools.update_wrapper(self, func, updated=[])
252 self.getter, self.key = func, self.key or func.__name__
253 return self
254
255 def __get__(self, obj, cls):
256 if obj is None: return self
257 key, storage = self.key, getattr(obj, self.attr)
258 if key not in storage: storage[key] = self.getter(obj)
259 return storage[key]
260
261 def __set__(self, obj, value):
262 if self.read_only: raise AttributeError("Read-Only property.")
263 getattr(obj, self.attr)[self.key] = value
264
265 def __delete__(self, obj):
266 if self.read_only: raise AttributeError("Read-Only property.")
267 del getattr(obj, self.attr)[self.key]
268
269
270 class cached_property(object):
271 """ A property that is only computed once per instance and then replaces
272 itself with an ordinary attribute. Deleting the attribute resets the
273 property. """
274
275 def __init__(self, func):
276 self.__doc__ = getattr(func, '__doc__')
277 self.func = func
278
279 def __get__(self, obj, cls):
280 if obj is None: return self
281 value = obj.__dict__[self.func.__name__] = self.func(obj)
282 return value
283
284
285 class lazy_attribute(object):
286 """ A property that caches itself to the class object. """
287
288 def __init__(self, func):
289 functools.update_wrapper(self, func, updated=[])
290 self.getter = func
291
292 def __get__(self, obj, cls):
293 value = self.getter(cls)
294 setattr(cls, self.__name__, value)
295 return value
296
297 ###############################################################################
298 # Exceptions and Events ########################################################
299 ###############################################################################
300
301
302 class BottleException(Exception):
303 """ A base class for exceptions used by bottle. """
304 pass
305
306 ###############################################################################
307 # Routing ######################################################################
308 ###############################################################################
309
310
311 class RouteError(BottleException):
312 """ This is a base class for all routing related exceptions """
313
314
315 class RouteReset(BottleException):
316 """ If raised by a plugin or request handler, the route is reset and all
317 plugins are re-applied. """
318
319
320 class RouterUnknownModeError(RouteError):
321
322 pass
323
324
325 class RouteSyntaxError(RouteError):
326 """ The route parser found something not supported by this router. """
327
328
329 class RouteBuildError(RouteError):
330 """ The route could not be built. """
331
332
333 def _re_flatten(p):
334 """ Turn all capturing groups in a regular expression pattern into
335 non-capturing groups. """
336 if '(' not in p:
337 return p
338 return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if
339 len(m.group(1)) % 2 else m.group(1) + '(?:', p)
340
341
342 class Router(object):
343 """ A Router is an ordered collection of route->target pairs. It is used to
344 efficiently match WSGI requests against a number of routes and return
345 the first target that satisfies the request. The target may be anything,
346 usually a string, ID or callable object. A route consists of a path-rule
347 and a HTTP method.
348
349 The path-rule is either a static path (e.g. `/contact`) or a dynamic
350 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
351 and details on the matching order are described in docs:`routing`.
352 """
353
354 default_pattern = '[^/]+'
355 default_filter = 're'
356
357 #: The current CPython regexp implementation does not allow more
358 #: than 99 matching groups per regular expression.
359 _MAX_GROUPS_PER_PATTERN = 99
360
361 def __init__(self, strict=False):
362 self.rules = [] # All rules in order
363 self._groups = {} # index of regexes to find them in dyna_routes
364 self.builder = {} # Data structure for the url builder
365 self.static = {} # Search structure for static routes
366 self.dyna_routes = {}
367 self.dyna_regexes = {} # Search structure for dynamic routes
368 #: If true, static routes are no longer checked first.
369 self.strict_order = strict
370 self.filters = {
371 're': lambda conf: (_re_flatten(conf or self.default_pattern),
372 None, None),
373 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))),
374 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))),
375 'path': lambda conf: (r'.+?', None, None)
376 }
377
378 def add_filter(self, name, func):
379 """ Add a filter. The provided function is called with the configuration
380 string as parameter and must return a (regexp, to_python, to_url) tuple.
381 The first element is a string, the last two are callables or None. """
382 self.filters[name] = func
383
384 rule_syntax = re.compile('(\\\\*)'
385 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'
386 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'
387 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))')
388
389 def _itertokens(self, rule):
390 offset, prefix = 0, ''
391 for match in self.rule_syntax.finditer(rule):
392 prefix += rule[offset:match.start()]
393 g = match.groups()
394 if g[2] is not None:
395 depr(0, 13, "Use of old route syntax.",
396 "Use <name> instead of :name in routes.")
397 if len(g[0]) % 2: # Escaped wildcard
398 prefix += match.group(0)[len(g[0]):]
399 offset = match.end()
400 continue
401 if prefix:
402 yield prefix, None, None
403 name, filtr, conf = g[4:7] if g[2] is None else g[1:4]
404 yield name, filtr or 'default', conf or None
405 offset, prefix = match.end(), ''
406 if offset <= len(rule) or prefix:
407 yield prefix + rule[offset:], None, None
408
409 def add(self, rule, method, target, name=None):
410 """ Add a new rule or replace the target for an existing rule. """
411 anons = 0 # Number of anonymous wildcards found
412 keys = [] # Names of keys
413 pattern = '' # Regular expression pattern with named groups
414 filters = [] # Lists of wildcard input filters
415 builder = [] # Data structure for the URL builder
416 is_static = True
417
418 for key, mode, conf in self._itertokens(rule):
419 if mode:
420 is_static = False
421 if mode == 'default': mode = self.default_filter
422 mask, in_filter, out_filter = self.filters[mode](conf)
423 if not key:
424 pattern += '(?:%s)' % mask
425 key = 'anon%d' % anons
426 anons += 1
427 else:
428 pattern += '(?P<%s>%s)' % (key, mask)
429 keys.append(key)
430 if in_filter: filters.append((key, in_filter))
431 builder.append((key, out_filter or str))
432 elif key:
433 pattern += re.escape(key)
434 builder.append((None, key))
435
436 self.builder[rule] = builder
437 if name: self.builder[name] = builder
438
439 if is_static and not self.strict_order:
440 self.static.setdefault(method, {})
441 self.static[method][self.build(rule)] = (target, None)
442 return
443
444 try:
445 re_pattern = re.compile('^(%s)$' % pattern)
446 re_match = re_pattern.match
447 except re.error:
448 raise RouteSyntaxError("Could not add Route: %s (%s)" %
449 (rule, _e()))
450
451 if filters:
452
453 def getargs(path):
454 url_args = re_match(path).groupdict()
455 for name, wildcard_filter in filters:
456 try:
457 url_args[name] = wildcard_filter(url_args[name])
458 except ValueError:
459 raise HTTPError(400, 'Path has wrong format.')
460 return url_args
461 elif re_pattern.groupindex:
462
463 def getargs(path):
464 return re_match(path).groupdict()
465 else:
466 getargs = None
467
468 flatpat = _re_flatten(pattern)
469 whole_rule = (rule, flatpat, target, getargs)
470
471 if (flatpat, method) in self._groups:
472 if DEBUG:
473 msg = 'Route <%s %s> overwrites a previously defined route'
474 warnings.warn(msg % (method, rule), RuntimeWarning)
475 self.dyna_routes[method][
476 self._groups[flatpat, method]] = whole_rule
477 else:
478 self.dyna_routes.setdefault(method, []).append(whole_rule)
479 self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1
480
481 self._compile(method)
482
483 def _compile(self, method):
484 all_rules = self.dyna_routes[method]
485 comborules = self.dyna_regexes[method] = []
486 maxgroups = self._MAX_GROUPS_PER_PATTERN
487 for x in range(0, len(all_rules), maxgroups):
488 some = all_rules[x:x + maxgroups]
489 combined = (flatpat for (_, flatpat, _, _) in some)
490 combined = '|'.join('(^%s$)' % flatpat for flatpat in combined)
491 combined = re.compile(combined).match
492 rules = [(target, getargs) for (_, _, target, getargs) in some]
493 comborules.append((combined, rules))
494
495 def build(self, _name, *anons, **query):
496 """ Build an URL by filling the wildcards in a rule. """
497 builder = self.builder.get(_name)
498 if not builder:
499 raise RouteBuildError("No route with that name.", _name)
500 try:
501 for i, value in enumerate(anons):
502 query['anon%d' % i] = value
503 url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder])
504 return url if not query else url + '?' + urlencode(query)
505 except KeyError:
506 raise RouteBuildError('Missing URL argument: %r' % _e().args[0])
507
508 def match(self, environ):
509 """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """
510 verb = environ['REQUEST_METHOD'].upper()
511 path = environ['PATH_INFO'] or '/'
512
513 if verb == 'HEAD':
514 methods = ['PROXY', verb, 'GET', 'ANY']
515 else:
516 methods = ['PROXY', verb, 'ANY']
517
518 for method in methods:
519 if method in self.static and path in self.static[method]:
520 target, getargs = self.static[method][path]
521 return target, getargs(path) if getargs else {}
522 elif method in self.dyna_regexes:
523 for combined, rules in self.dyna_regexes[method]:
524 match = combined(path)
525 if match:
526 target, getargs = rules[match.lastindex - 1]
527 return target, getargs(path) if getargs else {}
528
529 # No matching route found. Collect alternative methods for 405 response
530 allowed = set([])
531 nocheck = set(methods)
532 for method in set(self.static) - nocheck:
533 if path in self.static[method]:
534 allowed.add(verb)
535 for method in set(self.dyna_regexes) - allowed - nocheck:
536 for combined, rules in self.dyna_regexes[method]:
537 match = combined(path)
538 if match:
539 allowed.add(method)
540 if allowed:
541 allow_header = ",".join(sorted(allowed))
542 raise HTTPError(405, "Method not allowed.", Allow=allow_header)
543
544 # No matching route and no alternative method found. We give up
545 raise HTTPError(404, "Not found: " + repr(path))
546
547
548 class Route(object):
549 """ This class wraps a route callback along with route specific metadata and
550 configuration and applies Plugins on demand. It is also responsible for
551 turing an URL path rule into a regular expression usable by the Router.
552 """
553
554 def __init__(self, app, rule, method, callback,
555 name=None,
556 plugins=None,
557 skiplist=None, **config):
558 #: The application this route is installed to.
559 self.app = app
560 #: The path-rule string (e.g. ``/wiki/<page>``).
561 self.rule = rule
562 #: The HTTP method as a string (e.g. ``GET``).
563 self.method = method
564 #: The original callback with no plugins applied. Useful for introspection.
565 self.callback = callback
566 #: The name of the route (if specified) or ``None``.
567 self.name = name or None
568 #: A list of route-specific plugins (see :meth:`Bottle.route`).
569 self.plugins = plugins or []
570 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
571 self.skiplist = skiplist or []
572 #: Additional keyword arguments passed to the :meth:`Bottle.route`
573 #: decorator are stored in this dictionary. Used for route-specific
574 #: plugin configuration and meta-data.
575 self.config = ConfigDict().load_dict(config)
576
577 @cached_property
578 def call(self):
579 """ The route callback with all plugins applied. This property is
580 created on demand and then cached to speed up subsequent requests."""
581 return self._make_callback()
582
583 def reset(self):
584 """ Forget any cached values. The next time :attr:`call` is accessed,
585 all plugins are re-applied. """
586 self.__dict__.pop('call', None)
587
588 def prepare(self):
589 """ Do all on-demand work immediately (useful for debugging)."""
590 self.call
591
592 def all_plugins(self):
593 """ Yield all Plugins affecting this route. """
594 unique = set()
595 for p in reversed(self.app.plugins + self.plugins):
596 if True in self.skiplist: break
597 name = getattr(p, 'name', False)
598 if name and (name in self.skiplist or name in unique): continue
599 if p in self.skiplist or type(p) in self.skiplist: continue
600 if name: unique.add(name)
601 yield p
602
603 def _make_callback(self):
604 callback = self.callback
605 for plugin in self.all_plugins():
606 try:
607 if hasattr(plugin, 'apply'):
608 callback = plugin.apply(callback, self)
609 else:
610 callback = plugin(callback)
611 except RouteReset: # Try again with changed configuration.
612 return self._make_callback()
613 if not callback is self.callback:
614 update_wrapper(callback, self.callback)
615 return callback
616
617 def get_undecorated_callback(self):
618 """ Return the callback. If the callback is a decorated function, try to
619 recover the original function. """
620 func = self.callback
621 func = getattr(func, '__func__' if py3k else 'im_func', func)
622 closure_attr = '__closure__' if py3k else 'func_closure'
623 while hasattr(func, closure_attr) and getattr(func, closure_attr):
624 attributes = getattr(func, closure_attr)
625 func = attributes[0].cell_contents
626
627 # in case of decorators with multiple arguments
628 if not isinstance(func, FunctionType):
629 # pick first FunctionType instance from multiple arguments
630 func = filter(lambda x: isinstance(x, FunctionType),
631 map(lambda x: x.cell_contents, attributes))
632 func = list(func)[0] # py3 support
633 return func
634
635 def get_callback_args(self):
636 """ Return a list of argument names the callback (most likely) accepts
637 as keyword arguments. If the callback is a decorated function, try
638 to recover the original function before inspection. """
639 return getargspec(self.get_undecorated_callback())[0]
640
641 def get_config(self, key, default=None):
642 """ Lookup a config field and return its value, first checking the
643 route.config, then route.app.config."""
644 for conf in (self.config, self.app.config):
645 if key in conf: return conf[key]
646 return default
647
648 def __repr__(self):
649 cb = self.get_undecorated_callback()
650 return '<%s %r %r>' % (self.method, self.rule, cb)
651
652 ###############################################################################
653 # Application Object ###########################################################
654 ###############################################################################
655
656
657 class Bottle(object):
658 """ Each Bottle object represents a single, distinct web application and
659 consists of routes, callbacks, plugins, resources and configuration.
660 Instances are callable WSGI applications.
661
662 :param catchall: If true (default), handle all exceptions. Turn off to
663 let debugging middleware handle exceptions.
664 """
665
666 def __init__(self, catchall=True, autojson=True):
667 #: A :class:`ConfigDict` for app specific configuration.
668 self.config = ConfigDict()
669 self.config._add_change_listener(functools.partial(self.trigger_hook, 'config'))
670 self.config.meta_set('autojson', 'validate', bool)
671 self.config.meta_set('catchall', 'validate', bool)
672 self.config['catchall'] = catchall
673 self.config['autojson'] = autojson
674
675 self._mounts = []
676
677 #: A :class:`ResourceManager` for application files
678 self.resources = ResourceManager()
679
680 self.routes = [] # List of installed :class:`Route` instances.
681 self.router = Router() # Maps requests to :class:`Route` instances.
682 self.error_handler = {}
683
684 # Core plugins
685 self.plugins = [] # List of installed plugins.
686 if self.config['autojson']:
687 self.install(JSONPlugin())
688 self.install(TemplatePlugin())
689
690 #: If true, most exceptions are caught and returned as :exc:`HTTPError`
691 catchall = DictProperty('config', 'catchall')
692
693 __hook_names = 'before_request', 'after_request', 'app_reset', 'config'
694 __hook_reversed = 'after_request'
695
696 @cached_property
697 def _hooks(self):
698 return dict((name, []) for name in self.__hook_names)
699
700 def add_hook(self, name, func):
701 """ Attach a callback to a hook. Three hooks are currently implemented:
702
703 before_request
704 Executed once before each request. The request context is
705 available, but no routing has happened yet.
706 after_request
707 Executed once after each request regardless of its outcome.
708 app_reset
709 Called whenever :meth:`Bottle.reset` is called.
710 """
711 if name in self.__hook_reversed:
712 self._hooks[name].insert(0, func)
713 else:
714 self._hooks[name].append(func)
715
716 def remove_hook(self, name, func):
717 """ Remove a callback from a hook. """
718 if name in self._hooks and func in self._hooks[name]:
719 self._hooks[name].remove(func)
720 return True
721
722 def trigger_hook(self, __name, *args, **kwargs):
723 """ Trigger a hook and return a list of results. """
724 return [hook(*args, **kwargs) for hook in self._hooks[__name][:]]
725
726 def hook(self, name):
727 """ Return a decorator that attaches a callback to a hook. See
728 :meth:`add_hook` for details."""
729
730 def decorator(func):
731 self.add_hook(name, func)
732 return func
733
734 return decorator
735
736 def _mount_wsgi(self, prefix, app, **options):
737 segments = [p for p in prefix.split('/') if p]
738 if not segments:
739 raise ValueError('WSGI applications cannot be mounted to "/".')
740 path_depth = len(segments)
741
742 def mountpoint_wrapper():
743 try:
744 request.path_shift(path_depth)
745 rs = HTTPResponse([])
746
747 def start_response(status, headerlist, exc_info=None):
748 if exc_info:
749 _raise(*exc_info)
750 rs.status = status
751 for name, value in headerlist:
752 rs.add_header(name, value)
753 return rs.body.append
754
755 body = app(request.environ, start_response)
756 rs.body = itertools.chain(rs.body, body) if rs.body else body
757 return rs
758 finally:
759 request.path_shift(-path_depth)
760
761 options.setdefault('skip', True)
762 options.setdefault('method', 'PROXY')
763 options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
764 options['callback'] = mountpoint_wrapper
765
766 self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
767 if not prefix.endswith('/'):
768 self.route('/' + '/'.join(segments), **options)
769
770 def _mount_app(self, prefix, app, **options):
771 if app in self._mounts or '_mount.app' in app.config:
772 depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.",
773 "Clone application before mounting to a different location.")
774 return self._mount_wsgi(prefix, app, **options)
775
776 if options:
777 depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.",
778 "Do not specify any route options when mounting bottle application.")
779 return self._mount_wsgi(prefix, app, **options)
780
781 if not prefix.endswith("/"):
782 depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.",
783 "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.")
784 return self._mount_wsgi(prefix, app, **options)
785
786 self._mounts.append(app)
787 app.config['_mount.prefix'] = prefix
788 app.config['_mount.app'] = self
789 for route in app.routes:
790 route.rule = prefix + route.rule.lstrip('/')
791 self.add_route(route)
792
793 def mount(self, prefix, app, **options):
794 """ Mount an application (:class:`Bottle` or plain WSGI) to a specific
795 URL prefix. Example::
796
797 parent_app.mount('/prefix/', child_app)
798
799 :param prefix: path prefix or `mount-point`.
800 :param app: an instance of :class:`Bottle` or a WSGI application.
801
802 Plugins from the parent application are not applied to the routes
803 of the mounted child application. If you need plugins in the child
804 application, install them separately.
805
806 While it is possible to use path wildcards within the prefix path
807 (:class:`Bottle` childs only), it is highly discouraged.
808
809 The prefix path must end with a slash. If you want to access the
810 root of the child application via `/prefix` in addition to
811 `/prefix/`, consider adding a route with a 307 redirect to the
812 parent application.
813 """
814
815 if not prefix.startswith('/'):
816 raise ValueError("Prefix must start with '/'")
817
818 if isinstance(app, Bottle):
819 return self._mount_app(prefix, app, **options)
820 else:
821 return self._mount_wsgi(prefix, app, **options)
822
823 def merge(self, routes):
824 """ Merge the routes of another :class:`Bottle` application or a list of
825 :class:`Route` objects into this application. The routes keep their
826 'owner', meaning that the :data:`Route.app` attribute is not
827 changed. """
828 if isinstance(routes, Bottle):
829 routes = routes.routes
830 for route in routes:
831 self.add_route(route)
832
833 def install(self, plugin):
834 """ Add a plugin to the list of plugins and prepare it for being
835 applied to all routes of this application. A plugin may be a simple
836 decorator or an object that implements the :class:`Plugin` API.
837 """
838 if hasattr(plugin, 'setup'): plugin.setup(self)
839 if not callable(plugin) and not hasattr(plugin, 'apply'):
840 raise TypeError("Plugins must be callable or implement .apply()")
841 self.plugins.append(plugin)
842 self.reset()
843 return plugin
844
845 def uninstall(self, plugin):
846 """ Uninstall plugins. Pass an instance to remove a specific plugin, a type
847 object to remove all plugins that match that type, a string to remove
848 all plugins with a matching ``name`` attribute or ``True`` to remove all
849 plugins. Return the list of removed plugins. """
850 removed, remove = [], plugin
851 for i, plugin in list(enumerate(self.plugins))[::-1]:
852 if remove is True or remove is plugin or remove is type(plugin) \
853 or getattr(plugin, 'name', True) == remove:
854 removed.append(plugin)
855 del self.plugins[i]
856 if hasattr(plugin, 'close'): plugin.close()
857 if removed: self.reset()
858 return removed
859
860 def reset(self, route=None):
861 """ Reset all routes (force plugins to be re-applied) and clear all
862 caches. If an ID or route object is given, only that specific route
863 is affected. """
864 if route is None: routes = self.routes
865 elif isinstance(route, Route): routes = [route]
866 else: routes = [self.routes[route]]
867 for route in routes:
868 route.reset()
869 if DEBUG:
870 for route in routes:
871 route.prepare()
872 self.trigger_hook('app_reset')
873
874 def close(self):
875 """ Close the application and all installed plugins. """
876 for plugin in self.plugins:
877 if hasattr(plugin, 'close'): plugin.close()
878
879 def run(self, **kwargs):
880 """ Calls :func:`run` with the same parameters. """
881 run(self, **kwargs)
882
883 def match(self, environ):
884 """ Search for a matching route and return a (:class:`Route` , urlargs)
885 tuple. The second value is a dictionary with parameters extracted
886 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
887 return self.router.match(environ)
888
889 def get_url(self, routename, **kargs):
890 """ Return a string that matches a named route """
891 scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
892 location = self.router.build(routename, **kargs).lstrip('/')
893 return urljoin(urljoin('/', scriptname), location)
894
895 def add_route(self, route):
896 """ Add a route object, but do not change the :data:`Route.app`
897 attribute."""
898 self.routes.append(route)
899 self.router.add(route.rule, route.method, route, name=route.name)
900 if DEBUG: route.prepare()
901
902 def route(self,
903 path=None,
904 method='GET',
905 callback=None,
906 name=None,
907 apply=None,
908 skip=None, **config):
909 """ A decorator to bind a function to a request URL. Example::
910
911 @app.route('/hello/<name>')
912 def hello(name):
913 return 'Hello %s' % name
914
915 The ``<name>`` part is a wildcard. See :class:`Router` for syntax
916 details.
917
918 :param path: Request path or a list of paths to listen to. If no
919 path is specified, it is automatically generated from the
920 signature of the function.
921 :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of
922 methods to listen to. (default: `GET`)
923 :param callback: An optional shortcut to avoid the decorator
924 syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
925 :param name: The name for this route. (default: None)
926 :param apply: A decorator or plugin or a list of plugins. These are
927 applied to the route callback in addition to installed plugins.
928 :param skip: A list of plugins, plugin classes or names. Matching
929 plugins are not installed to this route. ``True`` skips all.
930
931 Any additional keyword arguments are stored as route-specific
932 configuration and passed to plugins (see :meth:`Plugin.apply`).
933 """
934 if callable(path): path, callback = None, path
935 plugins = makelist(apply)
936 skiplist = makelist(skip)
937
938 def decorator(callback):
939 if isinstance(callback, basestring): callback = load(callback)
940 for rule in makelist(path) or yieldroutes(callback):
941 for verb in makelist(method):
942 verb = verb.upper()
943 route = Route(self, rule, verb, callback,
944 name=name,
945 plugins=plugins,
946 skiplist=skiplist, **config)
947 self.add_route(route)
948 return callback
949
950 return decorator(callback) if callback else decorator
951
952 def get(self, path=None, method='GET', **options):
953 """ Equals :meth:`route`. """
954 return self.route(path, method, **options)
955
956 def post(self, path=None, method='POST', **options):
957 """ Equals :meth:`route` with a ``POST`` method parameter. """
958 return self.route(path, method, **options)
959
960 def put(self, path=None, method='PUT', **options):
961 """ Equals :meth:`route` with a ``PUT`` method parameter. """
962 return self.route(path, method, **options)
963
964 def delete(self, path=None, method='DELETE', **options):
965 """ Equals :meth:`route` with a ``DELETE`` method parameter. """
966 return self.route(path, method, **options)
967
968 def patch(self, path=None, method='PATCH', **options):
969 """ Equals :meth:`route` with a ``PATCH`` method parameter. """
970 return self.route(path, method, **options)
971
972 def error(self, code=500):
973 """ Decorator: Register an output handler for a HTTP error code"""
974
975 def wrapper(handler):
976 self.error_handler[int(code)] = handler
977 return handler
978
979 return wrapper
980
981 def default_error_handler(self, res):
982 return tob(template(ERROR_PAGE_TEMPLATE, e=res))
983
984 def _handle(self, environ):
985 path = environ['bottle.raw_path'] = environ['PATH_INFO']
986 if py3k:
987 environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore')
988
989 def _inner_handle():
990 # Maybe pass variables as locals for better performance?
991 try:
992 route, args = self.router.match(environ)
993 environ['route.handle'] = route
994 environ['bottle.route'] = route
995 environ['route.url_args'] = args
996 return route.call(**args)
997 except HTTPResponse:
998 return _e()
999 except RouteReset:
1000 route.reset()
1001 return _inner_handle()
1002 except (KeyboardInterrupt, SystemExit, MemoryError):
1003 raise
1004 except Exception:
1005 if not self.catchall: raise
1006 stacktrace = format_exc()
1007 environ['wsgi.errors'].write(stacktrace)
1008 return HTTPError(500, "Internal Server Error", _e(), stacktrace)
1009
1010 try:
1011 out = None
1012 environ['bottle.app'] = self
1013 request.bind(environ)
1014 response.bind()
1015 try:
1016 self.trigger_hook('before_request')
1017 except HTTPResponse:
1018 return _e()
1019 out = _inner_handle()
1020 return out
1021 finally:
1022 if isinstance(out, HTTPResponse):
1023 out.apply(response)
1024 self.trigger_hook('after_request')
1025
1026 def _cast(self, out, peek=None):
1027 """ Try to convert the parameter into something WSGI compatible and set
1028 correct HTTP headers when possible.
1029 Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
1030 iterable of strings and iterable of unicodes
1031 """
1032
1033 # Empty output is done here
1034 if not out:
1035 if 'Content-Length' not in response:
1036 response['Content-Length'] = 0
1037 return []
1038 # Join lists of byte or unicode strings. Mixed lists are NOT supported
1039 if isinstance(out, (tuple, list))\
1040 and isinstance(out[0], (bytes, unicode)):
1041 out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
1042 # Encode unicode strings
1043 if isinstance(out, unicode):
1044 out = out.encode(response.charset)
1045 # Byte Strings are just returned
1046 if isinstance(out, bytes):
1047 if 'Content-Length' not in response:
1048 response['Content-Length'] = len(out)
1049 return [out]
1050 # HTTPError or HTTPException (recursive, because they may wrap anything)
1051 # TODO: Handle these explicitly in handle() or make them iterable.
1052 if isinstance(out, HTTPError):
1053 out.apply(response)
1054 out = self.error_handler.get(out.status_code,
1055 self.default_error_handler)(out)
1056 return self._cast(out)
1057 if isinstance(out, HTTPResponse):
1058 out.apply(response)
1059 return self._cast(out.body)
1060
1061 # File-like objects.
1062 if hasattr(out, 'read'):
1063 if 'wsgi.file_wrapper' in request.environ:
1064 return request.environ['wsgi.file_wrapper'](out)
1065 elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
1066 return WSGIFileWrapper(out)
1067
1068 # Handle Iterables. We peek into them to detect their inner type.
1069 try:
1070 iout = iter(out)
1071 first = next(iout)
1072 while not first:
1073 first = next(iout)
1074 except StopIteration:
1075 return self._cast('')
1076 except HTTPResponse:
1077 first = _e()
1078 except (KeyboardInterrupt, SystemExit, MemoryError):
1079 raise
1080 except:
1081 if not self.catchall: raise
1082 first = HTTPError(500, 'Unhandled exception', _e(), format_exc())
1083
1084 # These are the inner types allowed in iterator or generator objects.
1085 if isinstance(first, HTTPResponse):
1086 return self._cast(first)
1087 elif isinstance(first, bytes):
1088 new_iter = itertools.chain([first], iout)
1089 elif isinstance(first, unicode):
1090 encoder = lambda x: x.encode(response.charset)
1091 new_iter = imap(encoder, itertools.chain([first], iout))
1092 else:
1093 msg = 'Unsupported response type: %s' % type(first)
1094 return self._cast(HTTPError(500, msg))
1095 if hasattr(out, 'close'):
1096 new_iter = _closeiter(new_iter, out.close)
1097 return new_iter
1098
1099 def wsgi(self, environ, start_response):
1100 """ The bottle WSGI-interface. """
1101 try:
1102 out = self._cast(self._handle(environ))
1103 # rfc2616 section 4.3
1104 if response._status_code in (100, 101, 204, 304)\
1105 or environ['REQUEST_METHOD'] == 'HEAD':
1106 if hasattr(out, 'close'): out.close()
1107 out = []
1108 start_response(response._status_line, response.headerlist)
1109 return out
1110 except (KeyboardInterrupt, SystemExit, MemoryError):
1111 raise
1112 except:
1113 if not self.catchall: raise
1114 err = '<h1>Critical error while processing request: %s</h1>' \
1115 % html_escape(environ.get('PATH_INFO', '/'))
1116 if DEBUG:
1117 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1118 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1119 % (html_escape(repr(_e())), html_escape(format_exc()))
1120 environ['wsgi.errors'].write(err)
1121 headers = [('Content-Type', 'text/html; charset=UTF-8')]
1122 start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
1123 return [tob(err)]
1124
1125 def __call__(self, environ, start_response):
1126 """ Each instance of :class:'Bottle' is a WSGI application. """
1127 return self.wsgi(environ, start_response)
1128
1129 def __enter__(self):
1130 """ Use this application as default for all module-level shortcuts. """
1131 default_app.push(self)
1132 return self
1133
1134 def __exit__(self, exc_type, exc_value, traceback):
1135 default_app.pop()
1136
1137 def __setattr__(self, name, value):
1138 if name in self.__dict__:
1139 raise AttributeError("Attribute %s already defined. Plugin conflict?" % name)
1140 self.__dict__[name] = value
1141
1142
1143 ###############################################################################
1144 # HTTP and WSGI Tools ##########################################################
1145 ###############################################################################
1146
1147
1148 class BaseRequest(object):
1149 """ A wrapper for WSGI environment dictionaries that adds a lot of
1150 convenient access methods and properties. Most of them are read-only.
1151
1152 Adding new attributes to a request actually adds them to the environ
1153 dictionary (as 'bottle.request.ext.<name>'). This is the recommended
1154 way to store and access request-specific data.
1155 """
1156
1157 __slots__ = ('environ', )
1158
1159 #: Maximum size of memory buffer for :attr:`body` in bytes.
1160 MEMFILE_MAX = 102400
1161
1162 def __init__(self, environ=None):
1163 """ Wrap a WSGI environ dictionary. """
1164 #: The wrapped WSGI environ dictionary. This is the only real attribute.
1165 #: All other attributes actually are read-only properties.
1166 self.environ = {} if environ is None else environ
1167 self.environ['bottle.request'] = self
1168
1169 @DictProperty('environ', 'bottle.app', read_only=True)
1170 def app(self):
1171 """ Bottle application handling this request. """
1172 raise RuntimeError('This request is not connected to an application.')
1173
1174 @DictProperty('environ', 'bottle.route', read_only=True)
1175 def route(self):
1176 """ The bottle :class:`Route` object that matches this request. """
1177 raise RuntimeError('This request is not connected to a route.')
1178
1179 @DictProperty('environ', 'route.url_args', read_only=True)
1180 def url_args(self):
1181 """ The arguments extracted from the URL. """
1182 raise RuntimeError('This request is not connected to a route.')
1183
1184 @property
1185 def path(self):
1186 """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix
1187 broken clients and avoid the "empty path" edge case). """
1188 return '/' + self.environ.get('PATH_INFO', '').lstrip('/')
1189
1190 @property
1191 def method(self):
1192 """ The ``REQUEST_METHOD`` value as an uppercase string. """
1193 return self.environ.get('REQUEST_METHOD', 'GET').upper()
1194
1195 @DictProperty('environ', 'bottle.request.headers', read_only=True)
1196 def headers(self):
1197 """ A :class:`WSGIHeaderDict` that provides case-insensitive access to
1198 HTTP request headers. """
1199 return WSGIHeaderDict(self.environ)
1200
1201 def get_header(self, name, default=None):
1202 """ Return the value of a request header, or a given default value. """
1203 return self.headers.get(name, default)
1204
1205 @DictProperty('environ', 'bottle.request.cookies', read_only=True)
1206 def cookies(self):
1207 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT
1208 decoded. Use :meth:`get_cookie` if you expect signed cookies. """
1209 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values()
1210 return FormsDict((c.key, c.value) for c in cookies)
1211
1212 def get_cookie(self, key, default=None, secret=None):
1213 """ Return the content of a cookie. To read a `Signed Cookie`, the
1214 `secret` must match the one used to create the cookie (see
1215 :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
1216 cookie or wrong signature), return a default value. """
1217 value = self.cookies.get(key)
1218 if secret and value:
1219 dec = cookie_decode(value, secret) # (key, value) tuple or None
1220 return dec[1] if dec and dec[0] == key else default
1221 return value or default
1222
1223 @DictProperty('environ', 'bottle.request.query', read_only=True)
1224 def query(self):
1225 """ The :attr:`query_string` parsed into a :class:`FormsDict`. These
1226 values are sometimes called "URL arguments" or "GET parameters", but
1227 not to be confused with "URL wildcards" as they are provided by the
1228 :class:`Router`. """
1229 get = self.environ['bottle.get'] = FormsDict()
1230 pairs = _parse_qsl(self.environ.get('QUERY_STRING', ''))
1231 for key, value in pairs:
1232 get[key] = value
1233 return get
1234
1235 @DictProperty('environ', 'bottle.request.forms', read_only=True)
1236 def forms(self):
1237 """ Form values parsed from an `url-encoded` or `multipart/form-data`
1238 encoded POST or PUT request body. The result is returned as a
1239 :class:`FormsDict`. All keys and values are strings. File uploads
1240 are stored separately in :attr:`files`. """
1241 forms = FormsDict()
1242 for name, item in self.POST.allitems():
1243 if not isinstance(item, FileUpload):
1244 forms[name] = item
1245 return forms
1246
1247 @DictProperty('environ', 'bottle.request.params', read_only=True)
1248 def params(self):
1249 """ A :class:`FormsDict` with the combined values of :attr:`query` and
1250 :attr:`forms`. File uploads are stored in :attr:`files`. """
1251 params = FormsDict()
1252 for key, value in self.query.allitems():
1253 params[key] = value
1254 for key, value in self.forms.allitems():
1255 params[key] = value
1256 return params
1257
1258 @DictProperty('environ', 'bottle.request.files', read_only=True)
1259 def files(self):
1260 """ File uploads parsed from `multipart/form-data` encoded POST or PUT
1261 request body. The values are instances of :class:`FileUpload`.
1262
1263 """
1264 files = FormsDict()
1265 for name, item in self.POST.allitems():
1266 if isinstance(item, FileUpload):
1267 files[name] = item
1268 return files
1269
1270 @DictProperty('environ', 'bottle.request.json', read_only=True)
1271 def json(self):
1272 """ If the ``Content-Type`` header is ``application/json`` or
1273 ``application/json-rpc``, this property holds the parsed content
1274 of the request body. Only requests smaller than :attr:`MEMFILE_MAX`
1275 are processed to avoid memory exhaustion.
1276 Invalid JSON raises a 400 error response.
1277 """
1278 ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0]
1279 if ctype in ('application/json', 'application/json-rpc'):
1280 b = self._get_body_string()
1281 if not b:
1282 return None
1283 try:
1284 return json_loads(b)
1285 except (ValueError, TypeError):
1286 raise HTTPError(400, 'Invalid JSON')
1287 return None
1288
1289 def _iter_body(self, read, bufsize):
1290 maxread = max(0, self.content_length)
1291 while maxread:
1292 part = read(min(maxread, bufsize))
1293 if not part: break
1294 yield part
1295 maxread -= len(part)
1296
1297 @staticmethod
1298 def _iter_chunked(read, bufsize):
1299 err = HTTPError(400, 'Error while parsing chunked transfer body.')
1300 rn, sem, bs = tob('\r\n'), tob(';'), tob('')
1301 while True:
1302 header = read(1)
1303 while header[-2:] != rn:
1304 c = read(1)
1305 header += c
1306 if not c: raise err
1307 if len(header) > bufsize: raise err
1308 size, _, _ = header.partition(sem)
1309 try:
1310 maxread = int(tonat(size.strip()), 16)
1311 except ValueError:
1312 raise err
1313 if maxread == 0: break
1314 buff = bs
1315 while maxread > 0:
1316 if not buff:
1317 buff = read(min(maxread, bufsize))
1318 part, buff = buff[:maxread], buff[maxread:]
1319 if not part: raise err
1320 yield part
1321 maxread -= len(part)
1322 if read(2) != rn:
1323 raise err
1324
1325 @DictProperty('environ', 'bottle.request.body', read_only=True)
1326 def _body(self):
1327 try:
1328 read_func = self.environ['wsgi.input'].read
1329 except KeyError:
1330 self.environ['wsgi.input'] = BytesIO()
1331 return self.environ['wsgi.input']
1332 body_iter = self._iter_chunked if self.chunked else self._iter_body
1333 body, body_size, is_temp_file = BytesIO(), 0, False
1334 for part in body_iter(read_func, self.MEMFILE_MAX):
1335 body.write(part)
1336 body_size += len(part)
1337 if not is_temp_file and body_size > self.MEMFILE_MAX:
1338 body, tmp = TemporaryFile(mode='w+b'), body
1339 body.write(tmp.getvalue())
1340 del tmp
1341 is_temp_file = True
1342 self.environ['wsgi.input'] = body
1343 body.seek(0)
1344 return body
1345
1346 def _get_body_string(self):
1347 """ read body until content-length or MEMFILE_MAX into a string. Raise
1348 HTTPError(413) on requests that are to large. """
1349 clen = self.content_length
1350 if clen > self.MEMFILE_MAX:
1351 raise HTTPError(413, 'Request entity too large')
1352 if clen < 0: clen = self.MEMFILE_MAX + 1
1353 data = self.body.read(clen)
1354 if len(data) > self.MEMFILE_MAX: # Fail fast
1355 raise HTTPError(413, 'Request entity too large')
1356 return data
1357
1358 @property
1359 def body(self):
1360 """ The HTTP request body as a seek-able file-like object. Depending on
1361 :attr:`MEMFILE_MAX`, this is either a temporary file or a
1362 :class:`io.BytesIO` instance. Accessing this property for the first
1363 time reads and replaces the ``wsgi.input`` environ variable.
1364 Subsequent accesses just do a `seek(0)` on the file object. """
1365 self._body.seek(0)
1366 return self._body
1367
1368 @property
1369 def chunked(self):
1370 """ True if Chunked transfer encoding was. """
1371 return 'chunked' in self.environ.get(
1372 'HTTP_TRANSFER_ENCODING', '').lower()
1373
1374 #: An alias for :attr:`query`.
1375 GET = query
1376
1377 @DictProperty('environ', 'bottle.request.post', read_only=True)
1378 def POST(self):
1379 """ The values of :attr:`forms` and :attr:`files` combined into a single
1380 :class:`FormsDict`. Values are either strings (form values) or
1381 instances of :class:`cgi.FieldStorage` (file uploads).
1382 """
1383 post = FormsDict()
1384 # We default to application/x-www-form-urlencoded for everything that
1385 # is not multipart and take the fast path (also: 3.1 workaround)
1386 if not self.content_type.startswith('multipart/'):
1387 pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1'))
1388 for key, value in pairs:
1389 post[key] = value
1390 return post
1391
1392 safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi
1393 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
1394 if key in self.environ: safe_env[key] = self.environ[key]
1395 args = dict(fp=self.body, environ=safe_env, keep_blank_values=True)
1396 if py31:
1397 args['fp'] = NCTextIOWrapper(args['fp'],
1398 encoding='utf8',
1399 newline='\n')
1400 elif py3k:
1401 args['encoding'] = 'utf8'
1402 data = cgi.FieldStorage(**args)
1403 self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394
1404 data = data.list or []
1405 for item in data:
1406 if item.filename:
1407 post[item.name] = FileUpload(item.file, item.name,
1408 item.filename, item.headers)
1409 else:
1410 post[item.name] = item.value
1411 return post
1412
1413 @property
1414 def url(self):
1415 """ The full request URI including hostname and scheme. If your app
1416 lives behind a reverse proxy or load balancer and you get confusing
1417 results, make sure that the ``X-Forwarded-Host`` header is set
1418 correctly. """
1419 return self.urlparts.geturl()
1420
1421 @DictProperty('environ', 'bottle.request.urlparts', read_only=True)
1422 def urlparts(self):
1423 """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple.
1424 The tuple contains (scheme, host, path, query_string and fragment),
1425 but the fragment is always empty because it is not visible to the
1426 server. """
1427 env = self.environ
1428 http = env.get('HTTP_X_FORWARDED_PROTO') \
1429 or env.get('wsgi.url_scheme', 'http')
1430 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
1431 if not host:
1432 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
1433 host = env.get('SERVER_NAME', '127.0.0.1')
1434 port = env.get('SERVER_PORT')
1435 if port and port != ('80' if http == 'http' else '443'):
1436 host += ':' + port
1437 path = urlquote(self.fullpath)
1438 return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
1439
1440 @property
1441 def fullpath(self):
1442 """ Request path including :attr:`script_name` (if present). """
1443 return urljoin(self.script_name, self.path.lstrip('/'))
1444
1445 @property
1446 def query_string(self):
1447 """ The raw :attr:`query` part of the URL (everything in between ``?``
1448 and ``#``) as a string. """
1449 return self.environ.get('QUERY_STRING', '')
1450
1451 @property
1452 def script_name(self):
1453 """ The initial portion of the URL's `path` that was removed by a higher
1454 level (server or routing middleware) before the application was
1455 called. This script path is returned with leading and tailing
1456 slashes. """
1457 script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
1458 return '/' + script_name + '/' if script_name else '/'
1459
1460 def path_shift(self, shift=1):
1461 """ Shift path segments from :attr:`path` to :attr:`script_name` and
1462 vice versa.
1463
1464 :param shift: The number of path segments to shift. May be negative
1465 to change the shift direction. (default: 1)
1466 """
1467 script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift)
1468 self['SCRIPT_NAME'], self['PATH_INFO'] = script, path
1469
1470 @property
1471 def content_length(self):
1472 """ The request body length as an integer. The client is responsible to
1473 set this header. Otherwise, the real length of the body is unknown
1474 and -1 is returned. In this case, :attr:`body` will be empty. """
1475 return int(self.environ.get('CONTENT_LENGTH') or -1)
1476
1477 @property
1478 def content_type(self):
1479 """ The Content-Type header as a lowercase-string (default: empty). """
1480 return self.environ.get('CONTENT_TYPE', '').lower()
1481
1482 @property
1483 def is_xhr(self):
1484 """ True if the request was triggered by a XMLHttpRequest. This only
1485 works with JavaScript libraries that support the `X-Requested-With`
1486 header (most of the popular libraries do). """
1487 requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '')
1488 return requested_with.lower() == 'xmlhttprequest'
1489
1490 @property
1491 def is_ajax(self):
1492 """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """
1493 return self.is_xhr
1494
1495 @property
1496 def auth(self):
1497 """ HTTP authentication data as a (user, password) tuple. This
1498 implementation currently supports basic (not digest) authentication
1499 only. If the authentication happened at a higher level (e.g. in the
1500 front web-server or a middleware), the password field is None, but
1501 the user field is looked up from the ``REMOTE_USER`` environ
1502 variable. On any errors, None is returned. """
1503 basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', ''))
1504 if basic: return basic
1505 ruser = self.environ.get('REMOTE_USER')
1506 if ruser: return (ruser, None)
1507 return None
1508
1509 @property
1510 def remote_route(self):
1511 """ A list of all IPs that were involved in this request, starting with
1512 the client IP and followed by zero or more proxies. This does only
1513 work if all proxies support the ```X-Forwarded-For`` header. Note
1514 that this information can be forged by malicious clients. """
1515 proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
1516 if proxy: return [ip.strip() for ip in proxy.split(',')]
1517 remote = self.environ.get('REMOTE_ADDR')
1518 return [remote] if remote else []
1519
1520 @property
1521 def remote_addr(self):
1522 """ The client IP as a string. Note that this information can be forged
1523 by malicious clients. """
1524 route = self.remote_route
1525 return route[0] if route else None
1526
1527 def copy(self):
1528 """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """
1529 return Request(self.environ.copy())
1530
1531 def get(self, value, default=None):
1532 return self.environ.get(value, default)
1533
1534 def __getitem__(self, key):
1535 return self.environ[key]
1536
1537 def __delitem__(self, key):
1538 self[key] = ""
1539 del (self.environ[key])
1540
1541 def __iter__(self):
1542 return iter(self.environ)
1543
1544 def __len__(self):
1545 return len(self.environ)
1546
1547 def keys(self):
1548 return self.environ.keys()
1549
1550 def __setitem__(self, key, value):
1551 """ Change an environ value and clear all caches that depend on it. """
1552
1553 if self.environ.get('bottle.request.readonly'):
1554 raise KeyError('The environ dictionary is read-only.')
1555
1556 self.environ[key] = value
1557 todelete = ()
1558
1559 if key == 'wsgi.input':
1560 todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
1561 elif key == 'QUERY_STRING':
1562 todelete = ('query', 'params')
1563 elif key.startswith('HTTP_'):
1564 todelete = ('headers', 'cookies')
1565
1566 for key in todelete:
1567 self.environ.pop('bottle.request.' + key, None)
1568
1569 def __repr__(self):
1570 return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)
1571
1572 def __getattr__(self, name):
1573 """ Search in self.environ for additional user defined attributes. """
1574 try:
1575 var = self.environ['bottle.request.ext.%s' % name]
1576 return var.__get__(self) if hasattr(var, '__get__') else var
1577 except KeyError:
1578 raise AttributeError('Attribute %r not defined.' % name)
1579
1580 def __setattr__(self, name, value):
1581 if name == 'environ': return object.__setattr__(self, name, value)
1582 key = 'bottle.request.ext.%s' % name
1583 if key in self.environ:
1584 raise AttributeError("Attribute already defined: %s" % name)
1585 self.environ[key] = value
1586
1587 def __delattr__(self, name, value):
1588 try:
1589 del self.environ['bottle.request.ext.%s' % name]
1590 except KeyError:
1591 raise AttributeError("Attribute not defined: %s" % name)
1592
1593 def _hkey(s):
1594 return s.title().replace('_', '-')
1595
1596
1597 class HeaderProperty(object):
1598 def __init__(self, name, reader=None, writer=str, default=''):
1599 self.name, self.default = name, default
1600 self.reader, self.writer = reader, writer
1601 self.__doc__ = 'Current value of the %r header.' % name.title()
1602
1603 def __get__(self, obj, _):
1604 if obj is None: return self
1605 value = obj.headers.get(self.name, self.default)
1606 return self.reader(value) if self.reader else value
1607
1608 def __set__(self, obj, value):
1609 obj.headers[self.name] = self.writer(value)
1610
1611 def __delete__(self, obj):
1612 del obj.headers[self.name]
1613
1614
1615 class BaseResponse(object):
1616 """ Storage class for a response body as well as headers and cookies.
1617
1618 This class does support dict-like case-insensitive item-access to
1619 headers, but is NOT a dict. Most notably, iterating over a response
1620 yields parts of the body and not the headers.
1621
1622 :param body: The response body as one of the supported types.
1623 :param status: Either an HTTP status code (e.g. 200) or a status line
1624 including the reason phrase (e.g. '200 OK').
1625 :param headers: A dictionary or a list of name-value pairs.
1626
1627 Additional keyword arguments are added to the list of headers.
1628 Underscores in the header name are replaced with dashes.
1629 """
1630
1631 default_status = 200
1632 default_content_type = 'text/html; charset=UTF-8'
1633
1634 # Header blacklist for specific response codes
1635 # (rfc2616 section 10.2.3 and 10.3.5)
1636 bad_headers = {
1637 204: set(('Content-Type', 'Content-Length')),
1638 304: set(('Allow', 'Content-Encoding', 'Content-Language',
1639 'Content-Length', 'Content-Range', 'Content-Type',
1640 'Content-Md5', 'Last-Modified'))
1641 }
1642
1643 def __init__(self, body='', status=None, headers=None, **more_headers):
1644 self._cookies = None
1645 self._headers = {}
1646 self.body = body
1647 self.status = status or self.default_status
1648 if headers:
1649 if isinstance(headers, dict):
1650 headers = headers.items()
1651 for name, value in headers:
1652 self.add_header(name, value)
1653 if more_headers:
1654 for name, value in more_headers.items():
1655 self.add_header(name, value)
1656
1657 def copy(self, cls=None):
1658 """ Returns a copy of self. """
1659 cls = cls or BaseResponse
1660 assert issubclass(cls, BaseResponse)
1661 copy = cls()
1662 copy.status = self.status
1663 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
1664 if self._cookies:
1665 copy._cookies = SimpleCookie()
1666 copy._cookies.load(self._cookies.output(header=''))
1667 return copy
1668
1669 def __iter__(self):
1670 return iter(self.body)
1671
1672 def close(self):
1673 if hasattr(self.body, 'close'):
1674 self.body.close()
1675
1676 @property
1677 def status_line(self):
1678 """ The HTTP status line as a string (e.g. ``404 Not Found``)."""
1679 return self._status_line
1680
1681 @property
1682 def status_code(self):
1683 """ The HTTP status code as an integer (e.g. 404)."""
1684 return self._status_code
1685
1686 def _set_status(self, status):
1687 if isinstance(status, int):
1688 code, status = status, _HTTP_STATUS_LINES.get(status)
1689 elif ' ' in status:
1690 status = status.strip()
1691 code = int(status.split()[0])
1692 else:
1693 raise ValueError('String status line without a reason phrase.')
1694 if not 100 <= code <= 999:
1695 raise ValueError('Status code out of range.')
1696 self._status_code = code
1697 self._status_line = str(status or ('%d Unknown' % code))
1698
1699 def _get_status(self):
1700 return self._status_line
1701
1702 status = property(
1703 _get_status, _set_status, None,
1704 ''' A writeable property to change the HTTP response status. It accepts
1705 either a numeric code (100-999) or a string with a custom reason
1706 phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
1707 :data:`status_code` are updated accordingly. The return value is
1708 always a status string. ''')
1709 del _get_status, _set_status
1710
1711 @property
1712 def headers(self):
1713 """ An instance of :class:`HeaderDict`, a case-insensitive dict-like
1714 view on the response headers. """
1715 hdict = HeaderDict()
1716 hdict.dict = self._headers
1717 return hdict
1718
1719 def __contains__(self, name):
1720 return _hkey(name) in self._headers
1721
1722 def __delitem__(self, name):
1723 del self._headers[_hkey(name)]
1724
1725 def __getitem__(self, name):
1726 return self._headers[_hkey(name)][-1]
1727
1728 def __setitem__(self, name, value):
1729 self._headers[_hkey(name)] = [value if isinstance(value, unicode) else
1730 str(value)]
1731
1732 def get_header(self, name, default=None):
1733 """ Return the value of a previously defined header. If there is no
1734 header with that name, return a default value. """
1735 return self._headers.get(_hkey(name), [default])[-1]
1736
1737 def set_header(self, name, value):
1738 """ Create a new response header, replacing any previously defined
1739 headers with the same name. """
1740 self._headers[_hkey(name)] = [value if isinstance(value, unicode)
1741 else str(value)]
1742
1743 def add_header(self, name, value):
1744 """ Add an additional response header, not removing duplicates. """
1745 self._headers.setdefault(_hkey(name), []).append(
1746 value if isinstance(value, unicode) else str(value))
1747
1748 def iter_headers(self):
1749 """ Yield (header, value) tuples, skipping headers that are not
1750 allowed with the current response status code. """
1751 return self.headerlist
1752
1753 @property
1754 def headerlist(self):
1755 """ WSGI conform list of (header, value) tuples. """
1756 out = []
1757 headers = list(self._headers.items())
1758 if 'Content-Type' not in self._headers:
1759 headers.append(('Content-Type', [self.default_content_type]))
1760 if self._status_code in self.bad_headers:
1761 bad_headers = self.bad_headers[self._status_code]
1762 headers = [h for h in headers if h[0] not in bad_headers]
1763 out += [(name, val) for (name, vals) in headers for val in vals]
1764 if self._cookies:
1765 for c in self._cookies.values():
1766 out.append(('Set-Cookie', c.OutputString()))
1767 if py3k:
1768 return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out]
1769 else:
1770 return [(k, v.encode('utf8') if isinstance(v, unicode) else v)
1771 for (k, v) in out]
1772
1773 content_type = HeaderProperty('Content-Type')
1774 content_length = HeaderProperty('Content-Length', reader=int)
1775 expires = HeaderProperty(
1776 'Expires',
1777 reader=lambda x: datetime.utcfromtimestamp(parse_date(x)),
1778 writer=lambda x: http_date(x))
1779
1780 @property
1781 def charset(self, default='UTF-8'):
1782 """ Return the charset specified in the content-type header (default: utf8). """
1783 if 'charset=' in self.content_type:
1784 return self.content_type.split('charset=')[-1].split(';')[0].strip()
1785 return default
1786
1787 def set_cookie(self, name, value, secret=None, **options):
1788 """ Create a new cookie or replace an old one. If the `secret` parameter is
1789 set, create a `Signed Cookie` (described below).
1790
1791 :param name: the name of the cookie.
1792 :param value: the value of the cookie.
1793 :param secret: a signature key required for signed cookies.
1794
1795 Additionally, this method accepts all RFC 2109 attributes that are
1796 supported by :class:`cookie.Morsel`, including:
1797
1798 :param max_age: maximum age in seconds. (default: None)
1799 :param expires: a datetime object or UNIX timestamp. (default: None)
1800 :param domain: the domain that is allowed to read the cookie.
1801 (default: current domain)
1802 :param path: limits the cookie to a given path (default: current path)
1803 :param secure: limit the cookie to HTTPS connections (default: off).
1804 :param httponly: prevents client-side javascript to read this cookie
1805 (default: off, requires Python 2.6 or newer).
1806
1807 If neither `expires` nor `max_age` is set (default), the cookie will
1808 expire at the end of the browser session (as soon as the browser
1809 window is closed).
1810
1811 Signed cookies may store any pickle-able object and are
1812 cryptographically signed to prevent manipulation. Keep in mind that
1813 cookies are limited to 4kb in most browsers.
1814
1815 Warning: Signed cookies are not encrypted (the client can still see
1816 the content) and not copy-protected (the client can restore an old
1817 cookie). The main intention is to make pickling and unpickling
1818 save, not to store secret information at client side.
1819 """
1820 if not self._cookies:
1821 self._cookies = SimpleCookie()
1822
1823 if secret:
1824 value = touni(cookie_encode((name, value), secret))
1825 elif not isinstance(value, basestring):
1826 raise TypeError('Secret key missing for non-string Cookie.')
1827
1828 # Cookie size plus options must not exceed 4kb.
1829 if len(name) + len(value) > 3800:
1830 raise ValueError('Content does not fit into a cookie.')
1831
1832 self._cookies[name] = value
1833
1834 for key, value in options.items():
1835 if key == 'max_age':
1836 if isinstance(value, timedelta):
1837 value = value.seconds + value.days * 24 * 3600
1838 if key == 'expires':
1839 if isinstance(value, (datedate, datetime)):
1840 value = value.timetuple()
1841 elif isinstance(value, (int, float)):
1842 value = time.gmtime(value)
1843 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value)
1844 if key in ('secure', 'httponly') and not value:
1845 continue
1846 self._cookies[name][key.replace('_', '-')] = value
1847
1848 def delete_cookie(self, key, **kwargs):
1849 """ Delete a cookie. Be sure to use the same `domain` and `path`
1850 settings as used to create the cookie. """
1851 kwargs['max_age'] = -1
1852 kwargs['expires'] = 0
1853 self.set_cookie(key, '', **kwargs)
1854
1855 def __repr__(self):
1856 out = ''
1857 for name, value in self.headerlist:
1858 out += '%s: %s\n' % (name.title(), value.strip())
1859 return out
1860
1861
1862 def _local_property():
1863 ls = threading.local()
1864
1865 def fget(_):
1866 try:
1867 return ls.var
1868 except AttributeError:
1869 raise RuntimeError("Request context not initialized.")
1870
1871 def fset(_, value):
1872 ls.var = value
1873
1874 def fdel(_):
1875 del ls.var
1876
1877 return property(fget, fset, fdel, 'Thread-local property')
1878
1879
1880 class LocalRequest(BaseRequest):
1881 """ A thread-local subclass of :class:`BaseRequest` with a different
1882 set of attributes for each thread. There is usually only one global
1883 instance of this class (:data:`request`). If accessed during a
1884 request/response cycle, this instance always refers to the *current*
1885 request (even on a multithreaded server). """
1886 bind = BaseRequest.__init__
1887 environ = _local_property()
1888
1889
1890 class LocalResponse(BaseResponse):
1891 """ A thread-local subclass of :class:`BaseResponse` with a different
1892 set of attributes for each thread. There is usually only one global
1893 instance of this class (:data:`response`). Its attributes are used
1894 to build the HTTP response at the end of the request/response cycle.
1895 """
1896 bind = BaseResponse.__init__
1897 _status_line = _local_property()
1898 _status_code = _local_property()
1899 _cookies = _local_property()
1900 _headers = _local_property()
1901 body = _local_property()
1902
1903
1904 Request = BaseRequest
1905 Response = BaseResponse
1906
1907
1908 class HTTPResponse(Response, BottleException):
1909 def __init__(self, body='', status=None, headers=None, **more_headers):
1910 super(HTTPResponse, self).__init__(body, status, headers, **more_headers)
1911
1912 def apply(self, other):
1913 other._status_code = self._status_code
1914 other._status_line = self._status_line
1915 other._headers = self._headers
1916 other._cookies = self._cookies
1917 other.body = self.body
1918
1919
1920 class HTTPError(HTTPResponse):
1921 default_status = 500
1922
1923 def __init__(self,
1924 status=None,
1925 body=None,
1926 exception=None,
1927 traceback=None, **more_headers):
1928 self.exception = exception
1929 self.traceback = traceback
1930 super(HTTPError, self).__init__(body, status, **more_headers)
1931
1932 ###############################################################################
1933 # Plugins ######################################################################
1934 ###############################################################################
1935
1936
1937 class PluginError(BottleException):
1938 pass
1939
1940
1941 class JSONPlugin(object):
1942 name = 'json'
1943 api = 2
1944
1945 def __init__(self, json_dumps=json_dumps):
1946 self.json_dumps = json_dumps
1947
1948 def apply(self, callback, _):
1949 dumps = self.json_dumps
1950 if not dumps: return callback
1951
1952 def wrapper(*a, **ka):
1953 try:
1954 rv = callback(*a, **ka)
1955 except HTTPError:
1956 rv = _e()
1957
1958 if isinstance(rv, dict):
1959 #Attempt to serialize, raises exception on failure
1960 json_response = dumps(rv)
1961 #Set content type only if serialization successful
1962 response.content_type = 'application/json'
1963 return json_response
1964 elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict):
1965 rv.body = dumps(rv.body)
1966 rv.content_type = 'application/json'
1967 return rv
1968
1969 return wrapper
1970
1971
1972 class TemplatePlugin(object):
1973 """ This plugin applies the :func:`view` decorator to all routes with a
1974 `template` config parameter. If the parameter is a tuple, the second
1975 element must be a dict with additional options (e.g. `template_engine`)
1976 or default variables for the template. """
1977 name = 'template'
1978 api = 2
1979
1980 def setup(self, app):
1981 app.tpl = self
1982
1983 def apply(self, callback, route):
1984 conf = route.config.get('template')
1985 if isinstance(conf, (tuple, list)) and len(conf) == 2:
1986 return view(conf[0], **conf[1])(callback)
1987 elif isinstance(conf, str):
1988 return view(conf)(callback)
1989 else:
1990 return callback
1991
1992
1993
1994
1995 #: Not a plugin, but part of the plugin API. TODO: Find a better place.
1996 class _ImportRedirect(object):
1997 def __init__(self, name, impmask):
1998 """ Create a virtual package that redirects imports (see PEP 302). """
1999 self.name = name
2000 self.impmask = impmask
2001 self.module = sys.modules.setdefault(name, imp.new_module(name))
2002 self.module.__dict__.update({
2003 '__file__': __file__,
2004 '__path__': [],
2005 '__all__': [],
2006 '__loader__': self
2007 })
2008 sys.meta_path.append(self)
2009
2010 def find_module(self, fullname, path=None):
2011 if '.' not in fullname: return
2012 packname = fullname.rsplit('.', 1)[0]
2013 if packname != self.name: return
2014 return self
2015
2016 def load_module(self, fullname):
2017 if fullname in sys.modules: return sys.modules[fullname]
2018 modname = fullname.rsplit('.', 1)[1]
2019 realname = self.impmask % modname
2020 __import__(realname)
2021 module = sys.modules[fullname] = sys.modules[realname]
2022 setattr(self.module, modname, module)
2023 module.__loader__ = self
2024 return module
2025
2026 ###############################################################################
2027 # Common Utilities #############################################################
2028 ###############################################################################
2029
2030
2031 class MultiDict(DictMixin):
2032 """ This dict stores multiple values per key, but behaves exactly like a
2033 normal dict in that it returns only the newest value for any given key.
2034 There are special methods available to access the full list of values.
2035 """
2036
2037 def __init__(self, *a, **k):
2038 self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
2039
2040 def __len__(self):
2041 return len(self.dict)
2042
2043 def __iter__(self):
2044 return iter(self.dict)
2045
2046 def __contains__(self, key):
2047 return key in self.dict
2048
2049 def __delitem__(self, key):
2050 del self.dict[key]
2051
2052 def __getitem__(self, key):
2053 return self.dict[key][-1]
2054
2055 def __setitem__(self, key, value):
2056 self.append(key, value)
2057
2058 def keys(self):
2059 return self.dict.keys()
2060
2061 if py3k:
2062
2063 def values(self):
2064 return (v[-1] for v in self.dict.values())
2065
2066 def items(self):
2067 return ((k, v[-1]) for k, v in self.dict.items())
2068
2069 def allitems(self):
2070 return ((k, v) for k, vl in self.dict.items() for v in vl)
2071
2072 iterkeys = keys
2073 itervalues = values
2074 iteritems = items
2075 iterallitems = allitems
2076
2077 else:
2078
2079 def values(self):
2080 return [v[-1] for v in self.dict.values()]
2081
2082 def items(self):
2083 return [(k, v[-1]) for k, v in self.dict.items()]
2084
2085 def iterkeys(self):
2086 return self.dict.iterkeys()
2087
2088 def itervalues(self):
2089 return (v[-1] for v in self.dict.itervalues())
2090
2091 def iteritems(self):
2092 return ((k, v[-1]) for k, v in self.dict.iteritems())
2093
2094 def iterallitems(self):
2095 return ((k, v) for k, vl in self.dict.iteritems() for v in vl)
2096
2097 def allitems(self):
2098 return [(k, v) for k, vl in self.dict.iteritems() for v in vl]
2099
2100 def get(self, key, default=None, index=-1, type=None):
2101 """ Return the most recent value for a key.
2102
2103 :param default: The default value to be returned if the key is not
2104 present or the type conversion fails.
2105 :param index: An index for the list of available values.
2106 :param type: If defined, this callable is used to cast the value
2107 into a specific type. Exception are suppressed and result in
2108 the default value to be returned.
2109 """
2110 try:
2111 val = self.dict[key][index]
2112 return type(val) if type else val
2113 except Exception:
2114 pass
2115 return default
2116
2117 def append(self, key, value):
2118 """ Add a new value to the list of values for this key. """
2119 self.dict.setdefault(key, []).append(value)
2120
2121 def replace(self, key, value):
2122 """ Replace the list of values with a single value. """
2123 self.dict[key] = [value]
2124
2125 def getall(self, key):
2126 """ Return a (possibly empty) list of values for a key. """
2127 return self.dict.get(key) or []
2128
2129 #: Aliases for WTForms to mimic other multi-dict APIs (Django)
2130 getone = get
2131 getlist = getall
2132
2133
2134 class FormsDict(MultiDict):
2135 """ This :class:`MultiDict` subclass is used to store request form data.
2136 Additionally to the normal dict-like item access methods (which return
2137 unmodified data as native strings), this container also supports
2138 attribute-like access to its values. Attributes are automatically de-
2139 or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing
2140 attributes default to an empty string. """
2141
2142 #: Encoding used for attribute values.
2143 input_encoding = 'utf8'
2144 #: If true (default), unicode strings are first encoded with `latin1`
2145 #: and then decoded to match :attr:`input_encoding`.
2146 recode_unicode = True
2147
2148 def _fix(self, s, encoding=None):
2149 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI
2150 return s.encode('latin1').decode(encoding or self.input_encoding)
2151 elif isinstance(s, bytes): # Python 2 WSGI
2152 return s.decode(encoding or self.input_encoding)
2153 else:
2154 return s
2155
2156 def decode(self, encoding=None):
2157 """ Returns a copy with all keys and values de- or recoded to match
2158 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a
2159 unicode dictionary. """
2160 copy = FormsDict()
2161 enc = copy.input_encoding = encoding or self.input_encoding
2162 copy.recode_unicode = False
2163 for key, value in self.allitems():
2164 copy.append(self._fix(key, enc), self._fix(value, enc))
2165 return copy
2166
2167 def getunicode(self, name, default=None, encoding=None):
2168 """ Return the value as a unicode string, or the default. """
2169 try:
2170 return self._fix(self[name], encoding)
2171 except (UnicodeError, KeyError):
2172 return default
2173
2174 def __getattr__(self, name, default=unicode()):
2175 # Without this guard, pickle generates a cryptic TypeError:
2176 if name.startswith('__') and name.endswith('__'):
2177 return super(FormsDict, self).__getattr__(name)
2178 return self.getunicode(name, default=default)
2179
2180
2181 class HeaderDict(MultiDict):
2182 """ A case-insensitive version of :class:`MultiDict` that defaults to
2183 replace the old value instead of appending it. """
2184
2185 def __init__(self, *a, **ka):
2186 self.dict = {}
2187 if a or ka: self.update(*a, **ka)
2188
2189 def __contains__(self, key):
2190 return _hkey(key) in self.dict
2191
2192 def __delitem__(self, key):
2193 del self.dict[_hkey(key)]
2194
2195 def __getitem__(self, key):
2196 return self.dict[_hkey(key)][-1]
2197
2198 def __setitem__(self, key, value):
2199 self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
2200 str(value)]
2201
2202 def append(self, key, value):
2203 self.dict.setdefault(_hkey(key), []).append(
2204 value if isinstance(value, unicode) else str(value))
2205
2206 def replace(self, key, value):
2207 self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
2208 str(value)]
2209
2210 def getall(self, key):
2211 return self.dict.get(_hkey(key)) or []
2212
2213 def get(self, key, default=None, index=-1):
2214 return MultiDict.get(self, _hkey(key), default, index)
2215
2216 def filter(self, names):
2217 for name in [_hkey(n) for n in names]:
2218 if name in self.dict:
2219 del self.dict[name]
2220
2221
2222 class WSGIHeaderDict(DictMixin):
2223 """ This dict-like class wraps a WSGI environ dict and provides convenient
2224 access to HTTP_* fields. Keys and values are native strings
2225 (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI
2226 environment contains non-native string values, these are de- or encoded
2227 using a lossless 'latin1' character set.
2228
2229 The API will remain stable even on changes to the relevant PEPs.
2230 Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one
2231 that uses non-native strings.)
2232 """
2233 #: List of keys that do not have a ``HTTP_`` prefix.
2234 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
2235
2236 def __init__(self, environ):
2237 self.environ = environ
2238
2239 def _ekey(self, key):
2240 """ Translate header field name to CGI/WSGI environ key. """
2241 key = key.replace('-', '_').upper()
2242 if key in self.cgikeys:
2243 return key
2244 return 'HTTP_' + key
2245
2246 def raw(self, key, default=None):
2247 """ Return the header value as is (may be bytes or unicode). """
2248 return self.environ.get(self._ekey(key), default)
2249
2250 def __getitem__(self, key):
2251 val = self.environ[self._ekey(key)]
2252 if py3k:
2253 if isinstance(val, unicode):
2254 val = val.encode('latin1').decode('utf8')
2255 else:
2256 val = val.decode('utf8')
2257 return val
2258
2259 def __setitem__(self, key, value):
2260 raise TypeError("%s is read-only." % self.__class__)
2261
2262 def __delitem__(self, key):
2263 raise TypeError("%s is read-only." % self.__class__)
2264
2265 def __iter__(self):
2266 for key in self.environ:
2267 if key[:5] == 'HTTP_':
2268 yield _hkey(key[5:])
2269 elif key in self.cgikeys:
2270 yield _hkey(key)
2271
2272 def keys(self):
2273 return [x for x in self]
2274
2275 def __len__(self):
2276 return len(self.keys())
2277
2278 def __contains__(self, key):
2279 return self._ekey(key) in self.environ
2280
2281
2282 class ConfigDict(dict):
2283 """ A dict-like configuration storage with additional support for
2284 namespaces, validators, meta-data, on_change listeners and more.
2285 """
2286
2287 __slots__ = ('_meta', '_change_listener', '_fallbacks')
2288
2289 def __init__(self):
2290 self._meta = {}
2291 self._change_listener = []
2292 self._fallbacks = []
2293
2294 def load_module(self, path, squash):
2295 """ Load values from a Python module.
2296 :param squash: Squash nested dicts into namespaces by using
2297 load_dict(), otherwise use update()
2298 Example: load_config('my.app.settings', True)
2299 Example: load_config('my.app.settings', False)
2300 """
2301 config_obj = __import__(path)
2302 obj = dict([(key, getattr(config_obj, key))
2303 for key in dir(config_obj) if key.isupper()])
2304 if squash:
2305 self.load_dict(obj)
2306 else:
2307 self.update(obj)
2308 return self
2309
2310 def load_config(self, filename):
2311 """ Load values from an ``*.ini`` style config file.
2312
2313 If the config file contains sections, their names are used as
2314 namespaces for the values within. The two special sections
2315 ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix).
2316 """
2317 conf = ConfigParser()
2318 conf.read(filename)
2319 for section in conf.sections():
2320 for key, value in conf.items(section):
2321 if section not in ('DEFAULT', 'bottle'):
2322 key = section + '.' + key
2323 self[key] = value
2324 return self
2325
2326 def load_dict(self, source, namespace=''):
2327 """ Load values from a dictionary structure. Nesting can be used to
2328 represent namespaces.
2329
2330 >>> c = ConfigDict()
2331 >>> c.load_dict({'some': {'namespace': {'key': 'value'} } })
2332 {'some.namespace.key': 'value'}
2333 """
2334 for key, value in source.items():
2335 if isinstance(key, basestring):
2336 nskey = (namespace + '.' + key).strip('.')
2337 if isinstance(value, dict):
2338 self.load_dict(value, namespace=nskey)
2339 else:
2340 self[nskey] = value
2341 else:
2342 raise TypeError('Key has type %r (not a string)' % type(key))
2343 return self
2344
2345 def update(self, *a, **ka):
2346 """ If the first parameter is a string, all keys are prefixed with this
2347 namespace. Apart from that it works just as the usual dict.update().
2348 Example: ``update('some.namespace', key='value')`` """
2349 prefix = ''
2350 if a and isinstance(a[0], basestring):
2351 prefix = a[0].strip('.') + '.'
2352 a = a[1:]
2353 for key, value in dict(*a, **ka).items():
2354 self[prefix + key] = value
2355
2356 def setdefault(self, key, value):
2357 if key not in self:
2358 self[key] = value
2359 return self[key]
2360
2361 def __setitem__(self, key, value):
2362 if not isinstance(key, basestring):
2363 raise TypeError('Key has type %r (not a string)' % type(key))
2364
2365 value = self.meta_get(key, 'filter', lambda x: x)(value)
2366 if key in self and self[key] is value:
2367 return
2368 self._on_change(key, value)
2369 dict.__setitem__(self, key, value)
2370
2371 def __delitem__(self, key):
2372 self._on_change(key, None)
2373 dict.__delitem__(self, key)
2374
2375 def __missing__(self, key):
2376 for fallback in self._fallbacks:
2377 if key in fallback:
2378 value = self[key] = fallback[key]
2379 self.meta_set(key, 'fallback', fallback)
2380 return value
2381 raise KeyError(key)
2382
2383 def _on_change(self, key, value):
2384 for cb in self._change_listener:
2385 if cb(self, key, value):
2386 return True
2387
2388 def _add_change_listener(self, func):
2389 self._change_listener.append(func)
2390 return func
2391
2392 def _set_fallback(self, fallback):
2393 self._fallbacks.append(fallback)
2394
2395 @fallback._add_change_listener
2396 def fallback_update(conf, key, value):
2397 if self.meta_get(key, 'fallback') is conf:
2398 self.meta_set(key, 'fallback', None)
2399 dict.__delitem__(self, key)
2400
2401 @self._add_change_listener
2402 def self_update(conf, key, value):
2403 if conf.meta_get(key, 'fallback'):
2404 conf.meta_set(key, 'fallback', None)
2405
2406 def meta_get(self, key, metafield, default=None):
2407 """ Return the value of a meta field for a key. """
2408 return self._meta.get(key, {}).get(metafield, default)
2409
2410 def meta_set(self, key, metafield, value):
2411 """ Set the meta field for a key to a new value. """
2412 self._meta.setdefault(key, {})[metafield] = value
2413
2414 def meta_list(self, key):
2415 """ Return an iterable of meta field names defined for a key. """
2416 return self._meta.get(key, {}).keys()
2417
2418
2419 class AppStack(list):
2420 """ A stack-like list. Calling it returns the head of the stack. """
2421
2422 def __call__(self):
2423 """ Return the current default application. """
2424 return self.default
2425
2426 def push(self, value=None):
2427 """ Add a new :class:`Bottle` instance to the stack """
2428 if not isinstance(value, Bottle):
2429 value = Bottle()
2430 self.append(value)
2431 return value
2432 new_app = push
2433 @property
2434 def default(self):
2435 try:
2436 return self[-1]
2437 except IndexError:
2438 return self.push()
2439
2440
2441 class WSGIFileWrapper(object):
2442 def __init__(self, fp, buffer_size=1024 * 64):
2443 self.fp, self.buffer_size = fp, buffer_size
2444 for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'):
2445 if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr))
2446
2447 def __iter__(self):
2448 buff, read = self.buffer_size, self.read
2449 while True:
2450 part = read(buff)
2451 if not part: return
2452 yield part
2453
2454
2455 class _closeiter(object):
2456 """ This only exists to be able to attach a .close method to iterators that
2457 do not support attribute assignment (most of itertools). """
2458
2459 def __init__(self, iterator, close=None):
2460 self.iterator = iterator
2461 self.close_callbacks = makelist(close)
2462
2463 def __iter__(self):
2464 return iter(self.iterator)
2465
2466 def close(self):
2467 for func in self.close_callbacks:
2468 func()
2469
2470
2471 class ResourceManager(object):
2472 """ This class manages a list of search paths and helps to find and open
2473 application-bound resources (files).
2474
2475 :param base: default value for :meth:`add_path` calls.
2476 :param opener: callable used to open resources.
2477 :param cachemode: controls which lookups are cached. One of 'all',
2478 'found' or 'none'.
2479 """
2480
2481 def __init__(self, base='./', opener=open, cachemode='all'):
2482 self.opener = opener
2483 self.base = base
2484 self.cachemode = cachemode
2485
2486 #: A list of search paths. See :meth:`add_path` for details.
2487 self.path = []
2488 #: A cache for resolved paths. ``res.cache.clear()`` clears the cache.
2489 self.cache = {}
2490
2491 def add_path(self, path, base=None, index=None, create=False):
2492 """ Add a new path to the list of search paths. Return False if the
2493 path does not exist.
2494
2495 :param path: The new search path. Relative paths are turned into
2496 an absolute and normalized form. If the path looks like a file
2497 (not ending in `/`), the filename is stripped off.
2498 :param base: Path used to absolutize relative search paths.
2499 Defaults to :attr:`base` which defaults to ``os.getcwd()``.
2500 :param index: Position within the list of search paths. Defaults
2501 to last index (appends to the list).
2502
2503 The `base` parameter makes it easy to reference files installed
2504 along with a python module or package::
2505
2506 res.add_path('./resources/', __file__)
2507 """
2508 base = os.path.abspath(os.path.dirname(base or self.base))
2509 path = os.path.abspath(os.path.join(base, os.path.dirname(path)))
2510 path += os.sep
2511 if path in self.path:
2512 self.path.remove(path)
2513 if create and not os.path.isdir(path):
2514 os.makedirs(path)
2515 if index is None:
2516 self.path.append(path)
2517 else:
2518 self.path.insert(index, path)
2519 self.cache.clear()
2520 return os.path.exists(path)
2521
2522 def __iter__(self):
2523 """ Iterate over all existing files in all registered paths. """
2524 search = self.path[:]
2525 while search:
2526 path = search.pop()
2527 if not os.path.isdir(path): continue
2528 for name in os.listdir(path):
2529 full = os.path.join(path, name)
2530 if os.path.isdir(full): search.append(full)
2531 else: yield full
2532
2533 def lookup(self, name):
2534 """ Search for a resource and return an absolute file path, or `None`.
2535
2536 The :attr:`path` list is searched in order. The first match is
2537 returend. Symlinks are followed. The result is cached to speed up
2538 future lookups. """
2539 if name not in self.cache or DEBUG:
2540 for path in self.path:
2541 fpath = os.path.join(path, name)
2542 if os.path.isfile(fpath):
2543 if self.cachemode in ('all', 'found'):
2544 self.cache[name] = fpath
2545 return fpath
2546 if self.cachemode == 'all':
2547 self.cache[name] = None
2548 return self.cache[name]
2549
2550 def open(self, name, mode='r', *args, **kwargs):
2551 """ Find a resource and return a file object, or raise IOError. """
2552 fname = self.lookup(name)
2553 if not fname: raise IOError("Resource %r not found." % name)
2554 return self.opener(fname, mode=mode, *args, **kwargs)
2555
2556
2557 class FileUpload(object):
2558 def __init__(self, fileobj, name, filename, headers=None):
2559 """ Wrapper for file uploads. """
2560 #: Open file(-like) object (BytesIO buffer or temporary file)
2561 self.file = fileobj
2562 #: Name of the upload form field
2563 self.name = name
2564 #: Raw filename as sent by the client (may contain unsafe characters)
2565 self.raw_filename = filename
2566 #: A :class:`HeaderDict` with additional headers (e.g. content-type)
2567 self.headers = HeaderDict(headers) if headers else HeaderDict()
2568
2569 content_type = HeaderProperty('Content-Type')
2570 content_length = HeaderProperty('Content-Length', reader=int, default=-1)
2571
2572 @cached_property
2573 def filename(self):
2574 """ Name of the file on the client file system, but normalized to ensure
2575 file system compatibility. An empty filename is returned as 'empty'.
2576
2577 Only ASCII letters, digits, dashes, underscores and dots are
2578 allowed in the final filename. Accents are removed, if possible.
2579 Whitespace is replaced by a single dash. Leading or tailing dots
2580 or dashes are removed. The filename is limited to 255 characters.
2581 """
2582 fname = self.raw_filename
2583 if not isinstance(fname, unicode):
2584 fname = fname.decode('utf8', 'ignore')
2585 fname = normalize('NFKD', fname)
2586 fname = fname.encode('ASCII', 'ignore').decode('ASCII')
2587 fname = os.path.basename(fname.replace('\\', os.path.sep))
2588 fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip()
2589 fname = re.sub(r'[-\s]+', '-', fname).strip('.-')
2590 return fname[:255] or 'empty'
2591
2592 def _copy_file(self, fp, chunk_size=2 ** 16):
2593 read, write, offset = self.file.read, fp.write, self.file.tell()
2594 while 1:
2595 buf = read(chunk_size)
2596 if not buf: break
2597 write(buf)
2598 self.file.seek(offset)
2599
2600 def save(self, destination, overwrite=False, chunk_size=2 ** 16):
2601 """ Save file to disk or copy its content to an open file(-like) object.
2602 If *destination* is a directory, :attr:`filename` is added to the
2603 path. Existing files are not overwritten by default (IOError).
2604
2605 :param destination: File path, directory or file(-like) object.
2606 :param overwrite: If True, replace existing files. (default: False)
2607 :param chunk_size: Bytes to read at a time. (default: 64kb)
2608 """
2609 if isinstance(destination, basestring): # Except file-likes here
2610 if os.path.isdir(destination):
2611 destination = os.path.join(destination, self.filename)
2612 if not overwrite and os.path.exists(destination):
2613 raise IOError('File exists.')
2614 with open(destination, 'wb') as fp:
2615 self._copy_file(fp, chunk_size)
2616 else:
2617 self._copy_file(destination, chunk_size)
2618
2619 ###############################################################################
2620 # Application Helper ###########################################################
2621 ###############################################################################
2622
2623
2624 def abort(code=500, text='Unknown Error.'):
2625 """ Aborts execution and causes a HTTP error. """
2626 raise HTTPError(code, text)
2627
2628
2629 def redirect(url, code=None):
2630 """ Aborts execution and causes a 303 or 302 redirect, depending on
2631 the HTTP protocol version. """
2632 if not code:
2633 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
2634 res = response.copy(cls=HTTPResponse)
2635 res.status = code
2636 res.body = ""
2637 res.set_header('Location', urljoin(request.url, url))
2638 raise res
2639
2640
2641 def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024):
2642 """ Yield chunks from a range in a file. No chunk is bigger than maxread."""
2643 fp.seek(offset)
2644 while bytes > 0:
2645 part = fp.read(min(bytes, maxread))
2646 if not part: break
2647 bytes -= len(part)
2648 yield part
2649
2650
2651 def static_file(filename, root,
2652 mimetype=True,
2653 download=False,
2654 charset='UTF-8',
2655 etag=None):
2656 """ Open a file in a safe way and return an instance of :exc:`HTTPResponse`
2657 that can be sent back to the client.
2658
2659 :param filename: Name or path of the file to send, relative to ``root``.
2660 :param root: Root path for file lookups. Should be an absolute directory
2661 path.
2662 :param mimetype: Provide the content-type header (default: guess from
2663 file extension)
2664 :param download: If True, ask the browser to open a `Save as...` dialog
2665 instead of opening the file with the associated program. You can
2666 specify a custom filename as a string. If not specified, the
2667 original filename is used (default: False).
2668 :param charset: The charset for files with a ``text/*`` mime-type.
2669 (default: UTF-8)
2670 :param etag: Provide a pre-computed ETag header. If set to ``False``,
2671 ETag handling is disabled. (default: auto-generate ETag header)
2672
2673 While checking user input is always a good idea, this function provides
2674 additional protection against malicious ``filename`` parameters from
2675 breaking out of the ``root`` directory and leaking sensitive information
2676 to an attacker.
2677
2678 Read-protected files or files outside of the ``root`` directory are
2679 answered with ``403 Access Denied``. Missing files result in a
2680 ``404 Not Found`` response. Conditional requests (``If-Modified-Since``,
2681 ``If-None-Match``) are answered with ``304 Not Modified`` whenever
2682 possible. ``HEAD`` and ``Range`` requests (used by download managers to
2683 check or continue partial downloads) are also handled automatically.
2684
2685 """
2686
2687 root = os.path.join(os.path.abspath(root), '')
2688 filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
2689 headers = dict()
2690
2691 if not filename.startswith(root):
2692 return HTTPError(403, "Access denied.")
2693 if not os.path.exists(filename) or not os.path.isfile(filename):
2694 return HTTPError(404, "File does not exist.")
2695 if not os.access(filename, os.R_OK):
2696 return HTTPError(403, "You do not have permission to access this file.")
2697
2698 if mimetype is True:
2699 if download and download != True:
2700 mimetype, encoding = mimetypes.guess_type(download)
2701 else:
2702 mimetype, encoding = mimetypes.guess_type(filename)
2703 if encoding: headers['Content-Encoding'] = encoding
2704
2705 if mimetype:
2706 if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
2707 and charset and 'charset' not in mimetype:
2708 mimetype += '; charset=%s' % charset
2709 headers['Content-Type'] = mimetype
2710
2711 if download:
2712 download = os.path.basename(filename if download == True else download)
2713 headers['Content-Disposition'] = 'attachment; filename="%s"' % download
2714
2715 stats = os.stat(filename)
2716 headers['Content-Length'] = clen = stats.st_size
2717 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
2718 headers['Last-Modified'] = lm
2719 headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
2720
2721 getenv = request.environ.get
2722
2723 if etag is None:
2724 etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
2725 clen, filename)
2726 etag = hashlib.sha1(tob(etag)).hexdigest()
2727
2728 if etag:
2729 headers['ETag'] = etag
2730 check = getenv('HTTP_IF_NONE_MATCH')
2731 if check and check == etag:
2732 return HTTPResponse(status=304, **headers)
2733
2734 ims = getenv('HTTP_IF_MODIFIED_SINCE')
2735 if ims:
2736 ims = parse_date(ims.split(";")[0].strip())
2737 if ims is not None and ims >= int(stats.st_mtime):
2738 return HTTPResponse(status=304, **headers)
2739
2740 body = '' if request.method == 'HEAD' else open(filename, 'rb')
2741
2742 headers["Accept-Ranges"] = "bytes"
2743 range_header = getenv('HTTP_RANGE')
2744 if range_header:
2745 ranges = list(parse_range_header(range_header, clen))
2746 if not ranges:
2747 return HTTPError(416, "Requested Range Not Satisfiable")
2748 offset, end = ranges[0]
2749 headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
2750 headers["Content-Length"] = str(end - offset)
2751 if body: body = _file_iter_range(body, offset, end - offset)
2752 return HTTPResponse(body, status=206, **headers)
2753 return HTTPResponse(body, **headers)
2754
2755 ###############################################################################
2756 # HTTP Utilities and MISC (TODO) ###############################################
2757 ###############################################################################
2758
2759
2760 def debug(mode=True):
2761 """ Change the debug level.
2762 There is only one debug level supported at the moment."""
2763 global DEBUG
2764 if mode: warnings.simplefilter('default')
2765 DEBUG = bool(mode)
2766
2767
2768 def http_date(value):
2769 if isinstance(value, (datedate, datetime)):
2770 value = value.utctimetuple()
2771 elif isinstance(value, (int, float)):
2772 value = time.gmtime(value)
2773 if not isinstance(value, basestring):
2774 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value)
2775 return value
2776
2777
2778 def parse_date(ims):
2779 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
2780 try:
2781 ts = email.utils.parsedate_tz(ims)
2782 return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone
2783 except (TypeError, ValueError, IndexError, OverflowError):
2784 return None
2785
2786
2787 def parse_auth(header):
2788 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
2789 try:
2790 method, data = header.split(None, 1)
2791 if method.lower() == 'basic':
2792 user, pwd = touni(base64.b64decode(tob(data))).split(':', 1)
2793 return user, pwd
2794 except (KeyError, ValueError):
2795 return None
2796
2797
2798 def parse_range_header(header, maxlen=0):
2799 """ Yield (start, end) ranges parsed from a HTTP Range header. Skip
2800 unsatisfiable ranges. The end index is non-inclusive."""
2801 if not header or header[:6] != 'bytes=': return
2802 ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r]
2803 for start, end in ranges:
2804 try:
2805 if not start: # bytes=-100 -> last 100 bytes
2806 start, end = max(0, maxlen - int(end)), maxlen
2807 elif not end: # bytes=100- -> all but the first 99 bytes
2808 start, end = int(start), maxlen
2809 else: # bytes=100-200 -> bytes 100-200 (inclusive)
2810 start, end = int(start), min(int(end) + 1, maxlen)
2811 if 0 <= start < end <= maxlen:
2812 yield start, end
2813 except ValueError:
2814 pass
2815
2816
2817 #: Header tokenizer used by _parse_http_header()
2818 _hsplit = re.compile('(?:(?:"((?:[^"\\\\]+|\\\\.)*)")|([^;,=]+))([;,=]?)').findall
2819
2820 def _parse_http_header(h):
2821 """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values
2822 and parameters. For non-standard or broken input, this implementation may return partial results.
2823 :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``)
2824 :return: List of (value, params) tuples. The second element is a (possibly empty) dict.
2825 """
2826 values = []
2827 if '"' not in h: # INFO: Fast path without regexp (~2x faster)
2828 for value in h.split(','):
2829 parts = value.split(';')
2830 values.append((parts[0].strip(), {}))
2831 for attr in parts[1:]:
2832 name, value = attr.split('=', 1)
2833 values[-1][1][name.strip()] = value.strip()
2834 else:
2835 lop, key, attrs = ',', None, {}
2836 for quoted, plain, tok in _hsplit(h):
2837 value = plain.strip() if plain else quoted.replace('\\"', '"')
2838 if lop == ',':
2839 attrs = {}
2840 values.append((value, attrs))
2841 elif lop == ';':
2842 if tok == '=':
2843 key = value
2844 else:
2845 attrs[value] = ''
2846 elif lop == '=' and key:
2847 attrs[key] = value
2848 key = None
2849 lop = tok
2850 return values
2851
2852
2853 def _parse_qsl(qs):
2854 r = []
2855 for pair in qs.replace(';', '&').split('&'):
2856 if not pair: continue
2857 nv = pair.split('=', 1)
2858 if len(nv) != 2: nv.append('')
2859 key = urlunquote(nv[0].replace('+', ' '))
2860 value = urlunquote(nv[1].replace('+', ' '))
2861 r.append((key, value))
2862 return r
2863
2864
2865 def _lscmp(a, b):
2866 """ Compares two strings in a cryptographically safe way:
2867 Runtime is not affected by length of common prefix. """
2868 return not sum(0 if x == y else 1
2869 for x, y in zip(a, b)) and len(a) == len(b)
2870
2871
2872 def cookie_encode(data, key, digestmod=None):
2873 """ Encode and sign a pickle-able object. Return a (byte) string """
2874 digestmod = digestmod or hashlib.sha256
2875 msg = base64.b64encode(pickle.dumps(data, -1))
2876 sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
2877 return tob('!') + sig + tob('?') + msg
2878
2879
2880 def cookie_decode(data, key, digestmod=None):
2881 """ Verify and decode an encoded string. Return an object or None."""
2882 data = tob(data)
2883 if cookie_is_encoded(data):
2884 sig, msg = data.split(tob('?'), 1)
2885 digestmod = digestmod or hashlib.sha256
2886 hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest()
2887 if _lscmp(sig[1:], base64.b64encode(hashed)):
2888 return pickle.loads(base64.b64decode(msg))
2889 return None
2890
2891
2892 def cookie_is_encoded(data):
2893 """ Return True if the argument looks like a encoded cookie."""
2894 return bool(data.startswith(tob('!')) and tob('?') in data)
2895
2896
2897 def html_escape(string):
2898 """ Escape HTML special characters ``&<>`` and quotes ``'"``. """
2899 return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\
2900 .replace('"', '&quot;').replace("'", '&#039;')
2901
2902
2903 def html_quote(string):
2904 """ Escape and quote a string to be used as an HTTP attribute."""
2905 return '"%s"' % html_escape(string).replace('\n', '&#10;')\
2906 .replace('\r', '&#13;').replace('\t', '&#9;')
2907
2908
2909 def yieldroutes(func):
2910 """ Return a generator for routes that match the signature (name, args)
2911 of the func parameter. This may yield more than one route if the function
2912 takes optional keyword arguments. The output is best described by example::
2913
2914 a() -> '/a'
2915 b(x, y) -> '/b/<x>/<y>'
2916 c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>'
2917 d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>'
2918 """
2919 path = '/' + func.__name__.replace('__', '/').lstrip('/')
2920 spec = getargspec(func)
2921 argc = len(spec[0]) - len(spec[3] or [])
2922 path += ('/<%s>' * argc) % tuple(spec[0][:argc])
2923 yield path
2924 for arg in spec[0][argc:]:
2925 path += '/<%s>' % arg
2926 yield path
2927
2928
2929 def path_shift(script_name, path_info, shift=1):
2930 """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
2931
2932 :return: The modified paths.
2933 :param script_name: The SCRIPT_NAME path.
2934 :param script_name: The PATH_INFO path.
2935 :param shift: The number of path fragments to shift. May be negative to
2936 change the shift direction. (default: 1)
2937 """
2938 if shift == 0: return script_name, path_info
2939 pathlist = path_info.strip('/').split('/')
2940 scriptlist = script_name.strip('/').split('/')
2941 if pathlist and pathlist[0] == '': pathlist = []
2942 if scriptlist and scriptlist[0] == '': scriptlist = []
2943 if 0 < shift <= len(pathlist):
2944 moved = pathlist[:shift]
2945 scriptlist = scriptlist + moved
2946 pathlist = pathlist[shift:]
2947 elif 0 > shift >= -len(scriptlist):
2948 moved = scriptlist[shift:]
2949 pathlist = moved + pathlist
2950 scriptlist = scriptlist[:shift]
2951 else:
2952 empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO'
2953 raise AssertionError("Cannot shift. Nothing left from %s" % empty)
2954 new_script_name = '/' + '/'.join(scriptlist)
2955 new_path_info = '/' + '/'.join(pathlist)
2956 if path_info.endswith('/') and pathlist: new_path_info += '/'
2957 return new_script_name, new_path_info
2958
2959
2960 def auth_basic(check, realm="private", text="Access denied"):
2961 """ Callback decorator to require HTTP auth (basic).
2962 TODO: Add route(check_auth=...) parameter. """
2963
2964 def decorator(func):
2965
2966 @functools.wraps(func)
2967 def wrapper(*a, **ka):
2968 user, password = request.auth or (None, None)
2969 if user is None or not check(user, password):
2970 err = HTTPError(401, text)
2971 err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
2972 return err
2973 return func(*a, **ka)
2974
2975 return wrapper
2976
2977 return decorator
2978
2979 # Shortcuts for common Bottle methods.
2980 # They all refer to the current default application.
2981
2982
2983 def make_default_app_wrapper(name):
2984 """ Return a callable that relays calls to the current default app. """
2985
2986 @functools.wraps(getattr(Bottle, name))
2987 def wrapper(*a, **ka):
2988 return getattr(app(), name)(*a, **ka)
2989
2990 return wrapper
2991
2992
2993 route = make_default_app_wrapper('route')
2994 get = make_default_app_wrapper('get')
2995 post = make_default_app_wrapper('post')
2996 put = make_default_app_wrapper('put')
2997 delete = make_default_app_wrapper('delete')
2998 patch = make_default_app_wrapper('patch')
2999 error = make_default_app_wrapper('error')
3000 mount = make_default_app_wrapper('mount')
3001 hook = make_default_app_wrapper('hook')
3002 install = make_default_app_wrapper('install')
3003 uninstall = make_default_app_wrapper('uninstall')
3004 url = make_default_app_wrapper('get_url')
3005
3006 ###############################################################################
3007 # Server Adapter ###############################################################
3008 ###############################################################################
3009
3010
3011 class ServerAdapter(object):
3012 quiet = False
3013
3014 def __init__(self, host='127.0.0.1', port=8080, **options):
3015 self.options = options
3016 self.host = host
3017 self.port = int(port)
3018
3019 def run(self, handler): # pragma: no cover
3020 pass
3021
3022 def __repr__(self):
3023 args = ', '.join(['%s=%s' % (k, repr(v))
3024 for k, v in self.options.items()])
3025 return "%s(%s)" % (self.__class__.__name__, args)
3026
3027
3028 class CGIServer(ServerAdapter):
3029 quiet = True
3030
3031 def run(self, handler): # pragma: no cover
3032 from wsgiref.handlers import CGIHandler
3033
3034 def fixed_environ(environ, start_response):
3035 environ.setdefault('PATH_INFO', '')
3036 return handler(environ, start_response)
3037
3038 CGIHandler().run(fixed_environ)
3039
3040
3041 class FlupFCGIServer(ServerAdapter):
3042 def run(self, handler): # pragma: no cover
3043 import flup.server.fcgi
3044 self.options.setdefault('bindAddress', (self.host, self.port))
3045 flup.server.fcgi.WSGIServer(handler, **self.options).run()
3046
3047
3048 class WSGIRefServer(ServerAdapter):
3049 def run(self, app): # pragma: no cover
3050 from wsgiref.simple_server import make_server
3051 from wsgiref.simple_server import WSGIRequestHandler, WSGIServer
3052 import socket
3053
3054 class FixedHandler(WSGIRequestHandler):
3055 def address_string(self): # Prevent reverse DNS lookups please.
3056 return self.client_address[0]
3057
3058 def log_request(*args, **kw):
3059 if not self.quiet:
3060 return WSGIRequestHandler.log_request(*args, **kw)
3061
3062 handler_cls = self.options.get('handler_class', FixedHandler)
3063 server_cls = self.options.get('server_class', WSGIServer)
3064
3065 if ':' in self.host: # Fix wsgiref for IPv6 addresses.
3066 if getattr(server_cls, 'address_family') == socket.AF_INET:
3067
3068 class server_cls(server_cls):
3069 address_family = socket.AF_INET6
3070
3071 self.srv = make_server(self.host, self.port, app, server_cls,
3072 handler_cls)
3073 self.port = self.srv.server_port # update port actual port (0 means random)
3074 try:
3075 self.srv.serve_forever()
3076 except KeyboardInterrupt:
3077 self.srv.server_close() # Prevent ResourceWarning: unclosed socket
3078 raise
3079
3080
3081 class CherryPyServer(ServerAdapter):
3082 def run(self, handler): # pragma: no cover
3083 from cherrypy import wsgiserver
3084 self.options['bind_addr'] = (self.host, self.port)
3085 self.options['wsgi_app'] = handler
3086
3087 certfile = self.options.get('certfile')
3088 if certfile:
3089 del self.options['certfile']
3090 keyfile = self.options.get('keyfile')
3091 if keyfile:
3092 del self.options['keyfile']
3093
3094 server = wsgiserver.CherryPyWSGIServer(**self.options)
3095 if certfile:
3096 server.ssl_certificate = certfile
3097 if keyfile:
3098 server.ssl_private_key = keyfile
3099
3100 try:
3101 server.start()
3102 finally:
3103 server.stop()
3104
3105
3106 class WaitressServer(ServerAdapter):
3107 def run(self, handler):
3108 from waitress import serve
3109 serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options)
3110
3111
3112 class PasteServer(ServerAdapter):
3113 def run(self, handler): # pragma: no cover
3114 from paste import httpserver
3115 from paste.translogger import TransLogger
3116 handler = TransLogger(handler, setup_console_handler=(not self.quiet))
3117 httpserver.serve(handler,
3118 host=self.host,
3119 port=str(self.port), **self.options)
3120
3121
3122 class MeinheldServer(ServerAdapter):
3123 def run(self, handler):
3124 from meinheld import server
3125 server.listen((self.host, self.port))
3126 server.run(handler)
3127
3128
3129 class FapwsServer(ServerAdapter):
3130 """ Extremely fast webserver using libev. See http://www.fapws.org/ """
3131
3132 def run(self, handler): # pragma: no cover
3133 import fapws._evwsgi as evwsgi
3134 from fapws import base, config
3135 port = self.port
3136 if float(config.SERVER_IDENT[-2:]) > 0.4:
3137 # fapws3 silently changed its API in 0.5
3138 port = str(port)
3139 evwsgi.start(self.host, port)
3140 # fapws3 never releases the GIL. Complain upstream. I tried. No luck.
3141 if 'BOTTLE_CHILD' in os.environ and not self.quiet:
3142 _stderr("WARNING: Auto-reloading does not work with Fapws3.\n")
3143 _stderr(" (Fapws3 breaks python thread support)\n")
3144 evwsgi.set_base_module(base)
3145
3146 def app(environ, start_response):
3147 environ['wsgi.multiprocess'] = False
3148 return handler(environ, start_response)
3149
3150 evwsgi.wsgi_cb(('', app))
3151 evwsgi.run()
3152
3153
3154 class TornadoServer(ServerAdapter):
3155 """ The super hyped asynchronous server by facebook. Untested. """
3156
3157 def run(self, handler): # pragma: no cover
3158 import tornado.wsgi, tornado.httpserver, tornado.ioloop
3159 container = tornado.wsgi.WSGIContainer(handler)
3160 server = tornado.httpserver.HTTPServer(container)
3161 server.listen(port=self.port, address=self.host)
3162 tornado.ioloop.IOLoop.instance().start()
3163
3164
3165 class AppEngineServer(ServerAdapter):
3166 """ Adapter for Google App Engine. """
3167 quiet = True
3168
3169 def run(self, handler):
3170 from google.appengine.ext.webapp import util
3171 # A main() function in the handler script enables 'App Caching'.
3172 # Lets makes sure it is there. This _really_ improves performance.
3173 module = sys.modules.get('__main__')
3174 if module and not hasattr(module, 'main'):
3175 module.main = lambda: util.run_wsgi_app(handler)
3176 util.run_wsgi_app(handler)
3177
3178
3179 class TwistedServer(ServerAdapter):
3180 """ Untested. """
3181
3182 def run(self, handler):
3183 from twisted.web import server, wsgi
3184 from twisted.python.threadpool import ThreadPool
3185 from twisted.internet import reactor
3186 thread_pool = ThreadPool()
3187 thread_pool.start()
3188 reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
3189 factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
3190 reactor.listenTCP(self.port, factory, interface=self.host)
3191 if not reactor.running:
3192 reactor.run()
3193
3194
3195 class DieselServer(ServerAdapter):
3196 """ Untested. """
3197
3198 def run(self, handler):
3199 from diesel.protocols.wsgi import WSGIApplication
3200 app = WSGIApplication(handler, port=self.port)
3201 app.run()
3202
3203
3204 class GeventServer(ServerAdapter):
3205 """ Untested. Options:
3206
3207 * See gevent.wsgi.WSGIServer() documentation for more options.
3208 """
3209
3210 def run(self, handler):
3211 from gevent import pywsgi, local
3212 if not isinstance(threading.local(), local.local):
3213 msg = "Bottle requires gevent.monkey.patch_all() (before import)"
3214 raise RuntimeError(msg)
3215 if self.quiet:
3216 self.options['log'] = None
3217 address = (self.host, self.port)
3218 server = pywsgi.WSGIServer(address, handler, **self.options)
3219 if 'BOTTLE_CHILD' in os.environ:
3220 import signal
3221 signal.signal(signal.SIGINT, lambda s, f: server.stop())
3222 server.serve_forever()
3223
3224
3225 class GeventSocketIOServer(ServerAdapter):
3226 def run(self, handler):
3227 from socketio import server
3228 address = (self.host, self.port)
3229 server.SocketIOServer(address, handler, **self.options).serve_forever()
3230
3231
3232 class GunicornServer(ServerAdapter):
3233 """ Untested. See http://gunicorn.org/configure.html for options. """
3234
3235 def run(self, handler):
3236 from gunicorn.app.base import Application
3237
3238 config = {'bind': "%s:%d" % (self.host, int(self.port))}
3239 config.update(self.options)
3240
3241 class GunicornApplication(Application):
3242 def init(self, parser, opts, args):
3243 return config
3244
3245 def load(self):
3246 return handler
3247
3248 GunicornApplication().run()
3249
3250
3251 class EventletServer(ServerAdapter):
3252 """ Untested. Options:
3253
3254 * `backlog` adjust the eventlet backlog parameter which is the maximum
3255 number of queued connections. Should be at least 1; the maximum
3256 value is system-dependent.
3257 * `family`: (default is 2) socket family, optional. See socket
3258 documentation for available families.
3259 """
3260
3261 def run(self, handler):
3262 from eventlet import wsgi, listen, patcher
3263 if not patcher.is_monkey_patched(os):
3264 msg = "Bottle requires eventlet.monkey_patch() (before import)"
3265 raise RuntimeError(msg)
3266 socket_args = {}
3267 for arg in ('backlog', 'family'):
3268 try:
3269 socket_args[arg] = self.options.pop(arg)
3270 except KeyError:
3271 pass
3272 address = (self.host, self.port)
3273 try:
3274 wsgi.server(listen(address, **socket_args), handler,
3275 log_output=(not self.quiet))
3276 except TypeError:
3277 # Fallback, if we have old version of eventlet
3278 wsgi.server(listen(address), handler)
3279
3280
3281 class RocketServer(ServerAdapter):
3282 """ Untested. """
3283
3284 def run(self, handler):
3285 from rocket import Rocket
3286 server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler})
3287 server.start()
3288
3289
3290 class BjoernServer(ServerAdapter):
3291 """ Fast server written in C: https://github.com/jonashaag/bjoern """
3292
3293 def run(self, handler):
3294 from bjoern import run
3295 run(handler, self.host, self.port)
3296
3297
3298 class AiohttpServer(ServerAdapter):
3299 """ Untested.
3300 aiohttp
3301 https://pypi.python.org/pypi/aiohttp/
3302 """
3303
3304 def run(self, handler):
3305 import asyncio
3306 from aiohttp.wsgi import WSGIServerHttpProtocol
3307 self.loop = asyncio.new_event_loop()
3308 asyncio.set_event_loop(self.loop)
3309
3310 protocol_factory = lambda: WSGIServerHttpProtocol(
3311 handler,
3312 readpayload=True,
3313 debug=(not self.quiet))
3314 self.loop.run_until_complete(self.loop.create_server(protocol_factory,
3315 self.host,
3316 self.port))
3317
3318 if 'BOTTLE_CHILD' in os.environ:
3319 import signal
3320 signal.signal(signal.SIGINT, lambda s, f: self.loop.stop())
3321
3322 try:
3323 self.loop.run_forever()
3324 except KeyboardInterrupt:
3325 self.loop.stop()
3326
3327
3328 class AutoServer(ServerAdapter):
3329 """ Untested. """
3330 adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer,
3331 WSGIRefServer]
3332
3333 def run(self, handler):
3334 for sa in self.adapters:
3335 try:
3336 return sa(self.host, self.port, **self.options).run(handler)
3337 except ImportError:
3338 pass
3339
3340
3341 server_names = {
3342 'cgi': CGIServer,
3343 'flup': FlupFCGIServer,
3344 'wsgiref': WSGIRefServer,
3345 'waitress': WaitressServer,
3346 'cherrypy': CherryPyServer,
3347 'paste': PasteServer,
3348 'fapws3': FapwsServer,
3349 'tornado': TornadoServer,
3350 'gae': AppEngineServer,
3351 'twisted': TwistedServer,
3352 'diesel': DieselServer,
3353 'meinheld': MeinheldServer,
3354 'gunicorn': GunicornServer,
3355 'eventlet': EventletServer,
3356 'gevent': GeventServer,
3357 'geventSocketIO': GeventSocketIOServer,
3358 'rocket': RocketServer,
3359 'bjoern': BjoernServer,
3360 'aiohttp': AiohttpServer,
3361 'auto': AutoServer,
3362 }
3363
3364 ###############################################################################
3365 # Application Control ##########################################################
3366 ###############################################################################
3367
3368
3369 def load(target, **namespace):
3370 """ Import a module or fetch an object from a module.
3371
3372 * ``package.module`` returns `module` as a module object.
3373 * ``pack.mod:name`` returns the module variable `name` from `pack.mod`.
3374 * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result.
3375
3376 The last form accepts not only function calls, but any type of
3377 expression. Keyword arguments passed to this function are available as
3378 local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
3379 """
3380 module, target = target.split(":", 1) if ':' in target else (target, None)
3381 if module not in sys.modules: __import__(module)
3382 if not target: return sys.modules[module]
3383 if target.isalnum(): return getattr(sys.modules[module], target)
3384 package_name = module.split('.')[0]
3385 namespace[package_name] = sys.modules[package_name]
3386 return eval('%s.%s' % (module, target), namespace)
3387
3388
3389 def load_app(target):
3390 """ Load a bottle application from a module and make sure that the import
3391 does not affect the current default application, but returns a separate
3392 application object. See :func:`load` for the target parameter. """
3393 global NORUN
3394 NORUN, nr_old = True, NORUN
3395 tmp = default_app.push() # Create a new "default application"
3396 try:
3397 rv = load(target) # Import the target module
3398 return rv if callable(rv) else tmp
3399 finally:
3400 default_app.remove(tmp) # Remove the temporary added default application
3401 NORUN = nr_old
3402
3403
3404 _debug = debug
3405
3406
3407 def run(app=None,
3408 server='wsgiref',
3409 host='127.0.0.1',
3410 port=8080,
3411 interval=1,
3412 reloader=False,
3413 quiet=False,
3414 plugins=None,
3415 debug=None,
3416 config=None, **kargs):
3417 """ Start a server instance. This method blocks until the server terminates.
3418
3419 :param app: WSGI application or target string supported by
3420 :func:`load_app`. (default: :func:`default_app`)
3421 :param server: Server adapter to use. See :data:`server_names` keys
3422 for valid names or pass a :class:`ServerAdapter` subclass.
3423 (default: `wsgiref`)
3424 :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on
3425 all interfaces including the external one. (default: 127.0.0.1)
3426 :param port: Server port to bind to. Values below 1024 require root
3427 privileges. (default: 8080)
3428 :param reloader: Start auto-reloading server? (default: False)
3429 :param interval: Auto-reloader interval in seconds (default: 1)
3430 :param quiet: Suppress output to stdout and stderr? (default: False)
3431 :param options: Options passed to the server adapter.
3432 """
3433 if NORUN: return
3434 if reloader and not os.environ.get('BOTTLE_CHILD'):
3435 import subprocess
3436 lockfile = None
3437 try:
3438 fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
3439 os.close(fd) # We only need this file to exist. We never write to it
3440 while os.path.exists(lockfile):
3441 args = [sys.executable] + sys.argv
3442 environ = os.environ.copy()
3443 environ['BOTTLE_CHILD'] = 'true'
3444 environ['BOTTLE_LOCKFILE'] = lockfile
3445 p = subprocess.Popen(args, env=environ)
3446 while p.poll() is None: # Busy wait...
3447 os.utime(lockfile, None) # I am alive!
3448 time.sleep(interval)
3449 if p.poll() != 3:
3450 if os.path.exists(lockfile): os.unlink(lockfile)
3451 sys.exit(p.poll())
3452 except KeyboardInterrupt:
3453 pass
3454 finally:
3455 if os.path.exists(lockfile):
3456 os.unlink(lockfile)
3457 return
3458
3459 try:
3460 if debug is not None: _debug(debug)
3461 app = app or default_app()
3462 if isinstance(app, basestring):
3463 app = load_app(app)
3464 if not callable(app):
3465 raise ValueError("Application is not callable: %r" % app)
3466
3467 for plugin in plugins or []:
3468 if isinstance(plugin, basestring):
3469 plugin = load(plugin)
3470 app.install(plugin)
3471
3472 if config:
3473 app.config.update(config)
3474
3475 if server in server_names:
3476 server = server_names.get(server)
3477 if isinstance(server, basestring):
3478 server = load(server)
3479 if isinstance(server, type):
3480 server = server(host=host, port=port, **kargs)
3481 if not isinstance(server, ServerAdapter):
3482 raise ValueError("Unknown or unsupported server: %r" % server)
3483
3484 server.quiet = server.quiet or quiet
3485 if not server.quiet:
3486 _stderr("Bottle v%s server starting up (using %s)...\n" %
3487 (__version__, repr(server)))
3488 _stderr("Listening on http://%s:%d/\n" %
3489 (server.host, server.port))
3490 _stderr("Hit Ctrl-C to quit.\n\n")
3491
3492 if reloader:
3493 lockfile = os.environ.get('BOTTLE_LOCKFILE')
3494 bgcheck = FileCheckerThread(lockfile, interval)
3495 with bgcheck:
3496 server.run(app)
3497 if bgcheck.status == 'reload':
3498 sys.exit(3)
3499 else:
3500 server.run(app)
3501 except KeyboardInterrupt:
3502 pass
3503 except (SystemExit, MemoryError):
3504 raise
3505 except:
3506 if not reloader: raise
3507 if not getattr(server, 'quiet', quiet):
3508 print_exc()
3509 time.sleep(interval)
3510 sys.exit(3)
3511
3512
3513 class FileCheckerThread(threading.Thread):
3514 """ Interrupt main-thread as soon as a changed module file is detected,
3515 the lockfile gets deleted or gets too old. """
3516
3517 def __init__(self, lockfile, interval):
3518 threading.Thread.__init__(self)
3519 self.daemon = True
3520 self.lockfile, self.interval = lockfile, interval
3521 #: Is one of 'reload', 'error' or 'exit'
3522 self.status = None
3523
3524 def run(self):
3525 exists = os.path.exists
3526 mtime = lambda p: os.stat(p).st_mtime
3527 files = dict()
3528
3529 for module in list(sys.modules.values()):
3530 path = getattr(module, '__file__', '')
3531 if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
3532 if path and exists(path): files[path] = mtime(path)
3533
3534 while not self.status:
3535 if not exists(self.lockfile)\
3536 or mtime(self.lockfile) < time.time() - self.interval - 5:
3537 self.status = 'error'
3538 thread.interrupt_main()
3539 for path, lmtime in list(files.items()):
3540 if not exists(path) or mtime(path) > lmtime:
3541 self.status = 'reload'
3542 thread.interrupt_main()
3543 break
3544 time.sleep(self.interval)
3545
3546 def __enter__(self):
3547 self.start()
3548
3549 def __exit__(self, exc_type, *_):
3550 if not self.status: self.status = 'exit' # silent exit
3551 self.join()
3552 return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
3553
3554 ###############################################################################
3555 # Template Adapters ############################################################
3556 ###############################################################################
3557
3558
3559 class TemplateError(HTTPError):
3560 def __init__(self, message):
3561 HTTPError.__init__(self, 500, message)
3562
3563
3564 class BaseTemplate(object):
3565 """ Base class and minimal API for template adapters """
3566 extensions = ['tpl', 'html', 'thtml', 'stpl']
3567 settings = {} #used in prepare()
3568 defaults = {} #used in render()
3569
3570 def __init__(self,
3571 source=None,
3572 name=None,
3573 lookup=None,
3574 encoding='utf8', **settings):
3575 """ Create a new template.
3576 If the source parameter (str or buffer) is missing, the name argument
3577 is used to guess a template filename. Subclasses can assume that
3578 self.source and/or self.filename are set. Both are strings.
3579 The lookup, encoding and settings parameters are stored as instance
3580 variables.
3581 The lookup parameter stores a list containing directory paths.
3582 The encoding parameter should be used to decode byte strings or files.
3583 The settings parameter contains a dict for engine-specific settings.
3584 """
3585 self.name = name
3586 self.source = source.read() if hasattr(source, 'read') else source
3587 self.filename = source.filename if hasattr(source, 'filename') else None
3588 self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
3589 self.encoding = encoding
3590 self.settings = self.settings.copy() # Copy from class variable
3591 self.settings.update(settings) # Apply
3592 if not self.source and self.name:
3593 self.filename = self.search(self.name, self.lookup)
3594 if not self.filename:
3595 raise TemplateError('Template %s not found.' % repr(name))
3596 if not self.source and not self.filename:
3597 raise TemplateError('No template specified.')
3598 self.prepare(**self.settings)
3599
3600 @classmethod
3601 def search(cls, name, lookup=None):
3602 """ Search name in all directories specified in lookup.
3603 First without, then with common extensions. Return first hit. """
3604 if not lookup:
3605 raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")
3606
3607 if os.path.isabs(name):
3608 raise depr(0, 12, "Use of absolute path for template name.",
3609 "Refer to templates with names or paths relative to the lookup path.")
3610
3611 for spath in lookup:
3612 spath = os.path.abspath(spath) + os.sep
3613 fname = os.path.abspath(os.path.join(spath, name))
3614 if not fname.startswith(spath): continue
3615 if os.path.isfile(fname): return fname
3616 for ext in cls.extensions:
3617 if os.path.isfile('%s.%s' % (fname, ext)):
3618 return '%s.%s' % (fname, ext)
3619
3620 @classmethod
3621 def global_config(cls, key, *args):
3622 """ This reads or sets the global settings stored in class.settings. """
3623 if args:
3624 cls.settings = cls.settings.copy() # Make settings local to class
3625 cls.settings[key] = args[0]
3626 else:
3627 return cls.settings[key]
3628
3629 def prepare(self, **options):
3630 """ Run preparations (parsing, caching, ...).
3631 It should be possible to call this again to refresh a template or to
3632 update settings.
3633 """
3634 raise NotImplementedError
3635
3636 def render(self, *args, **kwargs):
3637 """ Render the template with the specified local variables and return
3638 a single byte or unicode string. If it is a byte string, the encoding
3639 must match self.encoding. This method must be thread-safe!
3640 Local variables may be provided in dictionaries (args)
3641 or directly, as keywords (kwargs).
3642 """
3643 raise NotImplementedError
3644
3645
3646 class MakoTemplate(BaseTemplate):
3647 def prepare(self, **options):
3648 from mako.template import Template
3649 from mako.lookup import TemplateLookup
3650 options.update({'input_encoding': self.encoding})
3651 options.setdefault('format_exceptions', bool(DEBUG))
3652 lookup = TemplateLookup(directories=self.lookup, **options)
3653 if self.source:
3654 self.tpl = Template(self.source, lookup=lookup, **options)
3655 else:
3656 self.tpl = Template(uri=self.name,
3657 filename=self.filename,
3658 lookup=lookup, **options)
3659
3660 def render(self, *args, **kwargs):
3661 for dictarg in args:
3662 kwargs.update(dictarg)
3663 _defaults = self.defaults.copy()
3664 _defaults.update(kwargs)
3665 return self.tpl.render(**_defaults)
3666
3667
3668 class CheetahTemplate(BaseTemplate):
3669 def prepare(self, **options):
3670 from Cheetah.Template import Template
3671 self.context = threading.local()
3672 self.context.vars = {}
3673 options['searchList'] = [self.context.vars]
3674 if self.source:
3675 self.tpl = Template(source=self.source, **options)
3676 else:
3677 self.tpl = Template(file=self.filename, **options)
3678
3679 def render(self, *args, **kwargs):
3680 for dictarg in args:
3681 kwargs.update(dictarg)
3682 self.context.vars.update(self.defaults)
3683 self.context.vars.update(kwargs)
3684 out = str(self.tpl)
3685 self.context.vars.clear()
3686 return out
3687
3688
3689 class Jinja2Template(BaseTemplate):
3690 def prepare(self, filters=None, tests=None, globals={}, **kwargs):
3691 from jinja2 import Environment, FunctionLoader
3692 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
3693 if filters: self.env.filters.update(filters)
3694 if tests: self.env.tests.update(tests)
3695 if globals: self.env.globals.update(globals)
3696 if self.source:
3697 self.tpl = self.env.from_string(self.source)
3698 else:
3699 self.tpl = self.env.get_template(self.filename)
3700
3701 def render(self, *args, **kwargs):
3702 for dictarg in args:
3703 kwargs.update(dictarg)
3704 _defaults = self.defaults.copy()
3705 _defaults.update(kwargs)
3706 return self.tpl.render(**_defaults)
3707
3708 def loader(self, name):
3709 if name == self.filename:
3710 fname = name
3711 else:
3712 fname = self.search(name, self.lookup)
3713 if not fname: return
3714 with open(fname, "rb") as f:
3715 return f.read().decode(self.encoding)
3716
3717
3718 class SimpleTemplate(BaseTemplate):
3719 def prepare(self,
3720 escape_func=html_escape,
3721 noescape=False,
3722 syntax=None, **ka):
3723 self.cache = {}
3724 enc = self.encoding
3725 self._str = lambda x: touni(x, enc)
3726 self._escape = lambda x: escape_func(touni(x, enc))
3727 self.syntax = syntax
3728 if noescape:
3729 self._str, self._escape = self._escape, self._str
3730
3731 @cached_property
3732 def co(self):
3733 return compile(self.code, self.filename or '<string>', 'exec')
3734
3735 @cached_property
3736 def code(self):
3737 source = self.source
3738 if not source:
3739 with open(self.filename, 'rb') as f:
3740 source = f.read()
3741 try:
3742 source, encoding = touni(source), 'utf8'
3743 except UnicodeError:
3744 raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.')
3745 parser = StplParser(source, encoding=encoding, syntax=self.syntax)
3746 code = parser.translate()
3747 self.encoding = parser.encoding
3748 return code
3749
3750 def _rebase(self, _env, _name=None, **kwargs):
3751 _env['_rebase'] = (_name, kwargs)
3752
3753 def _include(self, _env, _name=None, **kwargs):
3754 env = _env.copy()
3755 env.update(kwargs)
3756 if _name not in self.cache:
3757 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax)
3758 return self.cache[_name].execute(env['_stdout'], env)
3759
3760 def execute(self, _stdout, kwargs):
3761 env = self.defaults.copy()
3762 env.update(kwargs)
3763 env.update({
3764 '_stdout': _stdout,
3765 '_printlist': _stdout.extend,
3766 'include': functools.partial(self._include, env),
3767 'rebase': functools.partial(self._rebase, env),
3768 '_rebase': None,
3769 '_str': self._str,
3770 '_escape': self._escape,
3771 'get': env.get,
3772 'setdefault': env.setdefault,
3773 'defined': env.__contains__
3774 })
3775 eval(self.co, env)
3776 if env.get('_rebase'):
3777 subtpl, rargs = env.pop('_rebase')
3778 rargs['base'] = ''.join(_stdout) #copy stdout
3779 del _stdout[:] # clear stdout
3780 return self._include(env, subtpl, **rargs)
3781 return env
3782
3783 def render(self, *args, **kwargs):
3784 """ Render the template using keyword arguments as local variables. """
3785 env = {}
3786 stdout = []
3787 for dictarg in args:
3788 env.update(dictarg)
3789 env.update(kwargs)
3790 self.execute(stdout, env)
3791 return ''.join(stdout)
3792
3793
3794 class StplSyntaxError(TemplateError):
3795
3796 pass
3797
3798
3799 class StplParser(object):
3800 """ Parser for stpl templates. """
3801 _re_cache = {} #: Cache for compiled re patterns
3802
3803 # This huge pile of voodoo magic splits python code into 8 different tokens.
3804 # We use the verbose (?x) regex mode to make this more manageable
3805
3806 _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode
3807 [urbURB]*
3808 (?: ''(?!')
3809 |""(?!")
3810 |'{6}
3811 |"{6}
3812 |'(?:[^\\']|\\.)+?'
3813 |"(?:[^\\"]|\\.)+?"
3814 |'{3}(?:[^\\]|\\.|\n)+?'{3}
3815 |"{3}(?:[^\\]|\\.|\n)+?"{3}
3816 )
3817 )'''
3818
3819 _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later
3820
3821 _re_tok += r'''
3822 # 2: Comments (until end of line, but not the newline itself)
3823 |(\#.*)
3824
3825 # 3: Open and close (4) grouping tokens
3826 |([\[\{\(])
3827 |([\]\}\)])
3828
3829 # 5,6: Keywords that start or continue a python block (only start of line)
3830 |^([\ \t]*(?:if|for|while|with|try|def|class)\b)
3831 |^([\ \t]*(?:elif|else|except|finally)\b)
3832
3833 # 7: Our special 'end' keyword (but only if it stands alone)
3834 |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#))
3835
3836 # 8: A customizable end-of-code-block template token (only end of line)
3837 |(%(block_close)s[\ \t]*(?=\r?$))
3838
3839 # 9: And finally, a single newline. The 10th token is 'everything else'
3840 |(\r?\n)
3841 '''
3842
3843 # Match the start tokens of code areas in a template
3844 _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))'''
3845 # Match inline statements (may contain python strings)
3846 _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl
3847
3848 default_syntax = '<% %> % {{ }}'
3849
3850 def __init__(self, source, syntax=None, encoding='utf8'):
3851 self.source, self.encoding = touni(source, encoding), encoding
3852 self.set_syntax(syntax or self.default_syntax)
3853 self.code_buffer, self.text_buffer = [], []
3854 self.lineno, self.offset = 1, 0
3855 self.indent, self.indent_mod = 0, 0
3856 self.paren_depth = 0
3857
3858 def get_syntax(self):
3859 """ Tokens as a space separated string (default: <% %> % {{ }}) """
3860 return self._syntax
3861
3862 def set_syntax(self, syntax):
3863 self._syntax = syntax
3864 self._tokens = syntax.split()
3865 if not syntax in self._re_cache:
3866 names = 'block_start block_close line_start inline_start inline_end'
3867 etokens = map(re.escape, self._tokens)
3868 pattern_vars = dict(zip(names.split(), etokens))
3869 patterns = (self._re_split, self._re_tok, self._re_inl)
3870 patterns = [re.compile(p % pattern_vars) for p in patterns]
3871 self._re_cache[syntax] = patterns
3872 self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax]
3873
3874 syntax = property(get_syntax, set_syntax)
3875
3876 def translate(self):
3877 if self.offset: raise RuntimeError('Parser is a one time instance.')
3878 while True:
3879 m = self.re_split.search(self.source, pos=self.offset)
3880 if m:
3881 text = self.source[self.offset:m.start()]
3882 self.text_buffer.append(text)
3883 self.offset = m.end()
3884 if m.group(1): # Escape syntax
3885 line, sep, _ = self.source[self.offset:].partition('\n')
3886 self.text_buffer.append(self.source[m.start():m.start(1)] +
3887 m.group(2) + line + sep)
3888 self.offset += len(line + sep)
3889 continue
3890 self.flush_text()
3891 self.offset += self.read_code(self.source[self.offset:],
3892 multiline=bool(m.group(4)))
3893 else:
3894 break
3895 self.text_buffer.append(self.source[self.offset:])
3896 self.flush_text()
3897 return ''.join(self.code_buffer)
3898
3899 def read_code(self, pysource, multiline):
3900 code_line, comment = '', ''
3901 offset = 0
3902 while True:
3903 m = self.re_tok.search(pysource, pos=offset)
3904 if not m:
3905 code_line += pysource[offset:]
3906 offset = len(pysource)
3907 self.write_code(code_line.strip(), comment)
3908 break
3909 code_line += pysource[offset:m.start()]
3910 offset = m.end()
3911 _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups()
3912 if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c
3913 code_line += _blk1 or _blk2
3914 continue
3915 if _str: # Python string
3916 code_line += _str
3917 elif _com: # Python comment (up to EOL)
3918 comment = _com
3919 if multiline and _com.strip().endswith(self._tokens[1]):
3920 multiline = False # Allow end-of-block in comments
3921 elif _po: # open parenthesis
3922 self.paren_depth += 1
3923 code_line += _po
3924 elif _pc: # close parenthesis
3925 if self.paren_depth > 0:
3926 # we could check for matching parentheses here, but it's
3927 # easier to leave that to python - just check counts
3928 self.paren_depth -= 1
3929 code_line += _pc
3930 elif _blk1: # Start-block keyword (if/for/while/def/try/...)
3931 code_line, self.indent_mod = _blk1, -1
3932 self.indent += 1
3933 elif _blk2: # Continue-block keyword (else/elif/except/...)
3934 code_line, self.indent_mod = _blk2, -1
3935 elif _end: # The non-standard 'end'-keyword (ends a block)
3936 self.indent -= 1
3937 elif _cend: # The end-code-block template token (usually '%>')
3938 if multiline: multiline = False
3939 else: code_line += _cend
3940 else: # \n
3941 self.write_code(code_line.strip(), comment)
3942 self.lineno += 1
3943 code_line, comment, self.indent_mod = '', '', 0
3944 if not multiline:
3945 break
3946
3947 return offset
3948
3949 def flush_text(self):
3950 text = ''.join(self.text_buffer)
3951 del self.text_buffer[:]
3952 if not text: return
3953 parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent
3954 for m in self.re_inl.finditer(text):
3955 prefix, pos = text[pos:m.start()], m.end()
3956 if prefix:
3957 parts.append(nl.join(map(repr, prefix.splitlines(True))))
3958 if prefix.endswith('\n'): parts[-1] += nl
3959 parts.append(self.process_inline(m.group(1).strip()))
3960 if pos < len(text):
3961 prefix = text[pos:]
3962 lines = prefix.splitlines(True)
3963 if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3]
3964 elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4]
3965 parts.append(nl.join(map(repr, lines)))
3966 code = '_printlist((%s,))' % ', '.join(parts)
3967 self.lineno += code.count('\n') + 1
3968 self.write_code(code)
3969
3970 @staticmethod
3971 def process_inline(chunk):
3972 if chunk[0] == '!': return '_str(%s)' % chunk[1:]
3973 return '_escape(%s)' % chunk
3974
3975 def write_code(self, line, comment=''):
3976 code = ' ' * (self.indent + self.indent_mod)
3977 code += line.lstrip() + comment + '\n'
3978 self.code_buffer.append(code)
3979
3980
3981 def template(*args, **kwargs):
3982 """
3983 Get a rendered template as a string iterator.
3984 You can use a name, a filename or a template string as first parameter.
3985 Template rendering arguments can be passed as dictionaries
3986 or directly (as keyword arguments).
3987 """
3988 tpl = args[0] if args else None
3989 for dictarg in args[1:]:
3990 kwargs.update(dictarg)
3991 adapter = kwargs.pop('template_adapter', SimpleTemplate)
3992 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
3993 tplid = (id(lookup), tpl)
3994 if tplid not in TEMPLATES or DEBUG:
3995 settings = kwargs.pop('template_settings', {})
3996 if isinstance(tpl, adapter):
3997 TEMPLATES[tplid] = tpl
3998 if settings: TEMPLATES[tplid].prepare(**settings)
3999 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
4000 TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
4001 else:
4002 TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
4003 if not TEMPLATES[tplid]:
4004 abort(500, 'Template (%s) not found' % tpl)
4005 return TEMPLATES[tplid].render(kwargs)
4006
4007
4008 mako_template = functools.partial(template, template_adapter=MakoTemplate)
4009 cheetah_template = functools.partial(template,
4010 template_adapter=CheetahTemplate)
4011 jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
4012
4013
4014 def view(tpl_name, **defaults):
4015 """ Decorator: renders a template for a handler.
4016 The handler can control its behavior like that:
4017
4018 - return a dict of template vars to fill out the template
4019 - return something other than a dict and the view decorator will not
4020 process the template, but return the handler result as is.
4021 This includes returning a HTTPResponse(dict) to get,
4022 for instance, JSON with autojson or other castfilters.
4023 """
4024
4025 def decorator(func):
4026
4027 @functools.wraps(func)
4028 def wrapper(*args, **kwargs):
4029 result = func(*args, **kwargs)
4030 if isinstance(result, (dict, DictMixin)):
4031 tplvars = defaults.copy()
4032 tplvars.update(result)
4033 return template(tpl_name, **tplvars)
4034 elif result is None:
4035 return template(tpl_name, defaults)
4036 return result
4037
4038 return wrapper
4039
4040 return decorator
4041
4042
4043 mako_view = functools.partial(view, template_adapter=MakoTemplate)
4044 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
4045 jinja2_view = functools.partial(view, template_adapter=Jinja2Template)
4046
4047 ###############################################################################
4048 # Constants and Globals ########################################################
4049 ###############################################################################
4050
4051 TEMPLATE_PATH = ['./', './views/']
4052 TEMPLATES = {}
4053 DEBUG = False
4054 NORUN = False # If set, run() does nothing. Used by load_app()
4055
4056 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
4057 HTTP_CODES = httplib.responses.copy()
4058 HTTP_CODES[418] = "I'm a teapot" # RFC 2324
4059 HTTP_CODES[428] = "Precondition Required"
4060 HTTP_CODES[429] = "Too Many Requests"
4061 HTTP_CODES[431] = "Request Header Fields Too Large"
4062 HTTP_CODES[511] = "Network Authentication Required"
4063 _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v))
4064 for (k, v) in HTTP_CODES.items())
4065
4066 #: The default template used for error pages. Override with @error()
4067 ERROR_PAGE_TEMPLATE = """
4068 %%try:
4069 %%from %s import DEBUG, request
4070 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
4071 <html>
4072 <head>
4073 <title>Error: {{e.status}}</title>
4074 <style type="text/css">
4075 html {background-color: #eee; font-family: sans-serif;}
4076 body {background-color: #fff; border: 1px solid #ddd;
4077 padding: 15px; margin: 15px;}
4078 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
4079 </style>
4080 </head>
4081 <body>
4082 <h1>Error: {{e.status}}</h1>
4083 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt>
4084 caused an error:</p>
4085 <pre>{{e.body}}</pre>
4086 %%if DEBUG and e.exception:
4087 <h2>Exception:</h2>
4088 <pre>{{repr(e.exception)}}</pre>
4089 %%end
4090 %%if DEBUG and e.traceback:
4091 <h2>Traceback:</h2>
4092 <pre>{{e.traceback}}</pre>
4093 %%end
4094 </body>
4095 </html>
4096 %%except ImportError:
4097 <b>ImportError:</b> Could not generate the error page. Please add bottle to
4098 the import path.
4099 %%end
4100 """ % __name__
4101
4102 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
4103 #: request callback, this instance always refers to the *current* request
4104 #: (even on a multi-threaded server).
4105 request = LocalRequest()
4106
4107 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the
4108 #: HTTP response for the *current* request.
4109 response = LocalResponse()
4110
4111 #: A thread-safe namespace. Not used by Bottle.
4112 local = threading.local()
4113
4114 # Initialize app stack (create first empty Bottle app now deferred until needed)
4115 # BC: 0.6.4 and needed for run()
4116 apps = app = default_app = AppStack()
4117
4118
4119 #: A virtual package that redirects import statements.
4120 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
4121 ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else
4122 __name__ + ".ext", 'bottle_%s').module
4123
4124
4125
4126 if __name__ == '__main__':
4127 opt, args, parser = _cli_parse(sys.argv)
4128
4129 def _cli_error(msg):
4130 parser.print_help()
4131 _stderr('\nError: %s\n' % msg)
4132 sys.exit(1)
4133
4134 if opt.version:
4135 _stdout('Bottle %s\n' % __version__)
4136 sys.exit(0)
4137 if not args:
4138 _cli_error("No application entry point specified.")
4139
4140 sys.path.insert(0, '.')
4141 sys.modules.setdefault('bottle', sys.modules['__main__'])
4142
4143 host, port = (opt.bind or 'localhost'), 8080
4144 if ':' in host and host.rfind(']') < host.rfind(':'):
4145 host, port = host.rsplit(':', 1)
4146 host = host.strip('[]')
4147
4148 config = ConfigDict()
4149
4150 for cfile in opt.conf or []:
4151 try:
4152 if cfile.endswith('.json'):
4153 with open(cfile, 'rb') as fp:
4154 config.load_dict(json_loads(fp.read()))
4155 else:
4156 config.load_config(cfile)
4157 except ConfigParserError:
4158 _cli_error(str(_e()))
4159 except IOError:
4160 _cli_error("Unable to read config file %r" % cfile)
4161 except (UnicodeError, TypeError, ValueError):
4162 _cli_error("Unable to parse config file %r: %s" % (cfile, _e()))
4163
4164 for cval in opt.param or []:
4165 if '=' in cval:
4166 config.update((cval.split('=', 1),))
4167 else:
4168 config[cval] = True
4169
4170 run(args[0],
4171 host=host,
4172 port=int(port),
4173 server=opt.server,
4174 reloader=opt.reload,
4175 plugins=opt.plugin,
4176 debug=opt.debug,
4177 config=config)
4178
4179 # THE END
General Comments 0
You need to be logged in to leave comments. Login now