summaryrefslogtreecommitdiffstats
path: root/module
diff options
context:
space:
mode:
authorGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2009-12-02 20:36:43 +0100
committerGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2009-12-02 20:36:43 +0100
commit106e79456886563e4ee4ed43027bc69984f65928 (patch)
treebd1d1ea1aabe2bac3faf77117145466382e5f3b7 /module
parentNew Update Function, pycurl able to just load headers, little fixes (diff)
downloadpyload-106e79456886563e4ee4ed43027bc69984f65928.tar.xz
new bottle.py, re implemented webserver(not ready yet)
Diffstat (limited to 'module')
-rw-r--r--module/web/WebServer.py428
-rw-r--r--module/web/bottle.py727
2 files changed, 896 insertions, 259 deletions
diff --git a/module/web/WebServer.py b/module/web/WebServer.py
index 0712f1dce..29b0aafe8 100644
--- a/module/web/WebServer.py
+++ b/module/web/WebServer.py
@@ -1,72 +1,364 @@
-import sys
-from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
-from xmlrpclib import ServerProxy
-from time import time
-import re
+#import sys
+#from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+#from xmlrpclib import ServerProxy
+#from time import time
+#import re
+#
+#class Handler(BaseHTTPRequestHandler):
+#
+# def do_GET(self):
+# global coreserver
+# stdout = sys.stdout
+# sys.stdout = self.wfile
+# if self.path == "/":
+# print "Server Runs"
+# elif self.path == "/downloads":
+# print self.get_downloads()
+# elif re.search("/add=.?", self.path):
+# if re.match(is_url, self.path.split("/add=")[1]):
+# coreserver.add_urls([self.path.split("/add=")[1]])
+# print "Link Added"
+# else:
+# try:
+# print open(self.path[1:], 'r').read()
+# except IOError:
+# self.send_error(404)
+#
+# def format_size(self, size):
+# return str(size / 1024) + " MiB"
+#
+# def format_time(self,seconds):
+# seconds = int(seconds)
+# hours, seconds = divmod(seconds, 3600)
+# minutes, seconds = divmod(seconds, 60)
+# return "%.2i:%.2i:%.2i" % (hours, minutes, seconds)
+#
+# def get_downloads(self):
+# data = coreserver.status_downloads()
+# for download in data:
+# print "<h3>%s</h3>" % download["name"]
+# if download["status"] == "downloading":
+# percent = download["percent"]
+# z = percent / 4
+# print "<h3>%s</h3>" % dl_name
+# print "<font face='font-family:Fixedsys,Courier,monospace;'>[" + z * "#" + (25-z) * "&nbsp;" + "]</font>" + str(percent) + "%<br />"
+# print "Speed: " + str(int(download['speed'])) + " kb/s"
+# print "Size: " + self.format_size(download['size'])
+# print "Finished in: " + self.format_time(download['eta'])
+# print "ID: " + str(download['id'])
+# dl_status = "[" + z * "#" + (25-z) * " " + "] " + str(percent) + "%" + " Speed: " + str(int(download['speed'])) + " kb/s" + " Size: " + self.format_size(download['size']) + " Finished in: " + self.format_time(download['eta']) + " ID: " + str(download['id'])
+# if download["status"] == "waiting":
+# print "waiting: " + self.format_time(download["wait_until"]- time())
+#
+#is_url = re.compile("^(((https?|ftp)\:\/\/)?([\w\.\-]+(\:[\w\.\&%\$\-]+)*@)?((([^\s\(\)\<\>\\\"\.\[\]\,@;:]+)(\.[^\s\(\)\<\>\\\"\.\[\]\,@;:]+)*(\.[a-zA-Z]{2,4}))|((([01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}([01]?\d{1,2}|2[0-4]\d|25[0-5])))(\b\:(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}|0)\b)?((\/[^\/][\w\.\,\?\'\\\/\+&%\$#\=~_\-@]*)*[^\.\,\?\"\'\(\)\[\]!;<>{}\s\x7F-\xFF])?)$",re.IGNORECASE)
+#
+#coreserver = None
+#
+#class WebServer():
+#
+# def start(self):
+# try:
+# global coreserver
+# coreserver = ServerProxy("https://testuser:testpw@localhost:1337", allow_none=True)
+# webserver = HTTPServer(('',8080),Handler)
+# print 'server started at port 8080'
+# webserver.serve_forever()
+# except KeyboardInterrupt:
+# webserver.socket.close()
+#
+#if __name__ == "__main__":
+# web = WebServer()
+# web.start()
-class Handler(BaseHTTPRequestHandler):
+
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+#Copyright (C) 2009 RaNaN
+#
+#This program is free software; you can redistribute it and/or modify
+#it under the terms of the GNU General Public License as published by
+#the Free Software Foundation; either version 3 of the License,
+#or (at your option) any later version.
+#
+#This program is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#See the GNU General Public License for more details.
+#
+#You should have received a copy of the GNU General Public License
+# along with this program; if not, see <http://www.gnu.org/licenses/>.
+#
+###
+import random
+import threading
+import time
+
+import bottle
+from bottle import abort
+from bottle import redirect
+from bottle import request
+from bottle import response
+from bottle import route
+from bottle import run
+from bottle import send_file
+from bottle import template
+from bottle import validate
+
+
+core = None
+core_methods = None
+
+PATH = "./module/web/"
+TIME = time.strftime("%a, %d %b %Y 00:00:00 +0000", time.localtime()) #set time to current day
+USERS = {}
+# TODO: Implement new server methods
+@route('/login', method='POST')
+def do_login():
+ #print request.GET
+
+
+ username = core.config['webinterface']['username']
+ pw = core.config['webinterface']['password']
+
+ if request.POST['u'] == username and request.POST['p'] == pw:
+
+ id = int(random.getrandbits(16))
+ ua = request.HEADER("HTTP_USER_AGENT")
+ ip = request.HEADER("REMOTE_ADDR")
+
+ auth = {}
+
+ auth['ua'] = ua
+ auth['ip'] = ip
+ auth['user'] = username
+
+ USERS[id] = auth
+
+ response.COOKIES['user'] = username
+ response.COOKIES['id'] = id
+
+ return template('default', page='loggedin', user=username)
+ else:
+ return template('default', page='login')
+
+@route('/login')
+def login():
+
+ if check_auth(request):
+ redirect("/")
+
+ return template('default', page='login')
+
+@route('/logout')
+def logout():
+ try:
+ del USERS[int(request.COOKIES.get('id'))]
+ except:
+ pass
- def do_GET(self):
- global coreserver
- stdout = sys.stdout
- sys.stdout = self.wfile
- if self.path == "/":
- print "Server Runs"
- elif self.path == "/downloads":
- print self.get_downloads()
- elif re.search("/add=.?", self.path):
- if re.match(is_url, self.path.split("/add=")[1]):
- coreserver.add_urls([self.path.split("/add=")[1]])
- print "Link Added"
+ redirect("/login")
+
+@route('/')
+def home():
+
+ if not check_auth(request):
+ redirect("/login")
+
+ username = request.COOKIES.get('user')
+
+ dls = core_methods.status_downloads()
+
+ for dl in dls:
+ dl['eta'] = str(core.format_time(dl['eta']))
+ dl['wait_until'] = str(core.format_time(dl['wait_until'] - time.time()))
+
+
+ return template('default', page='home', links=dls, user=username, status=core_methods.status_server())
+
+@route('/queue')
+def queue():
+
+ if not check_auth(request):
+ redirect("/login")
+
+ username = request.COOKIES.get('user')
+
+ return template('default', page='queue', links=core.get_links(), user=username, status=core_methods.status_server())
+
+@route('/downloads')
+def downloads():
+
+ if not check_auth(request):
+ redirect("/login")
+
+ username = request.COOKIES.get('user')
+
+ return template('default', page='downloads', links=core_methods.status_downloads(), user=username, status=core_methods.status_server())
+
+
+@route('/logs')
+def logs():
+
+ if not check_auth(request):
+ redirect("/login")
+
+ username = request.COOKIES.get('user')
+
+ return template('default', page='logs', links=core_methods.status_downloads(), user=username, status=core_methods.status_server())
+
+@route('/json/links')
+def get_links():
+ response.header['Cache-Control'] = 'no-cache, must-revalidate'
+ response.content_type = 'application/json'
+
+ if not check_auth(request):
+ abort(404, "No Access")
+
+ json = '{ "downloads": ['
+
+ downloads = core_methods.status_downloads()
+ ids = []
+
+ for dl in downloads:
+ ids.append(dl['id'])
+ json += '{'
+ json += '"id": %s, "name": "%s", "speed": %s, "eta": "%s", "kbleft": %s, "size": %s, "percent": %s, "wait": "%s", "status": "%s"'\
+ % (str(dl['id']), str(dl['name']), str(int(dl['speed'])), str(core.format_time(dl['eta'])), dl['kbleft'], dl['size'], dl['percent'], str(core.format_time(dl['wait_until'] - time.time())), dl['status'])
+
+ json += "},"
+
+ if json.endswith(","): json = json[:-1]
+
+ json += '], "ids": %s }' % str(ids)
+
+ return json
+
+@route('/json/status')
+def get_status():
+ response.header['Cache-Control'] = 'no-cache, must-revalidate'
+ response.content_type = 'application/json'
+
+ if not check_auth(request):
+ abort(404, "No Access")
+
+ data = core_methods.status_server()
+
+ if data['pause']:
+ status = "paused"
+ else:
+ status = "running"
+
+ json = '{ "status": "%s", "speed": "%s", "queue": "%s" }' % (status, str(int(data['speed'])), str(data['queue']))
+
+ return json
+
+@route('/json/addlinks', method='POST')
+def add_links():
+ response.header['Cache-Control'] = 'no-cache, must-revalidate'
+ response.content_type = 'application/json'
+
+ if not check_auth(request):
+ abort(404, "No Access")
+
+ links = request.POST['links'].split('\n')
+
+ core.add_links(links)
+
+ return "{}"
+
+@route('/json/pause')
+def pause():
+ response.header['Cache-Control'] = 'no-cache, must-revalidate'
+ response.content_type = 'application/json'
+
+ if not check_auth(request):
+ abort(404, "No Access")
+
+ core.thread_list.pause = True
+
+ return "{}"
+
+
+@route('/json/play')
+def play():
+ response.header['Cache-Control'] = 'no-cache, must-revalidate'
+ response.content_type = 'application/json'
+
+ if not check_auth(request):
+ abort(404, "No Access")
+
+ core.thread_list.pause = False
+
+ return "{}"
+
+@route('/favicon.ico')
+def favicon():
+
+ if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified")
+
+ response.header['Last-Modified'] = TIME
+
+ send_file('favicon.ico', root=(PATH + 'static/'))
+
+@route('static/:section/:filename')
+def static_folder(section, filename):
+
+ if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified")
+
+ response.header['Last-Modified'] = TIME
+ send_file(filename, root=(PATH + 'static/' + section))
+
+@route('/static/:filename')
+def static_file(filename):
+
+ if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified")
+
+ response.header['Last-Modified'] = TIME
+ send_file(filename, root=(PATH + 'static/'))
+
+
+def check_auth(req):
+
+ try:
+ user = req.COOKIES.get('user')
+ id = int(req.COOKIES.get('id'))
+ ua = req.HEADER("HTTP_USER_AGENT")
+ ip = req.HEADER("REMOTE_ADDR")
+
+ if USERS[id]['user'] == user and USERS[id]['ua'] == ua and USERS[id]['ip'] == ip:
+ return True
+ except:
+ return False
+
+ return False
+
+
+class WebServer(threading.Thread):
+ def __init__(self, pycore):
+ threading.Thread.__init__(self)
+
+ global core, core_methods, TIME
+ core = pycore
+ core_methods = pycore.server_methods
+ self.core = pycore
+ self.setDaemon(True)
+
+ if pycore.config['general']['debug_mode']:
+ bottle.debug(True)
+ TIME = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime())
else:
- try:
- print open(self.path[1:], 'r').read()
- except IOError:
- self.send_error(404)
-
- def format_size(self, size):
- return str(size / 1024) + " MiB"
-
- def format_time(self,seconds):
- seconds = int(seconds)
- hours, seconds = divmod(seconds, 3600)
- minutes, seconds = divmod(seconds, 60)
- return "%.2i:%.2i:%.2i" % (hours, minutes, seconds)
-
- def get_downloads(self):
- data = coreserver.status_downloads()
- for download in data:
- print "<h3>%s</h3>" % download["name"]
- if download["status"] == "downloading":
- percent = download["percent"]
- z = percent / 4
- print "<h3>%s</h3>" % dl_name
- print "<font face='font-family:Fixedsys,Courier,monospace;'>[" + z * "#" + (25-z) * "&nbsp;" + "]</font>" + str(percent) + "%<br />"
- print "Speed: " + str(int(download['speed'])) + " kb/s"
- print "Size: " + self.format_size(download['size'])
- print "Finished in: " + self.format_time(download['eta'])
- print "ID: " + str(download['id'])
- dl_status = "[" + z * "#" + (25-z) * " " + "] " + str(percent) + "%" + " Speed: " + str(int(download['speed'])) + " kb/s" + " Size: " + self.format_size(download['size']) + " Finished in: " + self.format_time(download['eta']) + " ID: " + str(download['id'])
- if download["status"] == "waiting":
- print "waiting: " + self.format_time(download["wait_until"]- time())
-
-is_url = re.compile("^(((https?|ftp)\:\/\/)?([\w\.\-]+(\:[\w\.\&%\$\-]+)*@)?((([^\s\(\)\<\>\\\"\.\[\]\,@;:]+)(\.[^\s\(\)\<\>\\\"\.\[\]\,@;:]+)*(\.[a-zA-Z]{2,4}))|((([01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}([01]?\d{1,2}|2[0-4]\d|25[0-5])))(\b\:(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}|0)\b)?((\/[^\/][\w\.\,\?\'\\\/\+&%\$#\=~_\-@]*)*[^\.\,\?\"\'\(\)\[\]!;<>{}\s\x7F-\xFF])?)$",re.IGNORECASE)
-
-coreserver = None
-
-class WebServer():
-
- def start(self):
- try:
- global coreserver
- coreserver = ServerProxy("https://testuser:testpw@localhost:1337", allow_none=True)
- webserver = HTTPServer(('',8080),Handler)
- print 'server started at port 8080'
- webserver.serve_forever()
- except KeyboardInterrupt:
- webserver.socket.close()
-
-if __name__ == "__main__":
- web = WebServer()
- web.start()
+ bottle.debug(False)
+
+ #@TODO remove
+ #TIME = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime())
+ bottle.TEMPLATE_PATH.append('./module/web/templates/')
+
+ def run(self):
+ self.core.logger.info("Starting Webinterface on %s port %s" % (self.core.config['webinterface']['listenaddr'],self.core.config['webinterface']['port']))
+ try:
+ run(host=self.core.config['webinterface']['listenaddr'], port=int(self.core.config['webinterface']['port']), quiet=True)
+ except:
+ self.core.logger.error("Failed starting webserver, no webinterface available: Can't create socket")
+ exit() \ No newline at end of file
diff --git a/module/web/bottle.py b/module/web/bottle.py
index 66ceb527f..41a8c8fc0 100644
--- a/module/web/bottle.py
+++ b/module/web/bottle.py
@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
"""
-Bottle is a fast and simple mirco-framework for small web-applications. It
-offers request dispatching (Routes) with url parameter support, Templates,
-key/value Databases, a build-in HTTP Server and adapters for many third party
-WSGI/HTTP-server and template engines. All in a single file and with no
+Bottle is a fast and simple micro-framework for small web applications. It
+offers request dispatching (Routes) with url parameter support, templates,
+key/value databases, a built-in HTTP Server and adapters for many third party
+WSGI/HTTP-server and template engines - all in a single file and with no
dependencies other than the Python Standard Library.
Homepage and documentation: http://wiki.github.com/defnull/bottle
Special thanks to Stefan Matthias Aust [http://github.com/sma]
- for his contribution to SimpelTemplate
+ for his contribution to SimpleTemplate
Licence (MIT)
-------------
@@ -62,9 +62,10 @@ Example
"""
__author__ = 'Marcel Hellkamp'
-__version__ = '0.5.7'
+__version__ = '0.6.4'
__license__ = 'MIT'
+import types
import sys
import cgi
import mimetypes
@@ -75,26 +76,31 @@ import re
import random
import threading
import time
+import warnings
+import email.utils
from wsgiref.headers import Headers as HeaderWrapper
from Cookie import SimpleCookie
import anydbm as dbm
+import subprocess
+import thread
+
try:
from urlparse import parse_qs
-except ImportError:
+except ImportError: # pragma: no cover
from cgi import parse_qs
try:
import cPickle as pickle
-except ImportError:
+except ImportError: # pragma: no cover
import pickle as pickle
try:
try:
from json import dumps as json_dumps
- except ImportError:
+ except ImportError: # pragma: no cover
from simplejson import dumps as json_dumps
-except ImportError:
+except ImportError: # pragma: no cover
json_dumps = None
@@ -105,38 +111,37 @@ except ImportError:
# Exceptions and Events
class BottleException(Exception):
- """ A base class for exceptions used by bottle."""
+ """ A base class for exceptions used by bottle. """
pass
class HTTPError(BottleException):
- """ A way to break the execution and instantly jump to an error handler. """
+ """
+ A way to break the execution and instantly jump to an error handler.
+ """
def __init__(self, status, text):
self.output = text
self.http_status = int(status)
+ BottleException.__init__(self, status, text)
def __repr__(self):
- return "HTTPError(%d,%s)" % (self.http_status, repr(self.output))
+ return 'HTTPError(%d,%s)' % (self.http_status, repr(self.output))
def __str__(self):
- out = []
- status = self.http_status
- name = HTTP_CODES.get(status,'Unknown').title()
- url = request.path
- out.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">')
- out.append('<html><head><title>Error %d: %s</title>' % (status, name))
- out.append('</head><body><h1>Error %d: %s</h1>' % (status, name))
- out.append('<p>Sorry, the requested URL "%s" caused an error.</p>' % url)
- out.append(''.join(list(self.output)))
- out.append('</body></html>')
- return "\n".join(out)
+ return HTTP_ERROR_TEMPLATE % {
+ 'status' : self.http_status,
+ 'url' : request.path,
+ 'error_name' : HTTP_CODES.get(self.http_status, 'Unknown').title(),
+ 'error_message' : ''.join(self.output)
+ }
class BreakTheBottle(BottleException):
- """ Not an exception, but a straight jump out of the controller code.
-
+ """
+ Not an exception, but a straight jump out of the controller code.
Causes the Bottle to instantly call start_response() and return the
- content of output """
+ content of output
+ """
def __init__(self, output):
self.output = output
@@ -149,8 +154,10 @@ class BreakTheBottle(BottleException):
_default_app = None
def default_app(newapp = None):
- ''' Returns the current default app or sets a new one.
- Defaults to an instance of Bottle '''
+ """
+ Returns the current default app or sets a new one.
+ Defaults to an instance of Bottle
+ """
global _default_app
if newapp:
_default_app = newapp
@@ -161,17 +168,20 @@ def default_app(newapp = None):
class Bottle(object):
- def __init__(self, catchall=True, debug=False, optimize=False, autojson=True):
+ def __init__(self, catchall=True, optimize=False, autojson=True):
self.simple_routes = {}
self.regexp_routes = {}
+ self.default_route = None
self.error_handler = {}
self.optimize = optimize
- self.debug = debug
self.autojson = autojson
self.catchall = catchall
+ self.serve = True
def match_url(self, url, method='GET'):
- """Returns the first matching handler and a parameter dict or (None, None) """
+ """
+ Returns the first matching handler and a parameter dict or (None, None)
+ """
url = url.strip().lstrip("/ ")
# Search for static routes first
route = self.simple_routes.get(method,{}).get(url,None)
@@ -186,47 +196,116 @@ class Bottle(object):
if i > 0 and self.optimize and random.random() <= 0.001:
routes[i-1], routes[i] = routes[i], routes[i-1]
return (handler, match.groupdict())
- return (None, None)
-
- def add_route(self, route, handler, method='GET', simple=False):
+ if self.default_route:
+ return (self.default_route, {})
+ if method == 'HEAD': # Fall back to GET
+ return self.match_url(url)
+ else:
+ return (None, None)
+
+ def add_controller(self, route, controller, **kargs):
+ """ Adds a controller class or object """
+ if '{action}' not in route and 'action' not in kargs:
+ raise BottleException("Routes to controller classes or object MUST"
+ " contain an {action} placeholder or use the action-parameter")
+ for action in (m for m in dir(controller) if not m.startswith('_')):
+ handler = getattr(controller, action)
+ if callable(handler) and action == kargs.get('action', action):
+ self.add_route(route.replace('{action}', action), handler, **kargs)
+
+ def add_route(self, route, handler, method='GET', simple=False, **kargs):
""" Adds a new route to the route mappings. """
+ if isinstance(handler, type) and issubclass(handler, BaseController):
+ handler = handler()
+ if isinstance(handler, BaseController):
+ self.add_controller(route, handler, method=method, simple=simple, **kargs)
+ return
method = method.strip().upper()
route = route.strip().lstrip('$^/ ').rstrip('$^ ')
if re.match(r'^(\w+/)*\w*$', route) or simple:
self.simple_routes.setdefault(method, {})[route] = handler
else:
- route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)',r'(?P<\1>\g<re>)',route)
- route = re.sub(r':([a-zA-Z_]+)',r'(?P<\1>[^/]+)', route)
+ route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)',
+ r'(?P<\1>\g<re>)',route)
+ route = re.sub(r':([a-zA-Z_]+)', r'(?P<\1>[^/]+)', route)
route = re.compile('^%s$' % route)
self.regexp_routes.setdefault(method, []).append([route, handler])
def route(self, url, **kargs):
- """ Decorator for request handler. Same as add_route(url, handler, **kargs)."""
+ """
+ Decorator for request handler.
+ Same as add_route(url, handler, **kargs).
+ """
def wrapper(handler):
self.add_route(url, handler, **kargs)
return handler
return wrapper
+ def set_default(self, handler):
+ self.default_route = handler
+
+ def default(self):
+ """ Decorator for request handler. Same as add_defroute( handler )."""
+ def wrapper(handler):
+ self.set_default(handler)
+ return handler
+ return wrapper
+
def set_error_handler(self, code, handler):
""" Adds a new error handler. """
- code = int(code)
- self.error_handler[code] = handler
+ self.error_handler[int(code)] = handler
def error(self, code=500):
- """ Decorator for error handler. Same as set_error_handler(code, handler)."""
+ """
+ Decorator for error handler.
+ Same as set_error_handler(code, handler).
+ """
def wrapper(handler):
self.set_error_handler(code, handler)
return handler
return wrapper
+ def cast(self, out):
+ """
+ Cast the output to an iterable of strings or something WSGI can handle.
+ Set Content-Type and Content-Length when possible. Then clear output
+ on HEAD requests.
+ Supports: False, str, unicode, list(unicode), dict(), open()
+ """
+ if not out:
+ out = []
+ response.header['Content-Length'] = '0'
+ elif isinstance(out, types.StringType):
+ out = [out]
+ elif isinstance(out, unicode):
+ out = [out.encode(response.charset)]
+ elif isinstance(out, list) and isinstance(out[0], unicode):
+ out = map(lambda x: x.encode(response.charset), out)
+ elif self.autojson and json_dumps and isinstance(out, dict):
+ out = [json_dumps(out)]
+ response.content_type = 'application/json'
+ elif hasattr(out, 'read'):
+ out = request.environ.get('wsgi.file_wrapper',
+ lambda x: iter(lambda: x.read(8192), ''))(out)
+ if isinstance(out, list) and len(out) == 1:
+ response.header['Content-Length'] = str(len(out[0]))
+ if not hasattr(out, '__iter__'):
+ raise TypeError('Request handler for route "%s" returned [%s] '
+ 'which is not iterable.' % (request.path, type(out).__name__))
+ return out
+
+
def __call__(self, environ, start_response):
- """ The bottle WSGI-interface ."""
+ """ The bottle WSGI-interface. """
request.bind(environ)
response.bind()
try: # Unhandled Exceptions
try: # Bottle Error Handling
+ if not self.serve:
+ abort(503, "Server stopped")
handler, args = self.match_url(request.path, request.method)
- if not handler: raise HTTPError(404, "Not found")
+ if not handler:
+ raise HTTPError(404, "Not found")
output = handler(**args)
db.close()
except BreakTheBottle, e:
@@ -234,26 +313,18 @@ class Bottle(object):
except HTTPError, e:
response.status = e.http_status
output = self.error_handler.get(response.status, str)(e)
- # output casting
- if hasattr(output, 'read'):
- output = environ.get('wsgi.file_wrapper', lambda x: iter(lambda: x.read(8192), ''))(output)
- elif self.autojson and json_dumps and isinstance(output, dict):
- output = json_dumps(output)
- response.content_type = 'application/json'
- if isinstance(output, str):
- response.header['Content-Length'] = str(len(output))
- output = [output]
+ output = self.cast(output)
+ if response.status in (100, 101, 204, 304) or request.method == 'HEAD':
+ output = [] # rfc2616 section 4.3
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception, e:
response.status = 500
if self.catchall:
err = "Unhandled Exception: %s\n" % (repr(e))
- if self.debug:
- err += "<h2>Traceback:</h2>\n<pre>\n"
- err += traceback.format_exc(10)
- err += "\n</pre>"
- output = str(HTTPError(500, err))
+ if DEBUG:
+ err += TRACEBACK_TEMPLATE % traceback.format_exc(10)
+ output = [str(HTTPError(500, err))]
request._environ['wsgi.errors'].write(err)
else:
raise
@@ -267,8 +338,11 @@ class Request(threading.local):
""" Represents a single request using thread-local namespace. """
def bind(self, environ):
- """ Binds the enviroment of the current request to this request handler """
+ """
+ Binds the enviroment of the current request to this request handler
+ """
self._environ = environ
+ self.environ = self._environ
self._GET = None
self._POST = None
self._GETPOST = None
@@ -279,25 +353,25 @@ class Request(threading.local):
@property
def method(self):
- ''' Returns the request method (GET,POST,PUT,DELETE,...) '''
+ """ Get the request method (GET,POST,PUT,DELETE,...) """
return self._environ.get('REQUEST_METHOD', 'GET').upper()
@property
def query_string(self):
- ''' Content of QUERY_STRING '''
+ """ Get content of QUERY_STRING """
return self._environ.get('QUERY_STRING', '')
@property
def input_length(self):
- ''' Content of CONTENT_LENGTH '''
+ """ Get content of CONTENT_LENGTH """
try:
- return int(self._environ.get('CONTENT_LENGTH', '0'))
+ return max(0,int(self._environ.get('CONTENT_LENGTH', '0')))
except ValueError:
return 0
@property
def GET(self):
- """Returns a dict with GET parameters."""
+ """ Get a dict with GET parameters. """
if self._GET is None:
data = parse_qs(self.query_string, keep_blank_values=True)
self._GET = {}
@@ -310,9 +384,10 @@ class Request(threading.local):
@property
def POST(self):
- """Returns a dict with parsed POST or PUT data."""
+ """ Get a dict with parsed POST or PUT data. """
if self._POST is None:
- data = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ, keep_blank_values=True)
+ data = cgi.FieldStorage(fp=self._environ['wsgi.input'],
+ environ=self._environ, keep_blank_values=True)
self._POST = {}
for item in data.list:
name = item.name
@@ -326,7 +401,7 @@ class Request(threading.local):
@property
def params(self):
- ''' Returns a mix of GET and POST data. POST overwrites GET '''
+ """ Returns a mix of GET and POST data. POST overwrites GET """
if self._GETPOST is None:
self._GETPOST = dict(self.GET)
self._GETPOST.update(dict(self.POST))
@@ -334,7 +409,7 @@ class Request(threading.local):
@property
def COOKIES(self):
- """Returns a dict with COOKIES."""
+ """ Returns a dict with COOKIES. """
if self._COOKIES is None:
raw_dict = SimpleCookie(self._environ.get('HTTP_COOKIE',''))
self._COOKIES = {}
@@ -357,6 +432,7 @@ class Response(threading.local):
self.header = HeaderWrapper(self.header_list)
self.content_type = 'text/html'
self.error = None
+ self.charset = 'utf8'
def wsgiheaders(self):
''' Returns a wsgi conform list of header/value pairs '''
@@ -371,19 +447,33 @@ class Response(threading.local):
return self._COOKIES
def set_cookie(self, key, value, **kargs):
- """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """
+ """
+ Sets a Cookie. Optional settings:
+ expires, path, comment, domain, max-age, secure, version, httponly
+ """
self.COOKIES[key] = value
- for k in kargs:
- self.COOKIES[key][k] = kargs[k]
+ for k, v in kargs.iteritems():
+ self.COOKIES[key][k] = v
def get_content_type(self):
- '''Gives access to the 'Content-Type' header and defaults to 'text/html'.'''
+ """ Get the current 'Content-Type' header. """
return self.header['Content-Type']
def set_content_type(self, value):
+ if 'charset=' in value:
+ self.charset = value.split('charset=')[-1].split(';')[0].strip()
self.header['Content-Type'] = value
-
- content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__)
+
+ content_type = property(get_content_type, set_content_type, None,
+ get_content_type.__doc__)
+
+
+class BaseController(object):
+ _singleton = None
+ def __new__(cls, *a, **k):
+ if not cls._singleton:
+ cls._singleton = object.__new__(cls, *a, **k)
+ return cls._singleton
def abort(code=500, text='Unknown Error: Appliction stopped.'):
@@ -398,12 +488,11 @@ def redirect(url, code=307):
raise BreakTheBottle("")
-def send_file(filename, root, guessmime = True, mimetype = 'text/plain'):
+def send_file(filename, root, guessmime = True, mimetype = None):
""" Aborts execution and sends a static files as response. """
- root = os.path.abspath(root) + '/'
- filename = os.path.normpath(filename).strip('/')
- filename = os.path.join(root, filename)
-
+ root = os.path.abspath(root) + os.sep
+ filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
+
if not filename.startswith(root):
abort(401, "Access denied.")
if not os.path.exists(filename) or not os.path.isfile(filename):
@@ -411,25 +500,41 @@ def send_file(filename, root, guessmime = True, mimetype = 'text/plain'):
if not os.access(filename, os.R_OK):
abort(401, "You do not have permission to access this file.")
- if guessmime:
- guess = mimetypes.guess_type(filename)[0]
- if guess:
- response.content_type = guess
- elif mimetype:
- response.content_type = mimetype
- elif mimetype:
- response.content_type = mimetype
+ if guessmime and not mimetype:
+ mimetype = mimetypes.guess_type(filename)[0]
+ if not mimetype: mimetype = 'text/plain'
+ response.content_type = mimetype
stats = os.stat(filename)
- # TODO: HTTP_IF_MODIFIED_SINCE -> 304 (Thu, 02 Jul 2009 23:16:31 CEST)
- if 'Content-Length' not in response.header:
- response.header['Content-Length'] = stats.st_size
if 'Last-Modified' not in response.header:
- ts = time.gmtime(stats.st_mtime)
- ts = time.strftime("%a, %d %b %Y %H:%M:%S +0000", ts)
- response.header['Last-Modified'] = ts
+ lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
+ response.header['Last-Modified'] = lm
+ if 'HTTP_IF_MODIFIED_SINCE' in request.environ:
+ ims = request.environ['HTTP_IF_MODIFIED_SINCE']
+ # IE sends "<date>; length=146"
+ ims = ims.split(";")[0].strip()
+ ims = parse_date(ims)
+ if ims is not None and ims >= stats.st_mtime:
+ abort(304, "Not modified")
+ if 'Content-Length' not in response.header:
+ response.header['Content-Length'] = str(stats.st_size)
+ raise BreakTheBottle(open(filename, 'rb'))
+
- raise BreakTheBottle(open(filename, 'r'))
+def parse_date(ims):
+ """
+ Parses date strings usually found in HTTP header and returns UTC epoch.
+ Understands rfc1123, rfc850 and asctime.
+ """
+ try:
+ ts = email.utils.parsedate_tz(ims)
+ if ts is not None:
+ if ts[9] is None:
+ return time.mktime(ts[:8] + (0,)) - time.timezone
+ else:
+ return time.mktime(ts[:8] + (0,)) - ts[9] - time.timezone
+ except (ValueError, IndexError):
+ return None
@@ -439,15 +544,17 @@ def send_file(filename, root, guessmime = True, mimetype = 'text/plain'):
# Decorators
def validate(**vkargs):
- ''' Validates and manipulates keyword arguments by user defined callables
- and handles ValueError and missing arguments by raising HTTPError(403) '''
+ """
+ Validates and manipulates keyword arguments by user defined callables.
+ Handles ValueError and missing arguments by raising HTTPError(403).
+ """
def decorator(func):
def wrapper(**kargs):
- for key in vkargs:
+ for key, value in vkargs.iteritems():
if key not in kargs:
abort(403, 'Missing parameter: %s' % key)
try:
- kargs[key] = vkargs[key](kargs[key])
+ kargs[key] = value(kargs[key])
except ValueError, e:
abort(403, 'Wrong parameter format for: %s' % key)
return func(**kargs)
@@ -456,12 +563,21 @@ def validate(**vkargs):
def route(url, **kargs):
- """ Decorator for request handler. Same as add_route(url, handler, **kargs)."""
+ """
+ Decorator for request handler. Same as add_route(url, handler, **kargs).
+ """
return default_app().route(url, **kargs)
+def default():
+ """
+ Decorator for request handler. Same as set_default(handler).
+ """
+ return default_app().default()
def error(code=500):
- """ Decorator for error handler. Same as set_error_handler(code, handler)."""
+ """
+ Decorator for error handler. Same as set_error_handler(code, handler).
+ """
return default_app().error(code)
@@ -471,8 +587,23 @@ def error(code=500):
# Server adapter
-class ServerAdapter(object):
+class WSGIAdapter(object):
+ def run(self, handler): # pragma: no cover
+ pass
+
+ def __repr__(self):
+ return "%s()" % (self.__class__.__name__)
+
+
+class CGIServer(WSGIAdapter):
+ def run(self, handler):
+ from wsgiref.handlers import CGIHandler
+ CGIHandler().run(handler)
+
+
+class ServerAdapter(WSGIAdapter):
def __init__(self, host='127.0.0.1', port=8080, **kargs):
+ WSGIAdapter.__init__(self)
self.host = host
self.port = int(port)
self.options = kargs
@@ -480,9 +611,6 @@ class ServerAdapter(object):
def __repr__(self):
return "%s (%s:%d)" % (self.__class__.__name__, self.host, self.port)
- def run(self, handler):
- pass
-
class WSGIRefServer(ServerAdapter):
def run(self, handler):
@@ -513,12 +641,14 @@ class PasteServer(ServerAdapter):
class FapwsServer(ServerAdapter):
- """ Extreamly fast Webserver using libev (see http://william-os4y.livejournal.com/)
- Experimental ... """
+ """
+ Extremly fast webserver using libev.
+ See http://william-os4y.livejournal.com/
+ Experimental ...
+ """
def run(self, handler):
import fapws._evwsgi as evwsgi
from fapws import base
- import sys
evwsgi.start(self.host, self.port)
evwsgi.set_base_module(base)
def app(environ, start_response):
@@ -528,35 +658,75 @@ class FapwsServer(ServerAdapter):
evwsgi.run()
-def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, **kargs):
- """ Runs bottle as a web server, using Python's built-in wsgiref implementation by default.
-
- You may choose between WSGIRefServer, CherryPyServer, FlupServer and
- PasteServer or write your own server adapter.
- """
+def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080,
+ interval=1, reloader=False, **kargs):
+ """ Runs bottle as a web server. """
if not app:
app = default_app()
- quiet = bool('quiet' in kargs and kargs['quiet'])
-
- # Instanciate server, if it is a class instead of an instance
- if isinstance(server, type) and issubclass(server, ServerAdapter):
- server = server(host=host, port=port, **kargs)
+ quiet = bool(kargs.get('quiet', False))
- if not isinstance(server, ServerAdapter):
- raise RuntimeError("Server must be a subclass of ServerAdapter")
+ # Instantiate server, if it is a class instead of an instance
+ if isinstance(server, type):
+ if issubclass(server, CGIServer):
+ server = server()
+ elif issubclass(server, ServerAdapter):
+ server = server(host=host, port=port, **kargs)
- if not quiet:
- print 'Bottle server starting up (using %s)...' % repr(server)
- print 'Listening on http://%s:%d/' % (server.host, server.port)
- print 'Use Ctrl-C to quit.'
- print
+ if not isinstance(server, WSGIAdapter):
+ raise RuntimeError("Server must be a subclass of WSGIAdapter")
+
+ if not quiet and isinstance(server, ServerAdapter): # pragma: no cover
+ if not reloader or os.environ.get('BOTTLE_CHILD') == 'true':
+ print "Bottle server starting up (using %s)..." % repr(server)
+ print "Listening on http://%s:%d/" % (server.host, server.port)
+ print "Use Ctrl-C to quit."
+ print
+ else:
+ print "Bottle auto reloader starting up..."
try:
- server.run(app)
+ if reloader and interval:
+ reloader_run(server, app, interval)
+ else:
+ server.run(app)
except KeyboardInterrupt:
- print "Shuting down..."
-
+ if not quiet: # pragma: no cover
+ print "Shutting Down..."
+
+
+#TODO: If the parent process is killed (with SIGTERM) the childs survive...
+def reloader_run(server, app, interval):
+ if os.environ.get('BOTTLE_CHILD') == 'true':
+ # We are a child process
+ files = dict()
+ for module in sys.modules.values():
+ file_path = getattr(module, '__file__', None)
+ if file_path and os.path.isfile(file_path):
+ file_split = os.path.splitext(file_path)
+ if file_split[1] in ('.py', '.pyc', '.pyo'):
+ file_path = file_split[0] + '.py'
+ files[file_path] = os.stat(file_path).st_mtime
+ thread.start_new_thread(server.run, (app,))
+ while True:
+ time.sleep(interval)
+ for file_path, file_mtime in files.iteritems():
+ if not os.path.exists(file_path):
+ print "File changed: %s (deleted)" % file_path
+ elif os.stat(file_path).st_mtime > file_mtime:
+ print "File changed: %s (modified)" % file_path
+ else: continue
+ print "Restarting..."
+ app.serve = False
+ time.sleep(interval) # be nice and wait for running requests
+ sys.exit(3)
+ while True:
+ args = [sys.executable] + sys.argv
+ environ = os.environ.copy()
+ environ['BOTTLE_CHILD'] = 'true'
+ exit_status = subprocess.call(args, env=environ)
+ if exit_status != 3:
+ sys.exit(exit_status)
@@ -564,89 +734,183 @@ def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, **kargs):
# Templates
+class TemplateError(HTTPError):
+ def __init__(self, message):
+ HTTPError.__init__(self, 500, message)
+
class BaseTemplate(object):
- def __init__(self, template='', filename=None):
- self.source = filename
- if self.source:
- fp = open(filename)
- template = fp.read()
- fp.close()
- self.parse(template)
-
- def parse(self, template): raise NotImplementedError
- def render(self, **args): raise NotImplementedError
-
- @classmethod
- def find(cls, name):
- for path in TEMPLATE_PATH:
- if os.path.isfile(path % name):
- return cls(filename = path % name)
- return None
+ def __init__(self, template='', name=None, filename=None, lookup=[]):
+ """
+ Create a new template.
+ If a name is provided, but no filename and no template string, the
+ filename is guessed using the lookup path list.
+ Subclasses can assume that either self.template or self.filename is set.
+ If both are present, self.template should be used.
+ """
+ self.name = name
+ self.filename = filename
+ self.template = template
+ self.lookup = lookup
+ if self.name and not self.filename:
+ for path in self.lookup:
+ fpath = os.path.join(path, self.name+'.tpl')
+ if os.path.isfile(fpath):
+ self.filename = fpath
+ if not self.template and not self.filename:
+ raise TemplateError('Template (%s) not found.' % self.name)
+ self.prepare()
+
+ def prepare(self):
+ """
+ Run preparatios (parsing, caching, ...).
+ It should be possible to call this multible times to refresh a template.
+ """
+ raise NotImplementedError
+
+ def render(self, **args):
+ """
+ Render the template with the specified local variables and return an
+ iterator of strings (bytes). This must be thread save!
+ """
+ raise NotImplementedError
class MakoTemplate(BaseTemplate):
- def parse(self, template):
+ output_encoding=None
+ input_encoding=None
+ default_filters=None
+ global_variables={}
+
+ def prepare(self):
from mako.template import Template
- self.tpl = Template(template)
+ from mako.lookup import TemplateLookup
+ #TODO: This is a hack... http://github.com/defnull/bottle/issues#issue/8
+ mylookup = TemplateLookup(directories=map(os.path.abspath, self.lookup)+['./'])
+ if self.template:
+ self.tpl = Template(self.template,
+ lookup=mylookup,
+ output_encoding=MakoTemplate.output_encoding,
+ input_encoding=MakoTemplate.input_encoding,
+ default_filters=MakoTemplate.default_filters
+ )
+ else:
+ self.tpl = Template(filename=self.filename,
+ lookup=mylookup,
+ output_encoding=MakoTemplate.output_encoding,
+ input_encoding=MakoTemplate.input_encoding,
+ default_filters=MakoTemplate.default_filters
+ )
def render(self, **args):
- return self.tpl.render(**args)
+ _defaults = MakoTemplate.global_variables.copy()
+ _defaults.update(args)
+ return self.tpl.render(**_defaults)
class CheetahTemplate(BaseTemplate):
- def parse(self, template):
+ def prepare(self):
from Cheetah.Template import Template
self.context = threading.local()
self.context.vars = {}
- self.tpl = Template(source = template, searchList=[self.context.vars])
+ if self.template:
+ self.tpl = Template(source=self.template, searchList=[self.context.vars])
+ else:
+ self.tpl = Template(file=self.filename, searchList=[self.context.vars])
def render(self, **args):
self.context.vars.update(args)
out = str(self.tpl)
self.context.vars.clear()
- return out
+ return [out]
+
+
+class Jinja2Template(BaseTemplate):
+ env = None # hopefully, a Jinja environment is actually thread-safe
+
+ def prepare(self):
+ if not self.env:
+ from jinja2 import Environment, FunctionLoader
+ self.env = Environment(line_statement_prefix="#", loader=FunctionLoader(self.loader))
+ if self.template:
+ self.tpl = self.env.from_string(self.template)
+ else:
+ self.tpl = self.env.get_template(self.filename)
+
+ def render(self, **args):
+ return self.tpl.render(**args).encode("utf-8")
+
+ def loader(self, name):
+ if not name.endswith(".tpl"):
+ for path in self.lookup:
+ fpath = os.path.join(path, name+'.tpl')
+ if os.path.isfile(fpath):
+ name = fpath
+ break
+ f = open(name)
+ try: return f.read()
+ finally: f.close()
class SimpleTemplate(BaseTemplate):
- re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include)|(end)|(.*))')
+ re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|'
+ 'while|with|def|class)|(include|rebase)|(end)|(.*))')
re_inline = re.compile(r'\{\{(.*?)\}\}')
dedent_keywords = ('elif', 'else', 'except', 'finally')
+ def prepare(self):
+ if self.template:
+ code = self.translate(self.template)
+ self.co = compile(code, '<string>', 'exec')
+ else:
+ code = self.translate(open(self.filename).read())
+ self.co = compile(code, self.filename, 'exec')
+
def translate(self, template):
indent = 0
strbuffer = []
code = []
- self.subtemplates = {}
+ self.includes = dict()
class PyStmt(str):
def __repr__(self): return 'str(' + self + ')'
def flush(allow_nobreak=False):
if len(strbuffer):
if allow_nobreak and strbuffer[-1].endswith("\\\\\n"):
strbuffer[-1]=strbuffer[-1][:-3]
- code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer)))
- code.append((" " * indent + "\n") * len(strbuffer)) # to preserve line numbers
+ code.append(' ' * indent + "_stdout.append(%s)" % repr(''.join(strbuffer)))
+ code.append((' ' * indent + '\n') * len(strbuffer)) # to preserve line numbers
del strbuffer[:]
for line in template.splitlines(True):
m = self.re_python.match(line)
if m:
flush(allow_nobreak=True)
- keyword, include, end, statement = m.groups()
+ keyword, subtpl, end, statement = m.groups()
if keyword:
if keyword in self.dedent_keywords:
indent -= 1
code.append(" " * indent + line[m.start(1):])
indent += 1
- elif include:
+ elif subtpl:
tmp = line[m.end(2):].strip().split(None, 1)
- name = tmp[0]
- args = tmp[1:] and tmp[1] or ''
- self.subtemplates[name] = SimpleTemplate.find(name)
- code.append(" " * indent + "stdout.append(_subtemplates[%s].render(%s))\n" % (repr(name), args))
+ if not tmp:
+ code.append(' ' * indent + "_stdout.extend(_base)\n")
+ else:
+ name = tmp[0]
+ args = tmp[1:] and tmp[1] or ''
+ if name not in self.includes:
+ self.includes[name] = SimpleTemplate(name=name, lookup=self.lookup)
+ if subtpl == 'include':
+ code.append(' ' * indent +
+ "_ = _includes[%s].execute(_stdout, %s)\n"
+ % (repr(name), args))
+ else:
+ code.append(' ' * indent +
+ "_tpl['_rebase'] = (_includes[%s], dict(%s))\n"
+ % (repr(name), args))
elif end:
indent -= 1
- code.append(" " * indent + '#' + line[m.start(3):])
+ code.append(' ' * indent + '#' + line[m.start(3):])
elif statement:
- code.append(" " * indent + line[m.start(4):])
+ code.append(' ' * indent + line[m.start(4):])
else:
splits = self.re_inline.split(line) # text, (expr, text)*
if len(splits) == 1:
@@ -656,40 +920,86 @@ class SimpleTemplate(BaseTemplate):
for i in range(1, len(splits), 2):
splits[i] = PyStmt(splits[i])
splits = [x for x in splits if bool(x)]
- code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits))
+ code.append(' ' * indent + "_stdout.extend(%s)\n" % repr(splits))
flush()
return ''.join(code)
- def parse(self, template):
- code = self.translate(template)
- self.co = compile(code, self.source or '<template>', 'exec')
-
- def render(self, **args):
- ''' Returns the rendered template using keyword arguments as local variables. '''
- args['stdout'] = []
- args['_subtemplates'] = self.subtemplates
+ def execute(self, stdout, **args):
+ args['_stdout'] = stdout
+ args['_includes'] = self.includes
+ args['_tpl'] = args
eval(self.co, args)
- return ''.join(args['stdout'])
+ if '_rebase' in args:
+ subtpl, args = args['_rebase']
+ args['_base'] = stdout[:] #copy stdout
+ del stdout[:] # clear stdout
+ return subtpl.execute(stdout, **args)
+ return args
+ def render(self, **args):
+ """ Render the template using keyword arguments as local variables. """
+ stdout = []
+ self.execute(stdout, **args)
+ return stdout
+
-def template(template, template_adapter=SimpleTemplate, **args):
- ''' Returns a string from a template '''
- if template not in TEMPLATES:
- if template.find("\n") == template.find("{") == template.find("%") == -1:
- TEMPLATES[template] = template_adapter.find(template)
+def template(tpl, template_adapter=SimpleTemplate, **args):
+ '''
+ Get a rendered template as a string iterator.
+ You can use a name, a filename or a template string as first parameter.
+ '''
+ lookup = args.get('template_lookup', TEMPLATE_PATH)
+ if tpl not in TEMPLATES or DEBUG:
+ if "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
+ TEMPLATES[tpl] = template_adapter(template=tpl, lookup=lookup)
+ elif '.' in tpl:
+ TEMPLATES[tpl] = template_adapter(filename=tpl, lookup=lookup)
else:
- TEMPLATES[template] = template_adapter(template)
- if not TEMPLATES[template]:
- abort(500, 'Template not found')
+ TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup)
+ if not TEMPLATES[tpl]:
+ abort(500, 'Template (%s) not found' % tpl)
args['abort'] = abort
args['request'] = request
args['response'] = response
- return TEMPLATES[template].render(**args)
+ return TEMPLATES[tpl].render(**args)
+
+
+def mako_template(tpl_name, **kargs):
+ kargs['template_adapter'] = MakoTemplate
+ return template(tpl_name, **kargs)
+
+def cheetah_template(tpl_name, **kargs):
+ kargs['template_adapter'] = CheetahTemplate
+ return template(tpl_name, **kargs)
+
+def jinja2_template(tpl_name, **kargs):
+ kargs['template_adapter'] = Jinja2Template
+ return template(tpl_name, **kargs)
+def view(tpl_name, **defaults):
+ ''' Decorator: Rendes a template for a handler.
+ Return a dict of template vars to fill out the template.
+ '''
+ def decorator(func):
+ def wrapper(**kargs):
+ out = func(**kargs)
+ defaults.update(out)
+ return template(tpl_name, **defaults)
+ return wrapper
+ return decorator
+
+def mako_view(tpl_name, **kargs):
+ kargs['template_adapter'] = MakoTemplate
+ return view(tpl_name, **kargs)
+
+def cheetah_view(tpl_name, **kargs):
+ kargs['template_adapter'] = CheetahTemplate
+ return view(tpl_name, **kargs)
-def mako_template(template_name, **args): return template(template_name, template_adapter=MakoTemplate, **args)
+def jinja2_view(tpl_name, **kargs):
+ kargs['template_adapter'] = Jinja2Template
+ return view(tpl_name, **kargs)
-def cheetah_template(template_name, **args): return template(template_name, template_adapter=CheetahTemplate, **args)
@@ -698,8 +1008,8 @@ def cheetah_template(template_name, **args): return template(template_name, temp
# Database
-class BottleBucket(object):
- '''Memory-caching wrapper around anydbm'''
+class BottleBucket(object): # pragma: no cover
+ """ Memory-caching wrapper around anydbm """
def __init__(self, name):
self.__dict__['name'] = name
self.__dict__['db'] = dbm.open(DB_PATH + '/%s.db' % name, 'c')
@@ -711,6 +1021,7 @@ class BottleBucket(object):
return self.mmap[key]
def __setitem__(self, key, value):
+ if not isinstance(key, str): raise TypeError("Bottle keys must be strings")
self.mmap[key] = value
def __delitem__(self, key):
@@ -754,7 +1065,10 @@ class BottleBucket(object):
if key not in self.db or pvalue != self.db[key]:
self.db[key] = pvalue
self.mmap.clear()
- self.db.close()
+ if hasattr(self.db, 'sync'):
+ self.db.sync()
+ if hasattr(self.db, 'close'):
+ self.db.close()
def clear(self):
for key in self.db:
@@ -773,12 +1087,13 @@ class BottleBucket(object):
raise
-class BottleDB(threading.local):
- '''Holds multible BottleBucket instances in a thread-local way.'''
+class BottleDB(threading.local): # pragma: no cover
+ """ Holds multible BottleBucket instances in a thread-local way. """
def __init__(self):
self.__dict__['open'] = {}
def __getitem__(self, key):
+ warnings.warn("Please do not use bottle.db anymore. This feature is deprecated. You may use anydb directly.", DeprecationWarning)
if key not in self.open and not key.startswith('_'):
self.open[key] = BottleBucket(key)
return self.open[key]
@@ -829,8 +1144,9 @@ class BottleDB(threading.local):
# Modul initialization and configuration
DB_PATH = './'
-TEMPLATE_PATH = ['./%s.tpl', './views/%s.tpl']
+TEMPLATE_PATH = ['./', './views/']
TEMPLATES = {}
+DEBUG = False
HTTP_CODES = {
100: 'CONTINUE',
101: 'SWITCHING PROTOCOLS',
@@ -875,12 +1191,41 @@ HTTP_CODES = {
505: 'HTTP VERSION NOT SUPPORTED',
}
+HTTP_ERROR_TEMPLATE = """
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html>
+ <head>
+ <title>Error %(status)d: %(error_name)s</title>
+ </head>
+ <body>
+ <h1>Error %(status)d: %(error_name)s</h1>
+ <p>Sorry, the requested URL <tt>%(url)s</tt> caused an error:</p>
+ <pre>
+ %(error_message)s
+ </pre>
+ </body>
+</html>
+"""
+
+TRACEBACK_TEMPLATE = """
+<h2>Traceback:</h2>
+<pre>
+%s
+</pre>
+"""
+
request = Request()
response = Response()
db = BottleDB()
local = threading.local()
-def debug(mode=True): default_app().debug = bool(mode)
-def optimize(mode=True): default_app().optimize = bool(mode)
+#TODO: Global and app local configuration (debug, defaults, ...) is a mess
+
+def debug(mode=True):
+ global DEBUG
+ DEBUG = bool(mode)
+
+def optimize(mode=True):
+ default_app().optimize = bool(mode)