summaryrefslogtreecommitdiffstats
path: root/module
diff options
context:
space:
mode:
authorGravatar zoidberg10 <zoidberg@mujmail.cz> 2011-12-08 02:08:35 +0100
committerGravatar zoidberg10 <zoidberg@mujmail.cz> 2011-12-08 02:08:35 +0100
commit30ba647fe479d86c3d7bac71908ee56ec80eb928 (patch)
tree765c5e0295d81f712e070da774d591561848f34b /module
parenthttprequest: encode('utf_8') for unicode post (diff)
parentupdated bottle.py (diff)
downloadpyload-30ba647fe479d86c3d7bac71908ee56ec80eb928.tar.xz
Merge
Diffstat (limited to 'module')
-rw-r--r--module/Api.py3
-rw-r--r--module/common/pylgettext.py62
-rw-r--r--module/lib/bottle.py2026
-rw-r--r--module/remote/thriftbackend/pyload.thrift57
-rwxr-xr-xmodule/remote/thriftbackend/thriftgen/pyload/Pyload-remote58
-rw-r--r--module/remote/thriftbackend/thriftgen/pyload/Pyload.py646
-rw-r--r--module/remote/thriftbackend/thriftgen/pyload/constants.py2
-rw-r--r--module/remote/thriftbackend/thriftgen/pyload/ttypes.py108
-rw-r--r--module/setup.py165
-rw-r--r--module/web/pyload_app.py2
-rw-r--r--module/web/webinterface.py6
11 files changed, 1879 insertions, 1256 deletions
diff --git a/module/Api.py b/module/Api.py
index fc36c9fea..f0bf5e264 100644
--- a/module/Api.py
+++ b/module/Api.py
@@ -65,6 +65,7 @@ class PERMS:
DOWNLOAD = 64 # can download from webinterface
SETTINGS = 128 # can access settings
ACCOUNTS = 256 # can access accounts
+ LOGS = 512 # can see server logs
class ROLE:
ADMIN = 0 #admin has all permissions implicit
@@ -249,7 +250,7 @@ class Api(Iface):
"""Restart pyload core"""
self.core.do_restart = True
- @permission(PERMS.STATUS)
+ @permission(PERMS.LOGS)
def getLog(self, offset=0):
"""Returns most recent log entries.
diff --git a/module/common/pylgettext.py b/module/common/pylgettext.py
new file mode 100644
index 000000000..ae6d39325
--- /dev/null
+++ b/module/common/pylgettext.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from gettext import *
+
+_searchdirs = None
+
+origfind = find
+
+def setpaths(pathlist):
+ global _searchdirs
+ if isinstance(pathlist, list):
+ _searchdirs = pathlist
+ else:
+ _searchdirs = list(pathlist)
+
+
+def addpath(path):
+ global _searchdirs
+ if _searchdirs is None:
+ _searchdirs = list(path)
+ else:
+ if path not in _searchdirs:
+ _searchdirs.append(path)
+
+
+def delpath(path):
+ global _searchdirs
+ if _searchdirs is not None:
+ if path in _searchdirs:
+ _searchdirs.remove(path)
+
+
+def clearpath():
+ global _searchdirs
+ if _searchdirs is not None:
+ _searchdirs = None
+
+
+def find(domain, localedir=None, languages=None, all=False):
+ if _searchdirs is None:
+ return origfind(domain, localedir, languages, all)
+ searches = [localedir] + _searchdirs
+ results = list()
+ for dir in searches:
+ res = origfind(domain, dir, languages, all)
+ if all is False:
+ results.append(res)
+ else:
+ results.extend(res)
+ if all is False:
+ results = filter(lambda x: x is not None, results)
+ if len(results) == 0:
+ return None
+ else:
+ return results[0]
+ else:
+ return results
+
+#Is there a smarter/cleaner pythonic way for this?
+translation.__globals__['find'] = find
+
diff --git a/module/lib/bottle.py b/module/lib/bottle.py
index f449e182c..f8624bf13 100644
--- a/module/lib/bottle.py
+++ b/module/lib/bottle.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Bottle is a fast and simple micro-framework for small web applications. It
@@ -15,9 +16,26 @@ License: MIT (see LICENSE.txt for details)
from __future__ import with_statement
__author__ = 'Marcel Hellkamp'
-__version__ = '0.9.1'
+__version__ = '0.10.2'
__license__ = 'MIT'
+# The gevent server adapter needs to patch some modules before they are imported
+# This is why we parse the commandline parameters here but handle them later
+if __name__ == '__main__':
+ from optparse import OptionParser
+ _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app")
+ _opt = _cmd_parser.add_option
+ _opt("--version", action="store_true", help="show version number.")
+ _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
+ _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
+ _opt("-p", "--plugin", action="append", help="install additional plugin/s.")
+ _opt("--debug", action="store_true", help="start server in debug mode.")
+ _opt("--reload", action="store_true", help="auto-reload on file changes.")
+ _cmd_options, _cmd_args = _cmd_parser.parse_args()
+ if _cmd_options.server and _cmd_options.server.startswith('gevent'):
+ import gevent.monkey; gevent.monkey.patch_all()
+
+import sys
import base64
import cgi
import email.utils
@@ -30,7 +48,6 @@ import mimetypes
import os
import re
import subprocess
-import sys
import tempfile
import thread
import threading
@@ -38,10 +55,16 @@ import time
import warnings
from Cookie import SimpleCookie
+from datetime import date as datedate, datetime, timedelta
from tempfile import TemporaryFile
-from traceback import format_exc
-from urllib import urlencode, quote as urlquote, unquote as urlunquote
-from urlparse import urlunsplit, urljoin, SplitResult as UrlSplitResult
+from traceback import format_exc, print_exc
+from urlparse import urljoin, SplitResult as UrlSplitResult
+
+# Workaround for a bug in some versions of lib2to3 (fixed on CPython 2.7 and 3.2)
+import urllib
+urlencode = urllib.urlencode
+urlquote = urllib.quote
+urlunquote = urllib.unquote
try: from collections import MutableMapping as DictMixin
except ImportError: # pragma: no cover
@@ -55,16 +78,25 @@ try: import cPickle as pickle
except ImportError: # pragma: no cover
import pickle
-try: from json import dumps as json_dumps
+try: from json import dumps as json_dumps, loads as json_lds
except ImportError: # pragma: no cover
- try: from simplejson import dumps as json_dumps
+ try: from simplejson import dumps as json_dumps, loads as json_lds
except ImportError: # pragma: no cover
- try: from django.utils.simplejson import dumps as json_dumps
+ try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds
except ImportError: # pragma: no cover
- json_dumps = None
+ def json_dumps(data):
+ raise ImportError("JSON support requires Python 2.6 or simplejson.")
+ json_lds = json_dumps
+py3k = sys.version_info >= (3,0,0)
NCTextIOWrapper = None
-if sys.version_info >= (3,0,0): # pragma: no cover
+
+if sys.version_info < (2,6,0):
+ msg = "Python 2.5 support may be dropped in future versions of Bottle."
+ warnings.warn(msg, DeprecationWarning)
+
+if py3k: # pragma: no cover
+ json_loads = lambda s: json_lds(touni(s))
# See Request.POST
from io import BytesIO
def touni(x, enc='utf8', err='strict'):
@@ -77,6 +109,7 @@ if sys.version_info >= (3,0,0): # pragma: no cover
the wrapped buffer. This subclass keeps it open. '''
def close(self): pass
else:
+ json_loads = json_lds
from StringIO import StringIO as BytesIO
bytes = str
def touni(x, enc='utf8', err='strict'):
@@ -87,17 +120,16 @@ def tob(data, enc='utf8'):
""" Convert anything to bytes """
return data.encode(enc) if isinstance(data, unicode) else bytes(data)
-# Convert strings and unicode to native strings
-if sys.version_info >= (3,0,0):
- tonat = touni
-else:
- tonat = tob
+tonat = touni if py3k else tob
tonat.__doc__ = """ Convert anything to native strings """
+def try_update_wrapper(wrapper, wrapped, *a, **ka):
+ try: # Bug: functools breaks if wrapper is an instane method
+ functools.update_wrapper(wrapper, wrapped, *a, **ka)
+ except AttributeError: pass
# Backward compatibility
-def depr(message, critical=False):
- if critical: raise DeprecationWarning(message)
+def depr(message):
warnings.warn(message, DeprecationWarning, stacklevel=3)
@@ -119,7 +151,7 @@ class DictProperty(object):
return self
def __get__(self, obj, cls):
- if not obj: return self
+ if obj is None: return self
key, storage = self.key, getattr(obj, self.attr)
if key not in storage: storage[key] = self.getter(obj)
return storage[key]
@@ -132,10 +164,22 @@ class DictProperty(object):
if self.read_only: raise AttributeError("Read-Only property.")
del getattr(obj, self.attr)[self.key]
-def cached_property(func):
- ''' A property that, if accessed, replaces itself with the computed
- value. Subsequent accesses won't call the getter again. '''
- return DictProperty('__dict__')(func)
+
+class CachedProperty(object):
+ ''' A property that is only computed once per instance and then replaces
+ itself with an ordinary attribute. Deleting the attribute resets the
+ property. '''
+
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, obj, cls):
+ if obj is None: return self
+ value = obj.__dict__[self.func.__name__] = self.func(obj)
+ return value
+
+cached_property = CachedProperty
+
class lazy_attribute(object): # Does not need configuration -> lower-case name
''' A property that caches itself to the class object. '''
@@ -163,6 +207,8 @@ class BottleException(Exception):
pass
+#TODO: These should subclass BaseRequest
+
class HTTPResponse(BottleException):
""" Used to break execution and immediately finish the response """
def __init__(self, output='', status=200, header=None):
@@ -207,15 +253,14 @@ class RouteReset(BottleException):
""" If raised by a plugin or request handler, the route is reset and all
plugins are re-applied. """
+class RouterUnknownModeError(RouteError): pass
class RouteSyntaxError(RouteError):
""" The route parser found something not supported by this router """
-
class RouteBuildError(RouteError):
""" The route could not been built """
-
class Router(object):
''' A Router is an ordered collection of route->target pairs. It is used to
efficiently match WSGI requests against a number of routes and return
@@ -224,83 +269,153 @@ class Router(object):
and a HTTP method.
The path-rule is either a static path (e.g. `/contact`) or a dynamic
- path that contains wildcards (e.g. `/wiki/:page`). By default, wildcards
- consume characters up to the next slash (`/`). To change that, you may
- add a regular expression pattern (e.g. `/wiki/:page#[a-z]+#`).
-
- For performance reasons, static routes (rules without wildcards) are
- checked first. Dynamic routes are searched in order. Try to avoid
- ambiguous or overlapping rules.
-
- The HTTP method string matches only on equality, with two exceptions:
- * ´GET´ routes also match ´HEAD´ requests if there is no appropriate
- ´HEAD´ route installed.
- * ´ANY´ routes do match if there is no other suitable route installed.
-
- An optional ``name`` parameter is used by :meth:`build` to identify
- routes.
+ path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
+ and details on the matching order are described in docs:`routing`.
'''
- default = '[^/]+'
-
- @lazy_attribute
- def syntax(cls):
- return re.compile(r'(?<!\\):([a-zA-Z_][a-zA-Z_0-9]*)?(?:#(.*?)#)?')
+ default_pattern = '[^/]+'
+ default_filter = 're'
+ #: Sorry for the mess. It works. Trust me.
+ rule_syntax = re.compile('(\\\\*)'\
+ '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\
+ '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\
+ '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))')
+
+ def __init__(self, strict=False):
+ self.rules = {} # A {rule: Rule} mapping
+ self.builder = {} # A rule/name->build_info mapping
+ self.static = {} # Cache for static routes: {path: {method: target}}
+ self.dynamic = [] # Cache for dynamic routes. See _compile()
+ #: If true, static routes are no longer checked first.
+ self.strict_order = strict
+ self.filters = {'re': self.re_filter, 'int': self.int_filter,
+ 'float': self.re_filter, 'path': self.path_filter}
+
+ def re_filter(self, conf):
+ return conf or self.default_pattern, None, None
+
+ def int_filter(self, conf):
+ return r'-?\d+', int, lambda x: str(int(x))
+
+ def float_filter(self, conf):
+ return r'-?\d*\.\d+', float, lambda x: str(float(x))
+
+ def path_filter(self, conf):
+ return r'.*?', None, None
+
+ def add_filter(self, name, func):
+ ''' Add a filter. The provided function is called with the configuration
+ string as parameter and must return a (regexp, to_python, to_url) tuple.
+ The first element is a string, the last two are callables or None. '''
+ self.filters[name] = func
+
+ def parse_rule(self, rule):
+ ''' Parses a rule into a (name, filter, conf) token stream. If mode is
+ None, name contains a static rule part. '''
+ offset, prefix = 0, ''
+ for match in self.rule_syntax.finditer(rule):
+ prefix += rule[offset:match.start()]
+ g = match.groups()
+ if len(g[0])%2: # Escaped wildcard
+ prefix += match.group(0)[len(g[0]):]
+ offset = match.end()
+ continue
+ if prefix: yield prefix, None, None
+ name, filtr, conf = g[1:4] if not g[2] is None else g[4:7]
+ if not filtr: filtr = self.default_filter
+ yield name, filtr, conf or None
+ offset, prefix = match.end(), ''
+ if offset <= len(rule) or prefix:
+ yield prefix+rule[offset:], None, None
+
+ def add(self, rule, method, target, name=None):
+ ''' Add a new route or replace the target for an existing route. '''
+ if rule in self.rules:
+ self.rules[rule][method] = target
+ if name: self.builder[name] = self.builder[rule]
+ return
+
+ target = self.rules[rule] = {method: target}
+
+ # Build pattern and other structures for dynamic routes
+ anons = 0 # Number of anonymous wildcards
+ pattern = '' # Regular expression pattern
+ filters = [] # Lists of wildcard input filters
+ builder = [] # Data structure for the URL builder
+ is_static = True
+ for key, mode, conf in self.parse_rule(rule):
+ if mode:
+ is_static = False
+ mask, in_filter, out_filter = self.filters[mode](conf)
+ if key:
+ pattern += '(?P<%s>%s)' % (key, mask)
+ else:
+ pattern += '(?:%s)' % mask
+ key = 'anon%d' % anons; anons += 1
+ if in_filter: filters.append((key, in_filter))
+ builder.append((key, out_filter or str))
+ elif key:
+ pattern += re.escape(key)
+ builder.append((None, key))
+ self.builder[rule] = builder
+ if name: self.builder[name] = builder
+
+ if is_static and not self.strict_order:
+ self.static[self.build(rule)] = target
+ return
- def __init__(self):
- self.routes = {} # A {rule: {method: target}} mapping
- self.rules = [] # An ordered list of rules
- self.named = {} # A name->(rule, build_info) mapping
- self.static = {} # Cache for static routes: {path: {method: target}}
- self.dynamic = [] # Cache for dynamic routes. See _compile()
+ def fpat_sub(m):
+ return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
+ flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern)
- def add(self, rule, method, target, name=None, static=False):
- ''' Add a new route or replace the target for an existing route. '''
- if static:
- depr("Use a backslash to escape ':' in routes.") # 0.9
- rule = rule.replace(':','\\:')
+ try:
+ re_match = re.compile('^(%s)$' % pattern).match
+ except re.error, e:
+ raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
+
+ def match(path):
+ """ Return an url-argument dictionary. """
+ url_args = re_match(path).groupdict()
+ for name, wildcard_filter in filters:
+ try:
+ url_args[name] = wildcard_filter(url_args[name])
+ except ValueError:
+ raise HTTPError(400, 'Path has wrong format.')
+ return url_args
- if rule in self.routes:
- self.routes[rule][method.upper()] = target
- else:
- self.routes[rule] = {method.upper(): target}
- self.rules.append(rule)
- if self.static or self.dynamic: # Clear precompiler cache.
- self.static, self.dynamic = {}, {}
- if name:
- self.named[name] = (rule, None)
-
- def build(self, _name, *anon, **args):
- ''' Return a string that matches a named route. Use keyword arguments
- to fill out named wildcards. Remaining arguments are appended as a
- query string. Raises RouteBuildError or KeyError.'''
- if _name not in self.named:
- raise RouteBuildError("No route with that name.", _name)
- rule, pairs = self.named[_name]
- if not pairs:
- token = self.syntax.split(rule)
- parts = [p.replace('\\:',':') for p in token[::3]]
- names = token[1::3]
- if len(parts) > len(names): names.append(None)
- pairs = zip(parts, names)
- self.named[_name] = (rule, pairs)
try:
- anon = list(anon)
- url = [s if k is None
- else s+str(args.pop(k)) if k else s+str(anon.pop())
- for s, k in pairs]
- except IndexError:
- msg = "Not enough arguments to fill out anonymous wildcards."
- raise RouteBuildError(msg)
+ combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern)
+ self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
+ self.dynamic[-1][1].append((match, target))
+ except (AssertionError, IndexError), e: # AssertionError: Too many groups
+ self.dynamic.append((re.compile('(^%s$)' % flat_pattern),
+ [(match, target)]))
+ return match
+
+ def build(self, _name, *anons, **query):
+ ''' Build an URL by filling the wildcards in a rule. '''
+ builder = self.builder.get(_name)
+ if not builder: raise RouteBuildError("No route with that name.", _name)
+ try:
+ for i, value in enumerate(anons): query['anon%d'%i] = value
+ url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder])
+ return url if not query else url+'?'+urlencode(query)
except KeyError, e:
- raise RouteBuildError(*e.args)
-
- if args: url += ['?', urlencode(args)]
- return ''.join(url)
+ raise RouteBuildError('Missing URL argument: %r' % e.args[0])
def match(self, environ):
- ''' Return a (target, url_agrs) tuple or raise HTTPError(404/405). '''
- targets, urlargs = self._match_path(environ)
+ ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
+ path, targets, urlargs = environ['PATH_INFO'] or '/', None, {}
+ if path in self.static:
+ targets = self.static[path]
+ else:
+ for combined, rules in self.dynamic:
+ match = combined.match(path)
+ if not match: continue
+ getargs, targets = rules[match.lastindex - 1]
+ urlargs = getargs(path) if getargs else {}
+ break
+
if not targets:
raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))
method = environ['REQUEST_METHOD'].upper()
@@ -316,66 +431,90 @@ class Router(object):
raise HTTPError(405, "Method not allowed.",
header=[('Allow',",".join(allowed))])
- def _match_path(self, environ):
- ''' Optimized PATH_INFO matcher. '''
- path = environ['PATH_INFO'] or '/'
- # Assume we are in a warm state. Search compiled rules first.
- match = self.static.get(path)
- if match: return match, {}
- for combined, rules in self.dynamic:
- match = combined.match(path)
- if not match: continue
- gpat, match = rules[match.lastindex - 1]
- return match, gpat.match(path).groupdict() if gpat else {}
- # Lazy-check if we are really in a warm state. If yes, stop here.
- if self.static or self.dynamic or not self.routes: return None, {}
- # Cold state: We have not compiled any rules yet. Do so and try again.
- if not environ.get('wsgi.run_once'):
- self._compile()
- return self._match_path(environ)
- # For run_once (CGI) environments, don't compile. Just check one by one.
- epath = path.replace(':','\\:') # Turn path into its own static rule.
- match = self.routes.get(epath) # This returns static rule only.
- if match: return match, {}
- for rule in self.rules:
- #: Skip static routes to reduce re.compile() calls.
- if rule.count(':') < rule.count('\\:'): continue
- match = self._compile_pattern(rule).match(path)
- if match: return self.routes[rule], match.groupdict()
- return None, {}
-
- def _compile(self):
- ''' Prepare static and dynamic search structures. '''
- self.static = {}
- self.dynamic = []
- def fpat_sub(m):
- return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
- for rule in self.rules:
- target = self.routes[rule]
- if not self.syntax.search(rule):
- self.static[rule.replace('\\:',':')] = target
- continue
- gpat = self._compile_pattern(rule)
- fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern)
- gpat = gpat if gpat.groupindex else None
+
+
+class Route(object):
+ ''' This class wraps a route callback along with route specific metadata and
+ configuration and applies Plugins on demand. It is also responsible for
+ turing an URL path rule into a regular expression usable by the Router.
+ '''
+
+
+ def __init__(self, app, rule, method, callback, name=None,
+ plugins=None, skiplist=None, **config):
+ #: The application this route is installed to.
+ self.app = app
+ #: The path-rule string (e.g. ``/wiki/:page``).
+ self.rule = rule
+ #: The HTTP method as a string (e.g. ``GET``).
+ self.method = method
+ #: The original callback with no plugins applied. Useful for introspection.
+ self.callback = callback
+ #: The name of the route (if specified) or ``None``.
+ self.name = name or None
+ #: A list of route-specific plugins (see :meth:`Bottle.route`).
+ self.plugins = plugins or []
+ #: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
+ self.skiplist = skiplist or []
+ #: Additional keyword arguments passed to the :meth:`Bottle.route`
+ #: decorator are stored in this dictionary. Used for route-specific
+ #: plugin configuration and meta-data.
+ self.config = ConfigDict(config)
+
+ def __call__(self, *a, **ka):
+ depr("Some APIs changed to return Route() instances instead of"\
+ " callables. Make sure to use the Route.call method and not to"\
+ " call Route instances directly.")
+ return self.call(*a, **ka)
+
+ @cached_property
+ def call(self):
+ ''' The route callback with all plugins applied. This property is
+ created on demand and then cached to speed up subsequent requests.'''
+ return self._make_callback()
+
+ def reset(self):
+ ''' Forget any cached values. The next time :attr:`call` is accessed,
+ all plugins are re-applied. '''
+ self.__dict__.pop('call', None)
+
+ def prepare(self):
+ ''' Do all on-demand work immediately (useful for debugging).'''
+ self.call
+
+ @property
+ def _context(self):
+ depr('Switch to Plugin API v2 and access the Route object directly.')
+ return dict(rule=self.rule, method=self.method, callback=self.callback,
+ name=self.name, app=self.app, config=self.config,
+ apply=self.plugins, skip=self.skiplist)
+
+ def all_plugins(self):
+ ''' Yield all Plugins affecting this route. '''
+ unique = set()
+ for p in reversed(self.app.plugins + self.plugins):
+ if True in self.skiplist: break
+ name = getattr(p, 'name', False)
+ if name and (name in self.skiplist or name in unique): continue
+ if p in self.skiplist or type(p) in self.skiplist: continue
+ if name: unique.add(name)
+ yield p
+
+ def _make_callback(self):
+ callback = self.callback
+ for plugin in self.all_plugins():
try:
- combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat)
- self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
- self.dynamic[-1][1].append((gpat, target))
- except (AssertionError, IndexError), e: # AssertionError: Too many groups
- self.dynamic.append((re.compile('(^%s$)'%fpat),
- [(gpat, target)]))
- except re.error, e:
- raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
-
- def _compile_pattern(self, rule):
- ''' Return a regular expression with named groups for each wildcard. '''
- out = ''
- for i, part in enumerate(self.syntax.split(rule)):
- if i%3 == 0: out += re.escape(part.replace('\\:',':'))
- elif i%3 == 1: out += '(?P<%s>' % part if part else '(?:'
- else: out += '%s)' % (part or '[^/]+')
- return re.compile('^%s$'%out)
+ if hasattr(plugin, 'apply'):
+ api = getattr(plugin, 'api', 1)
+ context = self if api > 1 else self._context
+ callback = plugin.apply(callback, context)
+ else:
+ callback = plugin(callback)
+ except RouteReset: # Try again with changed configuration.
+ return self._make_callback()
+ if not callback is self.callback:
+ try_update_wrapper(callback, self.callback)
+ return callback
@@ -394,61 +533,62 @@ class Bottle(object):
""" Create a new bottle instance.
You usually don't do that. Use `bottle.app.push()` instead.
"""
- self.routes = [] # List of installed routes including metadata.
- self.router = Router() # Maps requests to self.route indices.
- self.ccache = {} # Cache for callbacks with plugins applied.
-
+ self.routes = [] # List of installed :class:`Route` instances.
+ self.router = Router() # Maps requests to :class:`Route` instances.
self.plugins = [] # List of installed plugins.
- self.mounts = {}
self.error_handler = {}
#: If true, most exceptions are catched and returned as :exc:`HTTPError`
+ self.config = ConfigDict(config or {})
self.catchall = catchall
- self.config = config or {}
- self.serve = True
- # Default plugins
- self.hooks = self.install(HooksPlugin())
- self.typefilter = self.install(TypeFilterPlugin())
+ #: An instance of :class:`HooksPlugin`. Empty by default.
+ self.hooks = HooksPlugin()
+ self.install(self.hooks)
if autojson:
self.install(JSONPlugin())
self.install(TemplatePlugin())
- def optimize(self, *a, **ka):
- depr("Bottle.optimize() is obsolete.")
+ def mount(self, prefix, app, **options):
+ ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific
+ URL prefix. Example::
- def mount(self, app, prefix, **options):
- ''' Mount an application to a specific URL prefix. The prefix is added
- to SCIPT_PATH and removed from PATH_INFO before the sub-application
- is called.
+ root_app.mount('/admin/', admin_app)
- :param app: an instance of :class:`Bottle`.
- :param prefix: path prefix used as a mount-point.
+ :param prefix: path prefix or `mount-point`. If it ends in a slash,
+ that slash is mandatory.
+ :param app: an instance of :class:`Bottle` or a WSGI application.
All other parameters are passed to the underlying :meth:`route` call.
'''
- if not isinstance(app, Bottle):
- raise TypeError('Only Bottle instances are supported for now.')
- prefix = '/'.join(filter(None, prefix.split('/')))
- if not prefix:
- raise TypeError('Empty prefix. Perhaps you want a merge()?')
- for other in self.mounts:
- if other.startswith(prefix):
- raise TypeError('Conflict with existing mount: %s' % other)
- path_depth = prefix.count('/') + 1
- options.setdefault('method', 'ANY')
+ if isinstance(app, basestring):
+ prefix, app = app, prefix
+ depr('Parameter order of Bottle.mount() changed.') # 0.10
+
+ parts = filter(None, prefix.split('/'))
+ if not parts: raise ValueError('Empty path prefix.')
+ path_depth = len(parts)
options.setdefault('skip', True)
- self.mounts[prefix] = app
- @self.route('/%s/:#.*#' % prefix, **options)
- def mountpoint():
- request.path_shift(path_depth)
- return app.handle(request.environ)
+ options.setdefault('method', 'ANY')
- def add_filter(self, ftype, func):
- depr("Filters are deprecated and can be replaced with plugins.") #0.9
- self.typefilter.add(ftype, func)
+ @self.route('/%s/:#.*#' % '/'.join(parts), **options)
+ def mountpoint():
+ try:
+ request.path_shift(path_depth)
+ rs = BaseResponse([], 200)
+ def start_response(status, header):
+ rs.status = status
+ for name, value in header: rs.add_header(name, value)
+ return rs.body.append
+ rs.body = itertools.chain(rs.body, app(request.environ, start_response))
+ return HTTPResponse(rs.body, rs.status, rs.headers)
+ finally:
+ request.path_shift(-path_depth)
+
+ if not prefix.endswith('/'):
+ self.route('/' + '/'.join(parts), callback=mountpoint, **options)
def install(self, plugin):
- ''' Add a plugin to the list of plugins and prepare it for beeing
+ ''' Add a plugin to the list of plugins and prepare it for being
applied to all routes of this application. A plugin may be a simple
decorator or an object that implements the :class:`Plugin` API.
'''
@@ -460,11 +600,10 @@ class Bottle(object):
return plugin
def uninstall(self, plugin):
- ''' Uninstall plugins. Pass an instance to remove a specific plugin.
- Pass a type object to remove all plugins that match that type.
- Subclasses are not removed. Pass a string to remove all plugins with
- a matching ``name`` attribute. Pass ``True`` to remove all plugins.
- The list of affected plugins is returned. '''
+ ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type
+ object to remove all plugins that match that type, a string to remove
+ all plugins with a matching ``name`` attribute or ``True`` to remove all
+ plugins. Return the list of removed plugins. '''
removed, remove = [], plugin
for i, plugin in list(enumerate(self.plugins))[::-1]:
if remove is True or remove is plugin or remove is type(plugin) \
@@ -475,15 +614,17 @@ class Bottle(object):
if removed: self.reset()
return removed
- def reset(self, id=None):
+ def reset(self, route=None):
''' Reset all routes (force plugins to be re-applied) and clear all
- caches. If an ID is given, only that specific route is affected. '''
- if id is None: self.ccache.clear()
- else: self.ccache.pop(id, None)
+ caches. If an ID or route object is given, only that specific route
+ is affected. '''
+ if route is None: routes = self.routes
+ elif isinstance(route, Route): routes = [route]
+ else: routes = [self.routes[route]]
+ for route in routes: route.reset()
if DEBUG:
- for route in self.routes:
- if route['id'] not in self.ccache:
- self.ccache[route['id']] = self._build_callback(route)
+ for route in routes: route.prepare()
+ self.hooks.trigger('app_reset')
def close(self):
''' Close the application and all installed plugins. '''
@@ -492,45 +633,10 @@ class Bottle(object):
self.stopped = True
def match(self, environ):
- """ (deprecated) Search for a matching route and return a
- (callback, urlargs) tuple.
- The first element is the associated route callback with plugins
- applied. The second value is a dictionary with parameters extracted
- from the URL. The :class:`Router` raises :exc:`HTTPError` (404/405)
- on a non-match."""
- depr("This method will change semantics in 0.10.")
- return self._match(environ)
-
- def _match(self, environ):
- handle, args = self.router.match(environ)
- environ['route.handle'] = handle # TODO move to router?
- environ['route.url_args'] = args
- try:
- return self.ccache[handle], args
- except KeyError:
- config = self.routes[handle]
- callback = self.ccache[handle] = self._build_callback(config)
- return callback, args
-
- def _build_callback(self, config):
- ''' Apply plugins to a route and return a new callable. '''
- wrapped = config['callback']
- plugins = self.plugins + config['apply']
- skip = config['skip']
- try:
- for plugin in reversed(plugins):
- if True in skip: break
- if plugin in skip or type(plugin) in skip: continue
- if getattr(plugin, 'name', True) in skip: continue
- if hasattr(plugin, 'apply'):
- wrapped = plugin.apply(wrapped, config)
- else:
- wrapped = plugin(wrapped)
- if not wrapped: break
- functools.update_wrapper(wrapped, config['callback'])
- return wrapped
- except RouteReset: # A plugin may have changed the config dict inplace.
- return self._build_callback(config) # Apply all plugins again.
+ """ Search for a matching route and return a (:class:`Route` , urlargs)
+ tuple. The second value is a dictionary with parameters extracted
+ from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
+ return self.router.match(environ)
def get_url(self, routename, **kargs):
""" Return a string that matches a named route """
@@ -566,31 +672,20 @@ class Bottle(object):
configuration and passed to plugins (see :meth:`Plugin.apply`).
"""
if callable(path): path, callback = None, path
-
plugins = makelist(apply)
skiplist = makelist(skip)
- if 'decorate' in config:
- depr("The 'decorate' parameter was renamed to 'apply'") # 0.9
- plugins += makelist(config.pop('decorate'))
- if config.pop('no_hooks', False):
- depr("The no_hooks parameter is no longer used. Add 'hooks' to the"\
- " list of skipped plugins instead.") # 0.9
- skiplist.append('hooks')
- static = config.get('static', False) # depr 0.9
-
def decorator(callback):
+ # TODO: Documentation and tests
+ if isinstance(callback, basestring): callback = load(callback)
for rule in makelist(path) or yieldroutes(callback):
for verb in makelist(method):
verb = verb.upper()
- cfg = dict(rule=rule, method=verb, callback=callback,
- name=name, app=self, config=config,
- apply=plugins, skip=skiplist)
- self.routes.append(cfg)
- cfg['id'] = self.routes.index(cfg)
- self.router.add(rule, verb, cfg['id'], name=name, static=static)
- if DEBUG: self.ccache[cfg['id']] = self._build_callback(cfg)
+ route = Route(self, rule, verb, callback, name=name,
+ plugins=plugins, skiplist=skiplist, **config)
+ self.routes.append(route)
+ self.router.add(rule, verb, route, name=name)
+ if DEBUG: route.prepare()
return callback
-
return decorator(callback) if callback else decorator
def get(self, path=None, method='GET', **options):
@@ -623,14 +718,6 @@ class Bottle(object):
return func
return wrapper
- def add_hook(self, name, func):
- depr("Call Bottle.hooks.add() instead.") #0.9
- self.hooks.add(name, func)
-
- def remove_hook(self, name, func):
- depr("Call Bottle.hooks.remove() instead.") #0.9
- self.hooks.remove(name, func)
-
def handle(self, path, method='GET'):
""" (deprecated) Execute the first matching route callback and return
the result. :exc:`HTTPResponse` exceptions are catched and returned.
@@ -641,24 +728,25 @@ class Bottle(object):
if isinstance(path, dict):
return self._handle(path)
return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()})
-
+
def _handle(self, environ):
- if not self.serve:
- depr("Bottle.serve will be removed in 0.10.")
- return HTTPError(503, "Server stopped")
try:
- callback, args = self._match(environ)
- return callback(**args)
+ route, args = self.router.match(environ)
+ environ['route.handle'] = environ['bottle.route'] = route
+ environ['route.url_args'] = args
+ return route.call(**args)
except HTTPResponse, r:
return r
- except RouteReset: # Route reset requested by the callback or a plugin.
- del self.ccache[handle]
- return self.handle(environ) # Try again.
+ except RouteReset:
+ route.reset()
+ return self._handle(environ)
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
except Exception, e:
if not self.catchall: raise
- return HTTPError(500, "Internal Server Error", e, format_exc(10))
+ stacktrace = format_exc(10)
+ environ['wsgi.errors'].write(stacktrace)
+ return HTTPError(500, "Internal Server Error", e, stacktrace)
def _cast(self, out, request, response, peek=None):
""" Try to convert the parameter into something WSGI compatible and set
@@ -669,7 +757,7 @@ class Bottle(object):
# Empty output is done here
if not out:
- response.headers['Content-Length'] = 0
+ response['Content-Length'] = 0
return []
# Join lists of byte or unicode strings. Mixed lists are NOT supported
if isinstance(out, (tuple, list))\
@@ -680,9 +768,10 @@ class Bottle(object):
out = out.encode(response.charset)
# Byte Strings are just returned
if isinstance(out, bytes):
- response.headers['Content-Length'] = str(len(out))
+ response['Content-Length'] = len(out)
return [out]
# HTTPError or HTTPException (recursive, because they may wrap anything)
+ # TODO: Handle these explicitly in handle() or make them iterable.
if isinstance(out, HTTPError):
out.apply(response)
out = self.error_handler.get(out.status, repr)(out)
@@ -732,14 +821,13 @@ class Bottle(object):
environ['bottle.app'] = self
request.bind(environ)
response.bind()
- out = self._handle(environ)
- out = self._cast(out, request, response)
+ out = self._cast(self._handle(environ), request, response)
# rfc2616 section 4.3
- if response.status in (100, 101, 204, 304) or request.method == 'HEAD':
+ if response._status_code in (100, 101, 204, 304)\
+ or request.method == 'HEAD':
if hasattr(out, 'close'): out.close()
out = []
- status = '%d %s' % (response.status, HTTP_CODES[response.status])
- start_response(status, response.headerlist)
+ start_response(response._status_line, list(response.iter_headers()))
return out
except (KeyboardInterrupt, SystemExit, MemoryError):
raise
@@ -755,6 +843,7 @@ class Bottle(object):
return [tob(err)]
def __call__(self, environ, start_response):
+ ''' Each instance of :class:'Bottle' is a WSGI application. '''
return self.wsgi(environ, start_response)
@@ -767,196 +856,136 @@ class Bottle(object):
###############################################################################
-class Request(threading.local, DictMixin):
- """ Represents a single HTTP request using thread-local attributes.
- The Request object wraps a WSGI environment and can be used as such.
- """
- def __init__(self, environ=None):
- """ Create a new Request instance.
-
- You usually don't do this but use the global `bottle.request`
- instance instead.
- """
- self.bind(environ or {},)
+class BaseRequest(DictMixin):
+ """ A wrapper for WSGI environment dictionaries that adds a lot of
+ convenient access methods and properties. Most of them are read-only."""
- def bind(self, environ):
- """ Bind a new WSGI environment.
+ #: Maximum size of memory buffer for :attr:`body` in bytes.
+ MEMFILE_MAX = 102400
- This is done automatically for the global `bottle.request`
- instance on every request.
- """
+ def __init__(self, environ):
+ """ Wrap a WSGI environ dictionary. """
+ #: The wrapped WSGI environ dictionary. This is the only real attribute.
+ #: All other attributes actually are read-only properties.
self.environ = environ
- # These attributes are used anyway, so it is ok to compute them here
- self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/')
- self.method = environ.get('REQUEST_METHOD', 'GET').upper()
-
- @property
- def _environ(self):
- depr("Request._environ renamed to Request.environ")
- return self.environ
-
- def copy(self):
- ''' Returns a copy of self '''
- return Request(self.environ.copy())
-
- def path_shift(self, shift=1):
- ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
-
- :param shift: The number of path fragments to shift. May be negative
- to change the shift direction. (default: 1)
- '''
- script_name = self.environ.get('SCRIPT_NAME','/')
- self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift)
- self['PATH_INFO'] = self.path
-
- def __getitem__(self, key): return self.environ[key]
- def __delitem__(self, key): self[key] = ""; del(self.environ[key])
- def __iter__(self): return iter(self.environ)
- def __len__(self): return len(self.environ)
- def keys(self): return self.environ.keys()
- def __setitem__(self, key, value):
- """ Shortcut for Request.environ.__setitem__ """
- self.environ[key] = value
- todelete = []
- if key in ('PATH_INFO','REQUEST_METHOD'):
- self.bind(self.environ)
- elif key == 'wsgi.input': todelete = ('body','forms','files','params')
- elif key == 'QUERY_STRING': todelete = ('get','params')
- elif key.startswith('HTTP_'): todelete = ('headers', 'cookies')
- for key in todelete:
- if 'bottle.' + key in self.environ:
- del self.environ['bottle.' + key]
-
- @DictProperty('environ', 'bottle.urlparts', read_only=True)
- def urlparts(self):
- ''' Return a :class:`urlparse.SplitResult` tuple that can be used
- to reconstruct the full URL as requested by the client.
- The tuple contains: (scheme, host, path, query_string, fragment).
- The fragment is always empty because it is not visible to the server.
- '''
- env = self.environ
- host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST', '')
- http = env.get('wsgi.url_scheme', 'http')
- port = env.get('SERVER_PORT')
- if ':' in host: # Overrule SERVER_POST (proxy support)
- host, port = host.rsplit(':', 1)
- if not host or host == '127.0.0.1':
- host = env.get('SERVER_NAME', host)
- if port and http+port not in ('http80', 'https443'):
- host += ':' + port
- spath = self.environ.get('SCRIPT_NAME','').rstrip('/') + '/'
- rpath = self.path.lstrip('/')
- path = urlquote(urljoin(spath, rpath))
- return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
-
- @property
- def url(self):
- """ Full URL as requested by the client. """
- return self.urlparts.geturl()
-
- @property
- def fullpath(self):
- """ Request path including SCRIPT_NAME (if present). """
- return urlunquote(self.urlparts[2])
-
- @property
- def query_string(self):
- """ The part of the URL following the '?'. """
- return self.environ.get('QUERY_STRING', '')
+ environ['bottle.request'] = self
@property
- def content_length(self):
- """ Content-Length header as an integer, -1 if not specified """
- return int(self.environ.get('CONTENT_LENGTH', '') or -1)
+ def path(self):
+ ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix
+ broken clients and avoid the "empty path" edge case). '''
+ return '/' + self.environ.get('PATH_INFO','').lstrip('/')
@property
- def header(self):
- depr("The Request.header property was renamed to Request.headers")
- return self.headers
+ def method(self):
+ ''' The ``REQUEST_METHOD`` value as an uppercase string. '''
+ return self.environ.get('REQUEST_METHOD', 'GET').upper()
- @DictProperty('environ', 'bottle.headers', read_only=True)
+ @DictProperty('environ', 'bottle.request.headers', read_only=True)
def headers(self):
- ''' Request HTTP Headers stored in a :class:`HeaderDict`. '''
+ ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to
+ HTTP request headers. '''
return WSGIHeaderDict(self.environ)
- @DictProperty('environ', 'bottle.get', read_only=True)
- def GET(self):
- """ The QUERY_STRING parsed into an instance of :class:`MultiDict`. """
+ def get_header(self, name, default=None):
+ ''' Return the value of a request header, or a given default value. '''
+ return self.headers.get(name, default)
+
+ @DictProperty('environ', 'bottle.request.cookies', read_only=True)
+ def cookies(self):
+ """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT
+ decoded. Use :meth:`get_cookie` if you expect signed cookies. """
+ cookies = SimpleCookie(self.environ.get('HTTP_COOKIE',''))
+ return FormsDict((c.key, c.value) for c in cookies.itervalues())
+
+ def get_cookie(self, key, default=None, secret=None):
+ """ Return the content of a cookie. To read a `Signed Cookie`, the
+ `secret` must match the one used to create the cookie (see
+ :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
+ cookie or wrong signature), return a default value. """
+ value = self.cookies.get(key)
+ if secret and value:
+ dec = cookie_decode(value, secret) # (key, value) tuple or None
+ return dec[1] if dec and dec[0] == key else default
+ return value or default
+
+ @DictProperty('environ', 'bottle.request.query', read_only=True)
+ def query(self):
+ ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These
+ values are sometimes called "URL arguments" or "GET parameters", but
+ not to be confused with "URL wildcards" as they are provided by the
+ :class:`Router`. '''
data = parse_qs(self.query_string, keep_blank_values=True)
- get = self.environ['bottle.get'] = MultiDict()
+ get = self.environ['bottle.get'] = FormsDict()
for key, values in data.iteritems():
for value in values:
get[key] = value
return get
- @DictProperty('environ', 'bottle.post', read_only=True)
- def POST(self):
- """ The combined values from :attr:`forms` and :attr:`files`. Values are
- either strings (form values) or instances of
- :class:`cgi.FieldStorage` (file uploads).
- """
- post = MultiDict()
- safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi
- for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
- if key in self.environ: safe_env[key] = self.environ[key]
- if NCTextIOWrapper:
- fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n')
- else:
- fb = self.body
- data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True)
- for item in data.list or []:
- post[item.name] = item if item.filename else item.value
- return post
-
- @DictProperty('environ', 'bottle.forms', read_only=True)
+ @DictProperty('environ', 'bottle.request.forms', read_only=True)
def forms(self):
- """ POST form values parsed into an instance of :class:`MultiDict`.
-
- This property contains form values parsed from an `url-encoded`
- or `multipart/form-data` encoded POST request bidy. The values are
- native strings.
- """
- forms = MultiDict()
+ """ Form values parsed from an `url-encoded` or `multipart/form-data`
+ encoded POST or PUT request body. The result is retuned as a
+ :class:`FormsDict`. All keys and values are strings. File uploads
+ are stored separately in :attr:`files`. """
+ forms = FormsDict()
for name, item in self.POST.iterallitems():
if not hasattr(item, 'filename'):
forms[name] = item
return forms
- @DictProperty('environ', 'bottle.files', read_only=True)
- def files(self):
- """ File uploads parsed into an instance of :class:`MultiDict`.
+ @DictProperty('environ', 'bottle.request.params', read_only=True)
+ def params(self):
+ """ A :class:`FormsDict` with the combined values of :attr:`query` and
+ :attr:`forms`. File uploads are stored in :attr:`files`. """
+ params = FormsDict()
+ for key, value in self.query.iterallitems():
+ params[key] = value
+ for key, value in self.forms.iterallitems():
+ params[key] = value
+ return params
- This property contains file uploads parsed from an
- `multipart/form-data` encoded POST request body. The values are
- instances of :class:`cgi.FieldStorage`.
+ @DictProperty('environ', 'bottle.request.files', read_only=True)
+ def files(self):
+ """ File uploads parsed from an `url-encoded` or `multipart/form-data`
+ encoded POST or PUT request body. The values are instances of
+ :class:`cgi.FieldStorage`. The most important attributes are:
+
+ filename
+ The filename, if specified; otherwise None; this is the client
+ side filename, *not* the file name on which it is stored (that's
+ a temporary file you don't deal with)
+ file
+ The file(-like) object from which you can read the data.
+ value
+ The value as a *string*; for file uploads, this transparently
+ reads the file every time you request the value. Do not do this
+ on big files.
"""
- files = MultiDict()
+ files = FormsDict()
for name, item in self.POST.iterallitems():
if hasattr(item, 'filename'):
files[name] = item
return files
- @DictProperty('environ', 'bottle.params', read_only=True)
- def params(self):
- """ A combined :class:`MultiDict` with values from :attr:`forms` and
- :attr:`GET`. File-uploads are not included. """
- params = MultiDict(self.GET)
- for key, value in self.forms.iterallitems():
- params[key] = value
- return params
+ @DictProperty('environ', 'bottle.request.json', read_only=True)
+ def json(self):
+ ''' If the ``Content-Type`` header is ``application/json``, this
+ property holds the parsed content of the request body. Only requests
+ smaller than :attr:`MEMFILE_MAX` are processed to avoid memory
+ exhaustion. '''
+ if 'application/json' in self.environ.get('CONTENT_TYPE', '') \
+ and 0 < self.content_length < self.MEMFILE_MAX:
+ return json_loads(self.body.read(self.MEMFILE_MAX))
+ return None
- @DictProperty('environ', 'bottle.body', read_only=True)
+ @DictProperty('environ', 'bottle.request.body', read_only=True)
def _body(self):
- """ The HTTP request body as a seekable file-like object.
-
- This property returns a copy of the `wsgi.input` stream and should
- be used instead of `environ['wsgi.input']`.
- """
maxread = max(0, self.content_length)
stream = self.environ['wsgi.input']
- body = BytesIO() if maxread < MEMFILE_MAX else TemporaryFile(mode='w+b')
+ body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b')
while maxread > 0:
- part = stream.read(min(maxread, MEMFILE_MAX))
+ part = stream.read(min(maxread, self.MEMFILE_MAX))
if not part: break
body.write(part)
maxread -= len(part)
@@ -966,124 +995,378 @@ class Request(threading.local, DictMixin):
@property
def body(self):
+ """ The HTTP request body as a seek-able file-like object. Depending on
+ :attr:`MEMFILE_MAX`, this is either a temporary file or a
+ :class:`io.BytesIO` instance. Accessing this property for the first
+ time reads and replaces the ``wsgi.input`` environ variable.
+ Subsequent accesses just do a `seek(0)` on the file object. """
self._body.seek(0)
return self._body
- @property
- def auth(self): #TODO: Tests and docs. Add support for digest. namedtuple?
- """ HTTP authorization data as a (user, passwd) tuple. (experimental)
+ #: An alias for :attr:`query`.
+ GET = query
- This implementation currently only supports basic auth and returns
- None on errors.
+ @DictProperty('environ', 'bottle.request.post', read_only=True)
+ def POST(self):
+ """ The values of :attr:`forms` and :attr:`files` combined into a single
+ :class:`FormsDict`. Values are either strings (form values) or
+ instances of :class:`cgi.FieldStorage` (file uploads).
"""
- return parse_auth(self.headers.get('Authorization',''))
+ post = FormsDict()
+ safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi
+ for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
+ if key in self.environ: safe_env[key] = self.environ[key]
+ if NCTextIOWrapper:
+ fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n')
+ else:
+ fb = self.body
+ data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True)
+ for item in data.list or []:
+ post[item.name] = item if item.filename else item.value
+ return post
- @DictProperty('environ', 'bottle.cookies', read_only=True)
+ @property
def COOKIES(self):
- """ Cookies parsed into a dictionary. Signed cookies are NOT decoded
- automatically. See :meth:`get_cookie` for details.
- """
- raw_dict = SimpleCookie(self.headers.get('Cookie',''))
- cookies = {}
- for cookie in raw_dict.itervalues():
- cookies[cookie.key] = cookie.value
- return cookies
-
- def get_cookie(self, key, secret=None):
- """ Return the content of a cookie. To read a `Signed Cookies`, use the
- same `secret` as used to create the cookie (see
- :meth:`Response.set_cookie`). If anything goes wrong, None is
- returned.
- """
- value = self.COOKIES.get(key)
- if secret and value:
- dec = cookie_decode(value, secret) # (key, value) tuple or None
- return dec[1] if dec and dec[0] == key else None
- return value or None
+ ''' Alias for :attr:`cookies` (deprecated). '''
+ depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).')
+ return self.cookies
@property
- def is_ajax(self):
- ''' True if the request was generated using XMLHttpRequest '''
- #TODO: write tests
- return self.header.get('X-Requested-With') == 'XMLHttpRequest'
+ def url(self):
+ """ The full request URI including hostname and scheme. If your app
+ lives behind a reverse proxy or load balancer and you get confusing
+ results, make sure that the ``X-Forwarded-Host`` header is set
+ correctly. """
+ return self.urlparts.geturl()
+ @DictProperty('environ', 'bottle.request.urlparts', read_only=True)
+ def urlparts(self):
+ ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple.
+ The tuple contains (scheme, host, path, query_string and fragment),
+ but the fragment is always empty because it is not visible to the
+ server. '''
+ env = self.environ
+ http = env.get('wsgi.url_scheme', 'http')
+ host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
+ if not host:
+ # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
+ host = env.get('SERVER_NAME', '127.0.0.1')
+ port = env.get('SERVER_PORT')
+ if port and port != ('80' if http == 'http' else '443'):
+ host += ':' + port
+ path = urlquote(self.fullpath)
+ return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
-class Response(threading.local):
- """ Represents a single HTTP response using thread-local attributes.
- """
+ @property
+ def fullpath(self):
+ """ Request path including :attr:`script_name` (if present). """
+ return urljoin(self.script_name, self.path.lstrip('/'))
- def __init__(self):
- self.bind()
+ @property
+ def query_string(self):
+ """ The raw :attr:`query` part of the URL (everything in between ``?``
+ and ``#``) as a string. """
+ return self.environ.get('QUERY_STRING', '')
- def bind(self):
- """ Resets the Response object to its factory defaults. """
- self._COOKIES = None
- self.status = 200
- self.headers = HeaderDict()
- self.content_type = 'text/html; charset=UTF-8'
+ @property
+ def script_name(self):
+ ''' The initial portion of the URL's `path` that was removed by a higher
+ level (server or routing middleware) before the application was
+ called. This script path is returned with leading and tailing
+ slashes. '''
+ script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
+ return '/' + script_name + '/' if script_name else '/'
+
+ def path_shift(self, shift=1):
+ ''' Shift path segments from :attr:`path` to :attr:`script_name` and
+ vice versa.
+
+ :param shift: The number of path segments to shift. May be negative
+ to change the shift direction. (default: 1)
+ '''
+ script = self.environ.get('SCRIPT_NAME','/')
+ self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift)
@property
- def header(self):
- depr("Response.header renamed to Response.headers")
- return self.headers
+ def content_length(self):
+ ''' The request body length as an integer. The client is responsible to
+ set this header. Otherwise, the real length of the body is unknown
+ and -1 is returned. In this case, :attr:`body` will be empty. '''
+ return int(self.environ.get('CONTENT_LENGTH') or -1)
+
+ @property
+ def is_xhr(self):
+ ''' True if the request was triggered by a XMLHttpRequest. This only
+ works with JavaScript libraries that support the `X-Requested-With`
+ header (most of the popular libraries do). '''
+ requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','')
+ return requested_with.lower() == 'xmlhttprequest'
+
+ @property
+ def is_ajax(self):
+ ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. '''
+ return self.is_xhr
+
+ @property
+ def auth(self):
+ """ HTTP authentication data as a (user, password) tuple. This
+ implementation currently supports basic (not digest) authentication
+ only. If the authentication happened at a higher level (e.g. in the
+ front web-server or a middleware), the password field is None, but
+ the user field is looked up from the ``REMOTE_USER`` environ
+ variable. On any errors, None is returned. """
+ basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION',''))
+ if basic: return basic
+ ruser = self.environ.get('REMOTE_USER')
+ if ruser: return (ruser, None)
+ return None
+
+ @property
+ def remote_route(self):
+ """ A list of all IPs that were involved in this request, starting with
+ the client IP and followed by zero or more proxies. This does only
+ work if all proxies support the ```X-Forwarded-For`` header. Note
+ that this information can be forged by malicious clients. """
+ proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
+ if proxy: return [ip.strip() for ip in proxy.split(',')]
+ remote = self.environ.get('REMOTE_ADDR')
+ return [remote] if remote else []
+
+ @property
+ def remote_addr(self):
+ """ The client IP as a string. Note that this information can be forged
+ by malicious clients. """
+ route = self.remote_route
+ return route[0] if route else None
+
+ def copy(self):
+ """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """
+ return Request(self.environ.copy())
+
+ def __getitem__(self, key): return self.environ[key]
+ def __delitem__(self, key): self[key] = ""; del(self.environ[key])
+ def __iter__(self): return iter(self.environ)
+ def __len__(self): return len(self.environ)
+ def keys(self): return self.environ.keys()
+ def __setitem__(self, key, value):
+ """ Change an environ value and clear all caches that depend on it. """
+
+ if self.environ.get('bottle.request.readonly'):
+ raise KeyError('The environ dictionary is read-only.')
+
+ self.environ[key] = value
+ todelete = ()
+
+ if key == 'wsgi.input':
+ todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
+ elif key == 'QUERY_STRING':
+ todelete = ('query', 'params')
+ elif key.startswith('HTTP_'):
+ todelete = ('headers', 'cookies')
+
+ for key in todelete:
+ self.environ.pop('bottle.request.'+key, None)
+
+ def __repr__(self):
+ return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)
+
+def _hkey(s):
+ return s.title().replace('_','-')
+
+
+class HeaderProperty(object):
+ def __init__(self, name, reader=None, writer=str, default=''):
+ self.name, self.reader, self.writer, self.default = name, reader, writer, default
+ self.__doc__ = 'Current value of the %r header.' % name.title()
+
+ def __get__(self, obj, cls):
+ if obj is None: return self
+ value = obj.headers.get(self.name)
+ return self.reader(value) if (value and self.reader) else (value or self.default)
+
+ def __set__(self, obj, value):
+ if self.writer: value = self.writer(value)
+ obj.headers[self.name] = value
+
+ def __delete__(self, obj):
+ if self.name in obj.headers:
+ del obj.headers[self.name]
+
+
+class BaseResponse(object):
+ """ Storage class for a response body as well as headers and cookies.
+
+ This class does support dict-like case-insensitive item-access to
+ headers, but is NOT a dict. Most notably, iterating over a response
+ yields parts of the body and not the headers.
+ """
+
+ default_status = 200
+ default_content_type = 'text/html; charset=UTF-8'
+
+ # Header blacklist for specific response codes
+ # (rfc2616 section 10.2.3 and 10.3.5)
+ bad_headers = {
+ 204: set(('Content-Type',)),
+ 304: set(('Allow', 'Content-Encoding', 'Content-Language',
+ 'Content-Length', 'Content-Range', 'Content-Type',
+ 'Content-Md5', 'Last-Modified'))}
+
+ def __init__(self, body='', status=None, **headers):
+ self._status_line = None
+ self._status_code = None
+ self.body = body
+ self._cookies = None
+ self._headers = {'Content-Type': [self.default_content_type]}
+ self.status = status or self.default_status
+ if headers:
+ for name, value in headers.items():
+ self[name] = value
def copy(self):
''' Returns a copy of self. '''
copy = Response()
copy.status = self.status
- copy.headers = self.headers.copy()
- copy.content_type = self.content_type
+ copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
return copy
+ def __iter__(self):
+ return iter(self.body)
+
+ def close(self):
+ if hasattr(self.body, 'close'):
+ self.body.close()
+
+ @property
+ def status_line(self):
+ ''' The HTTP status line as a string (e.g. ``404 Not Found``).'''
+ return self._status_line
+
+ @property
+ def status_code(self):
+ ''' The HTTP status code as an integer (e.g. 404).'''
+ return self._status_code
+
+ def _set_status(self, status):
+ if isinstance(status, int):
+ code, status = status, _HTTP_STATUS_LINES.get(status)
+ elif ' ' in status:
+ status = status.strip()
+ code = int(status.split()[0])
+ else:
+ raise ValueError('String status line without a reason phrase.')
+ if not 100 <= code <= 999: raise ValueError('Status code out of range.')
+ self._status_code = code
+ self._status_line = status or ('%d Unknown' % code)
+
+ def _get_status(self):
+ depr('BaseReuqest.status will change to return a string in 0.11. Use'\
+ ' status_line and status_code to make sure.') #0.10
+ return self._status_code
+
+ status = property(_get_status, _set_status, None,
+ ''' A writeable property to change the HTTP response status. It accepts
+ either a numeric code (100-999) or a string with a custom reason
+ phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
+ :data:`status_code` are updates accordingly. The return value is
+ always a numeric code. ''')
+ del _get_status, _set_status
+
+ @property
+ def headers(self):
+ ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like
+ view on the response headers. '''
+ self.__dict__['headers'] = hdict = HeaderDict()
+ hdict.dict = self._headers
+ return hdict
+
+ def __contains__(self, name): return _hkey(name) in self._headers
+ def __delitem__(self, name): del self._headers[_hkey(name)]
+ def __getitem__(self, name): return self._headers[_hkey(name)][-1]
+ def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)]
+
+ def get_header(self, name, default=None):
+ ''' Return the value of a previously defined header. If there is no
+ header with that name, return a default value. '''
+ return self._headers.get(_hkey(name), [default])[-1]
+
+ def set_header(self, name, value, append=False):
+ ''' Create a new response header, replacing any previously defined
+ headers with the same name. '''
+ if append:
+ self.add_header(name, value)
+ else:
+ self._headers[_hkey(name)] = [str(value)]
+
+ def add_header(self, name, value):
+ ''' Add an additional response header, not removing duplicates. '''
+ self._headers.setdefault(_hkey(name), []).append(str(value))
+
+ def iter_headers(self):
+ ''' Yield (header, value) tuples, skipping headers that are not
+ allowed with the current response status code. '''
+ headers = self._headers.iteritems()
+ bad_headers = self.bad_headers.get(self.status_code)
+ if bad_headers:
+ headers = [h for h in headers if h[0] not in bad_headers]
+ for name, values in headers:
+ for value in values:
+ yield name, value
+ if self._cookies:
+ for c in self._cookies.values():
+ yield 'Set-Cookie', c.OutputString()
+
def wsgiheader(self):
- ''' Returns a wsgi conform list of header/value pairs. '''
- for c in self.COOKIES.values():
- if c.OutputString() not in self.headers.getall('Set-Cookie'):
- self.headers.append('Set-Cookie', c.OutputString())
- # rfc2616 section 10.2.3, 10.3.5
- if self.status in (204, 304) and 'content-type' in self.headers:
- del self.headers['content-type']
- if self.status == 304:
- for h in ('allow', 'content-encoding', 'content-language',
- 'content-length', 'content-md5', 'content-range',
- 'content-type', 'last-modified'): # + c-location, expires?
- if h in self.headers:
- del self.headers[h]
- return list(self.headers.iterallitems())
- headerlist = property(wsgiheader)
+ depr('The wsgiheader method is deprecated. See headerlist.') #0.10
+ return self.headerlist
@property
- def charset(self):
- """ Return the charset specified in the content-type header.
+ def headerlist(self):
+ ''' WSGI conform list of (header, value) tuples. '''
+ return list(self.iter_headers())
- This defaults to `UTF-8`.
- """
+ content_type = HeaderProperty('Content-Type')
+ content_length = HeaderProperty('Content-Length', reader=int)
+
+ @property
+ def charset(self):
+ """ Return the charset specified in the content-type header (default: utf8). """
if 'charset=' in self.content_type:
return self.content_type.split('charset=')[-1].split(';')[0].strip()
return 'UTF-8'
@property
def COOKIES(self):
- """ A dict-like SimpleCookie instance. Use :meth:`set_cookie` instead. """
- if not self._COOKIES:
- self._COOKIES = SimpleCookie()
- return self._COOKIES
-
- def set_cookie(self, key, value, secret=None, **kargs):
- ''' Add a cookie or overwrite an old one. If the `secret` parameter is
+ """ A dict-like SimpleCookie instance. This should not be used directly.
+ See :meth:`set_cookie`. """
+ depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10
+ if not self._cookies:
+ self._cookies = SimpleCookie()
+ return self._cookies
+
+ def set_cookie(self, name, value, secret=None, **options):
+ ''' Create a new cookie or replace an old one. If the `secret` parameter is
set, create a `Signed Cookie` (described below).
- :param key: the name of the cookie.
+ :param name: the name of the cookie.
:param value: the value of the cookie.
- :param secret: required for signed cookies. (default: None)
+ :param secret: a signature key required for signed cookies.
+
+ Additionally, this method accepts all RFC 2109 attributes that are
+ supported by :class:`cookie.Morsel`, including:
+
:param max_age: maximum age in seconds. (default: None)
- :param expires: a datetime object or UNIX timestamp. (defaut: None)
+ :param expires: a datetime object or UNIX timestamp. (default: None)
:param domain: the domain that is allowed to read the cookie.
(default: current domain)
- :param path: limits the cookie to a given path (default: /)
+ :param path: limits the cookie to a given path (default: current path)
+ :param secure: limit the cookie to HTTPS connections (default: off).
+ :param httponly: prevents client-side javascript to read this cookie
+ (default: off, requires Python 2.6 or newer).
- If neither `expires` nor `max_age` are set (default), the cookie
- lasts only as long as the browser is not closed.
+ If neither `expires` nor `max_age` is set (default), the cookie will
+ expire at the end of the browser session (as soon as the browser
+ window is closed).
Signed cookies may store any pickle-able object and are
cryptographically signed to prevent manipulation. Keep in mind that
@@ -1094,31 +1377,55 @@ class Response(threading.local):
cookie). The main intention is to make pickling and unpickling
save, not to store secret information at client side.
'''
+ if not self._cookies:
+ self._cookies = SimpleCookie()
+
if secret:
- value = touni(cookie_encode((key, value), secret))
+ value = touni(cookie_encode((name, value), secret))
elif not isinstance(value, basestring):
- raise TypeError('Secret missing for non-string Cookie.')
-
- self.COOKIES[key] = value
- for k, v in kargs.iteritems():
- self.COOKIES[key][k.replace('_', '-')] = v
+ raise TypeError('Secret key missing for non-string Cookie.')
+
+ if len(value) > 4096: raise ValueError('Cookie value to long.')
+ self._cookies[name] = value
+
+ for key, value in options.iteritems():
+ if key == 'max_age':
+ if isinstance(value, timedelta):
+ value = value.seconds + value.days * 24 * 3600
+ if key == 'expires':
+ if isinstance(value, (datedate, datetime)):
+ value = value.timetuple()
+ elif isinstance(value, (int, float)):
+ value = time.gmtime(value)
+ value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value)
+ self._cookies[name][key.replace('_', '-')] = value
def delete_cookie(self, key, **kwargs):
''' Delete a cookie. Be sure to use the same `domain` and `path`
- parameters as used to create the cookie. '''
+ settings as used to create the cookie. '''
kwargs['max_age'] = -1
kwargs['expires'] = 0
self.set_cookie(key, '', **kwargs)
- def get_content_type(self):
- """ Current 'Content-Type' header. """
- return self.headers['Content-Type']
+ def __repr__(self):
+ out = ''
+ for name, value in self.headerlist:
+ out += '%s: %s\n' % (name.title(), value.strip())
+ return out
+
- def set_content_type(self, value):
- self.headers['Content-Type'] = value
+class LocalRequest(BaseRequest, threading.local):
+ ''' A thread-local subclass of :class:`BaseRequest`. '''
+ def __init__(self): pass
+ bind = BaseRequest.__init__
- content_type = property(get_content_type, set_content_type, None,
- get_content_type.__doc__)
+
+class LocalResponse(BaseResponse, threading.local):
+ ''' A thread-local subclass of :class:`BaseResponse`. '''
+ bind = BaseResponse.__init__
+
+Response = LocalResponse # BC 0.9
+Request = LocalRequest # BC 0.9
@@ -1129,10 +1436,11 @@ class Response(threading.local):
# Plugins ######################################################################
###############################################################################
-
+class PluginError(BottleException): pass
class JSONPlugin(object):
name = 'json'
+ api = 2
def __init__(self, json_dumps=json_dumps):
self.json_dumps = json_dumps
@@ -1143,18 +1451,23 @@ class JSONPlugin(object):
def wrapper(*a, **ka):
rv = callback(*a, **ka)
if isinstance(rv, dict):
+ #Attempt to serialize, raises exception on failure
+ json_response = dumps(rv)
+ #Set content type only if serialization succesful
response.content_type = 'application/json'
- return dumps(rv)
+ return json_response
return rv
return wrapper
-
class HooksPlugin(object):
name = 'hooks'
+ api = 2
+
+ _names = 'before_request', 'after_request', 'app_reset'
def __init__(self):
- self.hooks = {'before_request': [], 'after_request': []}
+ self.hooks = dict((name, []) for name in self._names)
self.app = None
def _empty(self):
@@ -1165,56 +1478,29 @@ class HooksPlugin(object):
def add(self, name, func):
''' Attach a callback to a hook. '''
- if name not in self.hooks:
- raise ValueError("Unknown hook name %s" % name)
was_empty = self._empty()
- self.hooks[name].append(func)
+ self.hooks.setdefault(name, []).append(func)
if self.app and was_empty and not self._empty(): self.app.reset()
def remove(self, name, func):
''' Remove a callback from a hook. '''
- if name not in self.hooks:
- raise ValueError("Unknown hook name %s" % name)
was_empty = self._empty()
- self.hooks[name].remove(func)
+ if name in self.hooks and func in self.hooks[name]:
+ self.hooks[name].remove(func)
if self.app and not was_empty and self._empty(): self.app.reset()
- def apply(self, callback, context):
- if self._empty(): return callback
- before_request = self.hooks['before_request']
- after_request = self.hooks['after_request']
- def wrapper(*a, **ka):
- for hook in before_request: hook()
- rv = callback(*a, **ka)
- for hook in after_request[::-1]: hook()
- return rv
- return wrapper
-
-
-
-class TypeFilterPlugin(object):
- def __init__(self):
- self.filter = []
- self.app = None
-
- def setup(self, app):
- self.app = app
-
- def add(self, ftype, func):
- if not isinstance(ftype, type):
- raise TypeError("Expected type object, got %s" % type(ftype))
- self.filter = [(t, f) for (t, f) in self.filter if t != ftype]
- self.filter.append((ftype, func))
- if len(self.filter) == 1 and self.app: self.app.reset()
+ def trigger(self, name, *a, **ka):
+ ''' Trigger a hook and return a list of results. '''
+ hooks = self.hooks[name]
+ if ka.pop('reversed', False): hooks = hooks[::-1]
+ return [hook(*a, **ka) for hook in hooks]
def apply(self, callback, context):
- filter = self.filter
- if not filter: return callback
+ if self._empty(): return callback
def wrapper(*a, **ka):
+ self.trigger('before_request')
rv = callback(*a, **ka)
- for testtype, filterfunc in filter:
- if isinstance(rv, testtype):
- rv = filterfunc(rv)
+ self.trigger('after_request', reversed=True)
return rv
return wrapper
@@ -1225,14 +1511,15 @@ class TemplatePlugin(object):
element must be a dict with additional options (e.g. `template_engine`)
or default variables for the template. '''
name = 'template'
+ api = 2
- def apply(self, callback, context):
- conf = context['config'].get('template')
+ def apply(self, callback, route):
+ conf = route.config.get('template')
if isinstance(conf, (tuple, list)) and len(conf) == 2:
return view(conf[0], **conf[1])(callback)
- elif isinstance(conf, str) and 'template_opts' in context['config']:
+ elif isinstance(conf, str) and 'template_opts' in route.config:
depr('The `template_opts` parameter is deprecated.') #0.9
- return view(conf, **context['config']['template_opts'])(callback)
+ return view(conf, **route.config['template_opts'])(callback)
elif isinstance(conf, str):
return view(conf)(callback)
else:
@@ -1246,7 +1533,7 @@ class _ImportRedirect(object):
self.name = name
self.impmask = impmask
self.module = sys.modules.setdefault(name, imp.new_module(name))
- self.module.__dict__.update({'__file__': '<virtual>', '__path__': [],
+ self.module.__dict__.update({'__file__': __file__, '__path__': [],
'__all__': [], '__loader__': self})
sys.meta_path.append(self)
@@ -1277,53 +1564,115 @@ class _ImportRedirect(object):
class MultiDict(DictMixin):
- """ A dict that remembers old values for each key """
- # collections.MutableMapping would be better for Python >= 2.6
- def __init__(self, *a, **k):
- self.dict = dict()
- for k, v in dict(*a, **k).iteritems():
- self[k] = v
+ """ This dict stores multiple values per key, but behaves exactly like a
+ normal dict in that it returns only the newest value for any given key.
+ There are special methods available to access the full list of values.
+ """
+ def __init__(self, *a, **k):
+ self.dict = dict((k, [v]) for k, v in dict(*a, **k).iteritems())
def __len__(self): return len(self.dict)
def __iter__(self): return iter(self.dict)
def __contains__(self, key): return key in self.dict
def __delitem__(self, key): del self.dict[key]
- def keys(self): return self.dict.keys()
- def __getitem__(self, key): return self.get(key, KeyError, -1)
+ def __getitem__(self, key): return self.dict[key][-1]
def __setitem__(self, key, value): self.append(key, value)
-
- def append(self, key, value): self.dict.setdefault(key, []).append(value)
- def replace(self, key, value): self.dict[key] = [value]
- def getall(self, key): return self.dict.get(key) or []
-
- def get(self, key, default=None, index=-1):
- if key not in self.dict and default != KeyError:
- return [default][index]
- return self.dict[key][index]
-
+ def iterkeys(self): return self.dict.iterkeys()
+ def itervalues(self): return (v[-1] for v in self.dict.itervalues())
+ def iteritems(self): return ((k, v[-1]) for (k, v) in self.dict.iteritems())
def iterallitems(self):
for key, values in self.dict.iteritems():
for value in values:
yield key, value
+ # 2to3 is not able to fix these automatically.
+ keys = iterkeys if py3k else lambda self: list(self.iterkeys())
+ values = itervalues if py3k else lambda self: list(self.itervalues())
+ items = iteritems if py3k else lambda self: list(self.iteritems())
+ allitems = iterallitems if py3k else lambda self: list(self.iterallitems())
+
+ def get(self, key, default=None, index=-1, type=None):
+ ''' Return the most recent value for a key.
+
+ :param default: The default value to be returned if the key is not
+ present or the type conversion fails.
+ :param index: An index for the list of available values.
+ :param type: If defined, this callable is used to cast the value
+ into a specific type. Exception are suppressed and result in
+ the default value to be returned.
+ '''
+ try:
+ val = self.dict[key][index]
+ return type(val) if type else val
+ except Exception, e:
+ pass
+ return default
-class HeaderDict(MultiDict):
- """ Same as :class:`MultiDict`, but title()s the keys and overwrites. """
- def __contains__(self, key):
- return MultiDict.__contains__(self, self.httpkey(key))
- def __getitem__(self, key):
- return MultiDict.__getitem__(self, self.httpkey(key))
- def __delitem__(self, key):
- return MultiDict.__delitem__(self, self.httpkey(key))
- def __setitem__(self, key, value): self.replace(key, value)
- def get(self, key, default=None, index=-1):
- return MultiDict.get(self, self.httpkey(key), default, index)
def append(self, key, value):
- return MultiDict.append(self, self.httpkey(key), str(value))
+ ''' Add a new value to the list of values for this key. '''
+ self.dict.setdefault(key, []).append(value)
+
def replace(self, key, value):
- return MultiDict.replace(self, self.httpkey(key), str(value))
- def getall(self, key): return MultiDict.getall(self, self.httpkey(key))
- def httpkey(self, key): return str(key).replace('_','-').title()
+ ''' Replace the list of values with a single value. '''
+ self.dict[key] = [value]
+
+ def getall(self, key):
+ ''' Return a (possibly empty) list of values for a key. '''
+ return self.dict.get(key) or []
+
+ #: Aliases for WTForms to mimic other multi-dict APIs (Django)
+ getone = get
+ getlist = getall
+
+
+
+class FormsDict(MultiDict):
+ ''' This :class:`MultiDict` subclass is used to store request form data.
+ Additionally to the normal dict-like item access methods (which return
+ unmodified data as native strings), this container also supports
+ attribute-like access to its values. Attribues are automatiically de- or
+ recoded to match :attr:`input_encoding` (default: 'utf8'). Missing
+ attributes default to an empty string. '''
+
+ #: Encoding used for attribute values.
+ input_encoding = 'utf8'
+
+ def getunicode(self, name, default=None, encoding=None):
+ value, enc = self.get(name, default), encoding or self.input_encoding
+ try:
+ if isinstance(value, bytes): # Python 2 WSGI
+ return value.decode(enc)
+ elif isinstance(value, unicode): # Python 3 WSGI
+ return value.encode('latin1').decode(enc)
+ return value
+ except UnicodeError, e:
+ return default
+
+ def __getattr__(self, name): return self.getunicode(name, default=u'')
+
+
+class HeaderDict(MultiDict):
+ """ A case-insensitive version of :class:`MultiDict` that defaults to
+ replace the old value instead of appending it. """
+
+ def __init__(self, *a, **ka):
+ self.dict = {}
+ if a or ka: self.update(*a, **ka)
+
+ def __contains__(self, key): return _hkey(key) in self.dict
+ def __delitem__(self, key): del self.dict[_hkey(key)]
+ def __getitem__(self, key): return self.dict[_hkey(key)][-1]
+ def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)]
+ def append(self, key, value):
+ self.dict.setdefault(_hkey(key), []).append(str(value))
+ def replace(self, key, value): self.dict[_hkey(key)] = [str(value)]
+ def getall(self, key): return self.dict.get(_hkey(key)) or []
+ def get(self, key, default=None, index=-1):
+ return MultiDict.get(self, _hkey(key), default, index)
+ def filter(self, names):
+ for name in map(_hkey, names):
+ if name in self.dict:
+ del self.dict[name]
class WSGIHeaderDict(DictMixin):
@@ -1370,11 +1719,44 @@ class WSGIHeaderDict(DictMixin):
elif key in self.cgikeys:
yield key.replace('_', '-').title()
- def keys(self): return list(self)
- def __len__(self): return len(list(self))
+ def keys(self): return [x for x in self]
+ def __len__(self): return len(self.keys())
def __contains__(self, key): return self._ekey(key) in self.environ
+class ConfigDict(dict):
+ ''' A dict-subclass with some extras: You can access keys like attributes.
+ Uppercase attributes create new ConfigDicts and act as name-spaces.
+ Other missing attributes return None. Calling a ConfigDict updates its
+ values and returns itself.
+
+ >>> cfg = ConfigDict()
+ >>> cfg.Namespace.value = 5
+ >>> cfg.OtherNamespace(a=1, b=2)
+ >>> cfg
+ {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}}
+ '''
+
+ def __getattr__(self, key):
+ if key not in self and key[0].isupper():
+ self[key] = ConfigDict()
+ return self.get(key)
+
+ def __setattr__(self, key, value):
+ if hasattr(dict, key):
+ raise AttributeError('Read-only attribute.')
+ if key in self and self[key] and isinstance(self[key], ConfigDict):
+ raise AttributeError('Non-empty namespace attribute.')
+ self[key] = value
+
+ def __delattr__(self, key):
+ if key in self: del self[key]
+
+ def __call__(self, *a, **ka):
+ for key, value in dict(*a, **ka).iteritems(): setattr(self, key, value)
+ return self
+
+
class AppStack(list):
""" A stack-like list. Calling it returns the head of the stack. """
@@ -1414,30 +1796,21 @@ class WSGIFileWrapper(object):
###############################################################################
-def dict2json(d):
- depr('JSONPlugin is the preferred way to return JSON.') #0.9
- response.content_type = 'application/json'
- return json_dumps(d)
-
-
def abort(code=500, text='Unknown Error: Application stopped.'):
""" Aborts execution and causes a HTTP error. """
raise HTTPError(code, text)
-def redirect(url, code=303):
- """ Aborts execution and causes a 303 redirect. """
+def redirect(url, code=None):
+ """ Aborts execution and causes a 303 or 302 redirect, depending on
+ the HTTP protocol version. """
+ if code is None:
+ code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
location = urljoin(request.url, url)
raise HTTPResponse("", status=code, header=dict(Location=location))
-def send_file(*a, **k): #BC 0.6.4
- """ Raises the output of static_file(). (deprecated) """
- depr("Use 'raise static_file()' instead of 'send_file()'.")
- raise static_file(*a, **k)
-
-
-def static_file(filename, root, mimetype='auto', guessmime=True, download=False):
+def static_file(filename, root, mimetype='auto', download=False):
""" Open a file in a safe way and return :exc:`HTTPResponse` with status
code 200, 305, 401 or 404. Set Content-Type, Content-Encoding,
Content-Length and Last-Modified header. Obey If-Modified-Since header
@@ -1454,9 +1827,6 @@ def static_file(filename, root, mimetype='auto', guessmime=True, download=False)
if not os.access(filename, os.R_OK):
return HTTPError(403, "You do not have permission to access this file.")
- if not guessmime: #0.9
- if mimetype == 'auto': mimetype = 'text/plain'
- depr("To disable mime-type guessing, specify a type explicitly.")
if mimetype == 'auto':
mimetype, encoding = mimetypes.guess_type(filename)
if mimetype: header['Content-Type'] = mimetype
@@ -1514,9 +1884,10 @@ def parse_auth(header):
try:
method, data = header.split(None, 1)
if method.lower() == 'basic':
- name, pwd = base64.b64decode(data).split(':', 1)
- return name, pwd
- except (KeyError, ValueError, TypeError):
+ #TODO: Add 2to3 save base64[encode/decode] functions.
+ user, pwd = touni(base64.b64decode(tob(data))).split(':',1)
+ return user, pwd
+ except (KeyError, ValueError):
return None
@@ -1529,7 +1900,7 @@ def _lscmp(a, b):
def cookie_encode(data, key):
''' Encode and sign a pickle-able object. Return a (byte) string '''
msg = base64.b64encode(pickle.dumps(data, -1))
- sig = base64.b64encode(hmac.new(key, msg).digest())
+ sig = base64.b64encode(hmac.new(tob(key), msg).digest())
return tob('!') + sig + tob('?') + msg
@@ -1538,7 +1909,7 @@ def cookie_decode(data, key):
data = tob(data)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
- if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())):
+ if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())):
return pickle.loads(base64.b64decode(msg))
return None
@@ -1548,6 +1919,18 @@ def cookie_is_encoded(data):
return bool(data.startswith(tob('!')) and tob('?') in data)
+def html_escape(string):
+ ''' Escape HTML special characters ``&<>`` and quotes ``'"``. '''
+ return string.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')\
+ .replace('"','&quot;').replace("'",'&#039;')
+
+
+def html_quote(string):
+ ''' Escape and quote a string to be used as an HTTP attribute.'''
+ return '"%s"' % html_escape(string).replace('\n','%#10;')\
+ .replace('\r','&#13;').replace('\t','&#9;')
+
+
def yieldroutes(func):
""" Return a generator for routes that match the signature (name, args)
of the func parameter. This may yield more than one route if the function
@@ -1600,17 +1983,15 @@ def path_shift(script_name, path_info, shift=1):
return new_script_name, new_path_info
-
-# Decorators
-#TODO: Replace default_app() with app()
-
def validate(**vkargs):
"""
Validates and manipulates keyword arguments by user defined callables.
Handles ValueError and missing arguments by raising HTTPError(403).
"""
+ depr('Use route wildcard filters instead.')
def decorator(func):
- def wrapper(**kargs):
+ @functools.wraps(func)
+ def wrapper(*args, **kargs):
for key, value in vkargs.iteritems():
if key not in kargs:
abort(403, 'Missing parameter: %s' % key)
@@ -1618,7 +1999,7 @@ def validate(**vkargs):
kargs[key] = value(kargs[key])
except ValueError:
abort(403, 'Wrong parameter format for: %s' % key)
- return func(**kargs)
+ return func(*args, **kargs)
return wrapper
return decorator
@@ -1652,11 +2033,6 @@ url = make_default_app_wrapper('get_url')
del name
-def default():
- depr("The default() decorator is deprecated. Use @error(404) instead.")
- return error(404)
-
-
@@ -1685,15 +2061,17 @@ class CGIServer(ServerAdapter):
quiet = True
def run(self, handler): # pragma: no cover
from wsgiref.handlers import CGIHandler
- CGIHandler().run(handler) # Just ignore host and port here
+ def fixed_environ(environ, start_response):
+ environ.setdefault('PATH_INFO', '')
+ return handler(environ, start_response)
+ CGIHandler().run(fixed_environ)
class FlupFCGIServer(ServerAdapter):
def run(self, handler): # pragma: no cover
import flup.server.fcgi
- kwargs = {'bindAddress':(self.host, self.port)}
- kwargs.update(self.options) # allow to override bindAddress and others
- flup.server.fcgi.WSGIServer(handler, **kwargs).run()
+ self.options.setdefault('bindAddress', (self.host, self.port))
+ flup.server.fcgi.WSGIServer(handler, **self.options).run()
class WSGIRefServer(ServerAdapter):
@@ -1711,7 +2089,10 @@ class CherryPyServer(ServerAdapter):
def run(self, handler): # pragma: no cover
from cherrypy import wsgiserver
server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler)
- server.start()
+ try:
+ server.start()
+ finally:
+ server.stop()
class PasteServer(ServerAdapter):
@@ -1723,6 +2104,7 @@ class PasteServer(ServerAdapter):
httpserver.serve(handler, host=self.host, port=str(self.port),
**self.options)
+
class MeinheldServer(ServerAdapter):
def run(self, handler):
from meinheld import server
@@ -1755,9 +2137,7 @@ class FapwsServer(ServerAdapter):
class TornadoServer(ServerAdapter):
""" The super hyped asynchronous server by facebook. Untested. """
def run(self, handler): # pragma: no cover
- import tornado.wsgi
- import tornado.httpserver
- import tornado.ioloop
+ import tornado.wsgi, tornado.httpserver, tornado.ioloop
container = tornado.wsgi.WSGIContainer(handler)
server = tornado.httpserver.HTTPServer(container)
server.listen(port=self.port)
@@ -1807,22 +2187,29 @@ class GeventServer(ServerAdapter):
issues: No streaming, no pipelining, no SSL.
"""
def run(self, handler):
- from gevent import wsgi as wsgi_fast, pywsgi as wsgi, monkey
+ from gevent import wsgi as wsgi_fast, pywsgi, monkey, local
if self.options.get('monkey', True):
- monkey.patch_all()
- if self.options.get('fast', False):
- wsgi = wsgi_fast
+ if not threading.local is local.local: monkey.patch_all()
+ wsgi = wsgi_fast if self.options.get('fast') else pywsgi
wsgi.WSGIServer((self.host, self.port), handler).serve_forever()
class GunicornServer(ServerAdapter):
- """ Untested. """
+ """ Untested. See http://gunicorn.org/configure.html for options. """
def run(self, handler):
- from gunicorn.arbiter import Arbiter
- from gunicorn.config import Config
- handler.cfg = Config({'bind': "%s:%d" % (self.host, self.port), 'workers': 4})
- arbiter = Arbiter(handler)
- arbiter.run()
+ from gunicorn.app.base import Application
+
+ config = {'bind': "%s:%d" % (self.host, int(self.port))}
+ config.update(self.options)
+
+ class GunicornApplication(Application):
+ def init(self, parser, opts, args):
+ return config
+
+ def load(self):
+ return handler
+
+ GunicornApplication().run()
class EventletServer(ServerAdapter):
@@ -1833,8 +2220,7 @@ class EventletServer(ServerAdapter):
class RocketServer(ServerAdapter):
- """ Untested. As requested in issue 63
- https://github.com/defnull/bottle/issues/#issue/63 """
+ """ Untested. """
def run(self, handler):
from rocket import Rocket
server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler })
@@ -1842,7 +2228,7 @@ class RocketServer(ServerAdapter):
class BjoernServer(ServerAdapter):
- """ Screamingly fast server written in C: https://github.com/jonashaag/bjoern """
+ """ Fast server written in C: https://github.com/jonashaag/bjoern """
def run(self, handler):
from bjoern import run
run(handler, self.host, self.port)
@@ -1858,7 +2244,6 @@ class AutoServer(ServerAdapter):
except ImportError:
pass
-
server_names = {
'cgi': CGIServer,
'flup': FlupFCGIServer,
@@ -1889,57 +2274,41 @@ server_names = {
###############################################################################
-def _load(target, **vars):
- """ Fetch something from a module. The exact behaviour depends on the the
- target string:
-
- If the target is a valid python import path (e.g. `package.module`),
- the rightmost part is returned as a module object.
- If the target contains a colon (e.g. `package.module:var`) the module
- variable specified after the colon is returned.
- If the part after the colon contains any non-alphanumeric characters
- (e.g. `package.module:func(var)`) the result of the expression
- is returned. The expression has access to keyword arguments supplied
- to this function.
+def load(target, **namespace):
+ """ Import a module or fetch an object from a module.
- Example::
- >>> _load('bottle')
- <module 'bottle' from 'bottle.py'>
- >>> _load('bottle:Bottle')
- <class 'bottle.Bottle'>
- >>> _load('bottle:cookie_encode(v, secret)', v='foo', secret='bar')
- '!F+hN4dQxaDJ4QxxaZ+Z3jw==?gAJVA2Zvb3EBLg=='
+ * ``package.module`` returns `module` as a module object.
+ * ``pack.mod:name`` returns the module variable `name` from `pack.mod`.
+ * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result.
+ The last form accepts not only function calls, but any type of
+ expression. Keyword arguments passed to this function are available as
+ local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
"""
module, target = target.split(":", 1) if ':' in target else (target, None)
- if module not in sys.modules:
- __import__(module)
- if not target:
- return sys.modules[module]
- if target.isalnum():
- return getattr(sys.modules[module], target)
+ if module not in sys.modules: __import__(module)
+ if not target: return sys.modules[module]
+ if target.isalnum(): return getattr(sys.modules[module], target)
package_name = module.split('.')[0]
- vars[package_name] = sys.modules[package_name]
- return eval('%s.%s' % (module, target), vars)
+ namespace[package_name] = sys.modules[package_name]
+ return eval('%s.%s' % (module, target), namespace)
def load_app(target):
- """ Load a bottle application based on a target string and return the
- application object.
-
- If the target is an import path (e.g. package.module), the application
- stack is used to isolate the routes defined in that module.
- If the target contains a colon (e.g. package.module:myapp) the
- module variable specified after the colon is returned instead.
- """
- tmp = app.push() # Create a new "default application"
- rv = _load(target) # Import the target module
- app.remove(tmp) # Remove the temporary added default application
- return rv if isinstance(rv, Bottle) else tmp
-
+ """ Load a bottle application from a module and make sure that the import
+ does not affect the current default application, but returns a separate
+ application object. See :func:`load` for the target parameter. """
+ global NORUN; NORUN, nr_old = True, NORUN
+ try:
+ tmp = default_app.push() # Create a new "default application"
+ rv = load(target) # Import the target module
+ return rv if callable(rv) else tmp
+ finally:
+ default_app.remove(tmp) # Remove the temporary added default application
+ NORUN = nr_old
def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,
- interval=1, reloader=False, quiet=False, **kargs):
+ interval=1, reloader=False, quiet=False, plugins=None, **kargs):
""" Start a server instance. This method blocks until the server terminates.
:param app: WSGI application or target string supported by
@@ -1956,114 +2325,115 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,
:param quiet: Suppress output to stdout and stderr? (default: False)
:param options: Options passed to the server adapter.
"""
- app = app or default_app()
- if isinstance(app, basestring):
- app = load_app(app)
- if isinstance(server, basestring):
- server = server_names.get(server)
- if isinstance(server, type):
- server = server(host=host, port=port, **kargs)
- if not isinstance(server, ServerAdapter):
- raise RuntimeError("Server must be a subclass of ServerAdapter")
- server.quiet = server.quiet or quiet
- if not server.quiet and not os.environ.get('BOTTLE_CHILD'):
- print "Bottle server starting up (using %s)..." % repr(server)
- print "Listening on http://%s:%d/" % (server.host, server.port)
- print "Use Ctrl-C to quit."
- print
+ if NORUN: return
+ if reloader and not os.environ.get('BOTTLE_CHILD'):
+ try:
+ fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
+ os.close(fd) # We only need this file to exist. We never write to it
+ while os.path.exists(lockfile):
+ args = [sys.executable] + sys.argv
+ environ = os.environ.copy()
+ environ['BOTTLE_CHILD'] = 'true'
+ environ['BOTTLE_LOCKFILE'] = lockfile
+ p = subprocess.Popen(args, env=environ)
+ while p.poll() is None: # Busy wait...
+ os.utime(lockfile, None) # I am alive!
+ time.sleep(interval)
+ if p.poll() != 3:
+ if os.path.exists(lockfile): os.unlink(lockfile)
+ sys.exit(p.poll())
+ except KeyboardInterrupt:
+ pass
+ finally:
+ if os.path.exists(lockfile):
+ os.unlink(lockfile)
+ return
+
+ stderr = sys.stderr.write
+
try:
+ app = app or default_app()
+ if isinstance(app, basestring):
+ app = load_app(app)
+ if not callable(app):
+ raise ValueError("Application is not callable: %r" % app)
+
+ for plugin in plugins or []:
+ app.install(plugin)
+
+ if server in server_names:
+ server = server_names.get(server)
+ if isinstance(server, basestring):
+ server = load(server)
+ if isinstance(server, type):
+ server = server(host=host, port=port, **kargs)
+ if not isinstance(server, ServerAdapter):
+ raise ValueError("Unknown or unsupported server: %r" % server)
+
+ server.quiet = server.quiet or quiet
+ if not server.quiet:
+ stderr("Bottle server starting up (using %s)...\n" % repr(server))
+ stderr("Listening on http://%s:%d/\n" % (server.host, server.port))
+ stderr("Hit Ctrl-C to quit.\n\n")
+
if reloader:
- interval = min(interval, 1)
- if os.environ.get('BOTTLE_CHILD'):
- _reloader_child(server, app, interval)
- else:
- _reloader_observer(server, app, interval)
+ lockfile = os.environ.get('BOTTLE_LOCKFILE')
+ bgcheck = FileCheckerThread(lockfile, interval)
+ with bgcheck:
+ server.run(app)
+ if bgcheck.status == 'reload':
+ sys.exit(3)
else:
server.run(app)
except KeyboardInterrupt:
pass
- if not server.quiet and not os.environ.get('BOTTLE_CHILD'):
- print "Shutting down..."
+ except (SyntaxError, ImportError):
+ if not reloader: raise
+ if not getattr(server, 'quiet', False): print_exc()
+ sys.exit(3)
+ finally:
+ if not getattr(server, 'quiet', False): stderr('Shutdown...\n')
class FileCheckerThread(threading.Thread):
- ''' Thread that periodically checks for changed module files. '''
+ ''' Interrupt main-thread as soon as a changed module file is detected,
+ the lockfile gets deleted or gets to old. '''
def __init__(self, lockfile, interval):
threading.Thread.__init__(self)
self.lockfile, self.interval = lockfile, interval
- #1: lockfile to old; 2: lockfile missing
- #3: module file changed; 5: external exit
- self.status = 0
+ #: Is one of 'reload', 'error' or 'exit'
+ self.status = None
def run(self):
exists = os.path.exists
mtime = lambda path: os.stat(path).st_mtime
files = dict()
+
for module in sys.modules.values():
path = getattr(module, '__file__', '')
if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
if path and exists(path): files[path] = mtime(path)
+
while not self.status:
+ if not exists(self.lockfile)\
+ or mtime(self.lockfile) < time.time() - self.interval - 5:
+ self.status = 'error'
+ thread.interrupt_main()
for path, lmtime in files.iteritems():
if not exists(path) or mtime(path) > lmtime:
- self.status = 3
- if not exists(self.lockfile):
- self.status = 2
- elif mtime(self.lockfile) < time.time() - self.interval - 5:
- self.status = 1
- if not self.status:
- time.sleep(self.interval)
- if self.status != 5:
- thread.interrupt_main()
-
-
-def _reloader_child(server, app, interval):
- ''' Start the server and check for modified files in a background thread.
- As soon as an update is detected, KeyboardInterrupt is thrown in
- the main thread to exit the server loop. The process exists with status
- code 3 to request a reload by the observer process. If the lockfile
- is not modified in 2*interval second or missing, we assume that the
- observer process died and exit with status code 1 or 2.
- '''
- lockfile = os.environ.get('BOTTLE_LOCKFILE')
- bgcheck = FileCheckerThread(lockfile, interval)
- try:
- bgcheck.start()
- server.run(app)
- except KeyboardInterrupt:
- pass
- bgcheck.status, status = 5, bgcheck.status
- bgcheck.join() # bgcheck.status == 5 --> silent exit
- if status: sys.exit(status)
-
-
-def _reloader_observer(server, app, interval):
- ''' Start a child process with identical commandline arguments and restart
- it as long as it exists with status code 3. Also create a lockfile and
- touch it (update mtime) every interval seconds.
- '''
- fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock')
- os.close(fd) # We only need this file to exist. We never write to it
- try:
- while os.path.exists(lockfile):
- args = [sys.executable] + sys.argv
- environ = os.environ.copy()
- environ['BOTTLE_CHILD'] = 'true'
- environ['BOTTLE_LOCKFILE'] = lockfile
- p = subprocess.Popen(args, env=environ)
- while p.poll() is None: # Busy wait...
- os.utime(lockfile, None) # I am alive!
- time.sleep(interval)
- if p.poll() != 3:
- if os.path.exists(lockfile): os.unlink(lockfile)
- sys.exit(p.poll())
- elif not server.quiet:
- print "Reloading server..."
- except KeyboardInterrupt:
- pass
- if os.path.exists(lockfile): os.unlink(lockfile)
-
+ self.status = 'reload'
+ thread.interrupt_main()
+ break
+ time.sleep(self.interval)
+
+ def __enter__(self):
+ self.start()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if not self.status: self.status = 'exit' # silent exit
+ self.join()
+ return issubclass(exc_type, KeyboardInterrupt)
@@ -2081,7 +2451,7 @@ class TemplateError(HTTPError):
class BaseTemplate(object):
""" Base class and minimal API for template adapters """
- extentions = ['tpl','html','thtml','stpl']
+ extensions = ['tpl','html','thtml','stpl']
settings = {} #used in prepare()
defaults = {} #used in render()
@@ -2120,7 +2490,7 @@ class BaseTemplate(object):
fname = os.path.join(spath, name)
if os.path.isfile(fname):
return fname
- for ext in cls.extentions:
+ for ext in cls.extensions:
if os.path.isfile('%s.%s' % (fname, ext)):
return '%s.%s' % (fname, ext)
@@ -2128,6 +2498,7 @@ class BaseTemplate(object):
def global_config(cls, key, *args):
''' This reads or sets the global settings stored in class.settings. '''
if args:
+ cls.settings = cls.settings.copy() # Make settings local to class
cls.settings[key] = args[0]
else:
return cls.settings[key]
@@ -2185,7 +2556,7 @@ class CheetahTemplate(BaseTemplate):
self.context.vars.update(kwargs)
out = str(self.tpl)
self.context.vars.clear()
- return [out]
+ return out
class Jinja2Template(BaseTemplate):
@@ -2206,7 +2577,7 @@ class Jinja2Template(BaseTemplate):
for dictarg in args: kwargs.update(dictarg)
_defaults = self.defaults.copy()
_defaults.update(kwargs)
- return self.tpl.render(**_defaults).encode("utf-8")
+ return self.tpl.render(**_defaults)
def loader(self, name):
fname = self.search(name, self.lookup)
@@ -2228,7 +2599,6 @@ class SimpleTALTemplate(BaseTemplate):
def render(self, *args, **kwargs):
from simpletal import simpleTALES
- from StringIO import StringIO
for dictarg in args: kwargs.update(dictarg)
# TODO: maybe reuse a context instead of always creating one
context = simpleTALES.Context()
@@ -2242,7 +2612,8 @@ class SimpleTALTemplate(BaseTemplate):
class SimpleTemplate(BaseTemplate):
- blocks = ('if','elif','else','try','except','finally','for','while','with','def','class')
+ blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while',
+ 'with', 'def', 'class')
dedent_blocks = ('elif', 'else', 'except', 'finally')
@lazy_attribute
@@ -2258,7 +2629,7 @@ class SimpleTemplate(BaseTemplate):
|\#.* # Comments
)''', re.VERBOSE)
- def prepare(self, escape_func=cgi.escape, noescape=False):
+ def prepare(self, escape_func=html_escape, noescape=False, **kwargs):
self.cache = {}
enc = self.encoding
self._str = lambda x: touni(x, enc)
@@ -2285,7 +2656,7 @@ class SimpleTemplate(BaseTemplate):
ptrbuffer = [] # Buffer for printable strings and token tuple instances
codebuffer = [] # Buffer for generated python code
multiline = dedent = oneline = False
- template = self.source if self.source else open(self.filename).read()
+ template = self.source or open(self.filename, 'rb').read()
def yield_tokens(line):
for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)):
@@ -2327,7 +2698,7 @@ class SimpleTemplate(BaseTemplate):
line = line.split('%',1)[1].lstrip() # Full line following the %
cline = self.split_comment(line).strip()
cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0]
- flush() ##encodig (TODO: why?)
+ flush() # You are actually reading this? Good luck, it's a mess :)
if cmd in self.blocks or multiline:
cmd = multiline or cmd
dedent = cmd in self.dedent_blocks # "else:"
@@ -2374,15 +2745,15 @@ class SimpleTemplate(BaseTemplate):
env = self.defaults.copy()
env.update({'_stdout': _stdout, '_printlist': _stdout.extend,
'_include': self.subtemplate, '_str': self._str,
- '_escape': self._escape})
+ '_escape': self._escape, 'get': env.get,
+ 'setdefault': env.setdefault, 'defined': env.__contains__})
env.update(kwargs)
eval(self.co, env)
if '_rebase' in env:
subtpl, rargs = env['_rebase']
- subtpl = self.__class__(name=subtpl, lookup=self.lookup)
rargs['_base'] = _stdout[:] #copy stdout
del _stdout[:] # clear stdout
- return subtpl.execute(_stdout, rargs)
+ return self.subtemplate(subtpl,_stdout,rargs)
return env
def render(self, *args, **kwargs):
@@ -2463,11 +2834,16 @@ simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate)
TEMPLATE_PATH = ['./', './views/']
TEMPLATES = {}
DEBUG = False
-MEMFILE_MAX = 1024*100
+NORUN = False # If set, run() does nothing. Used by load_app()
#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
HTTP_CODES = httplib.responses
HTTP_CODES[418] = "I'm a teapot" # RFC 2324
+HTTP_CODES[428] = "Precondition Required"
+HTTP_CODES[429] = "Too Many Requests"
+HTTP_CODES[431] = "Request Header Fields Too Large"
+HTTP_CODES[511] = "Network Authentication Required"
+_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.iteritems())
#: The default template used for error pages. Override with @error()
ERROR_PAGE_TEMPLATE = """
@@ -2480,13 +2856,15 @@ ERROR_PAGE_TEMPLATE = """
<title>Error {{e.status}}: {{status_name}}</title>
<style type="text/css">
html {background-color: #eee; font-family: sans;}
- body {background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 15px;}
+ body {background-color: #fff; border: 1px solid #ddd;
+ padding: 15px; margin: 15px;}
pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
</style>
</head>
<body>
<h1>Error {{e.status}}: {{status_name}}</h1>
- <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> caused an error:</p>
+ <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt>
+ caused an error:</p>
<pre>{{e.output}}</pre>
%if DEBUG and e.exception:
<h2>Exception:</h2>
@@ -2499,17 +2877,18 @@ ERROR_PAGE_TEMPLATE = """
</body>
</html>
%except ImportError:
- <b>ImportError:</b> Could not generate the error page. Please add bottle to sys.path
+ <b>ImportError:</b> Could not generate the error page. Please add bottle to
+ the import path.
%end
"""
-#: A thread-save instance of :class:`Request` representing the `current` request.
+#: A thread-safe instance of :class:`Request` representing the `current` request.
request = Request()
-#: A thread-save instance of :class:`Response` used to build the HTTP response.
+#: A thread-safe instance of :class:`Response` used to build the HTTP response.
response = Response()
-#: A thread-save namepsace. Not used by Bottle.
+#: A thread-safe namespace. Not used by Bottle.
local = threading.local()
# Initialize app stack (create first empty Bottle app)
@@ -2520,3 +2899,28 @@ app.push()
#: A virtual package that redirects import statements.
#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module
+
+if __name__ == '__main__':
+ opt, args, parser = _cmd_options, _cmd_args, _cmd_parser
+ if opt.version:
+ print 'Bottle', __version__; sys.exit(0)
+ if not args:
+ parser.print_help()
+ print '\nError: No application specified.\n'
+ sys.exit(1)
+
+ try:
+ sys.path.insert(0, '.')
+ sys.modules.setdefault('bottle', sys.modules['__main__'])
+ except (AttributeError, ImportError), e:
+ parser.error(e.args[0])
+
+ if opt.bind and ':' in opt.bind:
+ host, port = opt.bind.rsplit(':', 1)
+ else:
+ host, port = (opt.bind or 'localhost'), 8080
+
+ debug(opt.debug)
+ run(args[0], host=host, port=port, server=opt.server, reloader=opt.reload, plugins=opt.plugin)
+
+# THE END
diff --git a/module/remote/thriftbackend/pyload.thrift b/module/remote/thriftbackend/pyload.thrift
index 5d828854c..1542e651a 100644
--- a/module/remote/thriftbackend/pyload.thrift
+++ b/module/remote/thriftbackend/pyload.thrift
@@ -4,6 +4,7 @@ typedef i32 FileID
typedef i32 PackageID
typedef i32 TaskID
typedef i32 ResultID
+typedef i32 InteractionID
typedef list<string> LinkList
typedef string PluginName
typedef byte Progress
@@ -38,6 +39,31 @@ enum ElementType {
File
}
+// types for user interaction
+// some may only be place holder currently not supported
+// also all input - output combination are not reasonable, see InteractionManager for further info
+enum Input {
+ NONE,
+ TEXT,
+ TEXTBOX,
+ PASSWORD,
+ BOOL, // confirm like, yes or no dialog
+ CLICK, // for positional captchas
+ CHOICE, // choice from list
+ MULTIPLE, // multiple choice from list of elements
+ LIST, // arbitary list of elements
+ TABLE // table like data structure
+}
+// more can be implemented by need
+
+// this describes the type of the outgoing interaction
+// ensure they can be logcial or'ed
+enum Output {
+ CAPTCHA = 1,
+ QUESTION = 2,
+ NOTIFICATION = 4,
+}
+
struct DownloadInfo {
1: FileID fid,
2: string name,
@@ -111,6 +137,18 @@ struct PackageData {
13: optional list<FileID> fids
}
+struct InteractionTask {
+ 1: InteractionID iid,
+ 2: Input input,
+ 3: list<string> structure,
+ 4: list<string> preset,
+ 5: Output output,
+ 6: list<string> data,
+ 7: string title,
+ 8: string description,
+ 9: string plugin,
+}
+
struct CaptchaTask {
1: i16 tid,
2: binary data,
@@ -257,13 +295,6 @@ service Pyload {
list<PackageID> deleteFinished(),
void restartFailed(),
-
- //captcha
- bool isCaptchaWaiting(),
- CaptchaTask getCaptchaTask(1: bool exclusive),
- string getCaptchaTaskStatus(1: TaskID tid),
- void setCaptchaResult(1: TaskID tid, 2: string result),
-
//events
list<EventInfo> getEvents(1: string uuid)
@@ -289,8 +320,18 @@ service Pyload {
//info
// {plugin: {name: value}}
map<PluginName, map<string,string>> getAllInfo(),
- map<string, string> getInfoByPlugin(1: PluginName plugin)
+ map<string, string> getInfoByPlugin(1: PluginName plugin),
//scheduler
+ // TODO
+
+
+ // User interaction
+
+ //captcha
+ bool isCaptchaWaiting(),
+ CaptchaTask getCaptchaTask(1: bool exclusive),
+ string getCaptchaTaskStatus(1: TaskID tid),
+ void setCaptchaResult(1: TaskID tid, 2: string result),
}
diff --git a/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote b/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote
index 854b1589e..bfaf5b078 100755
--- a/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote
+++ b/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote
@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
-# Autogenerated by Thrift Compiler (0.8.0-dev)
+# Autogenerated by Thrift Compiler (0.9.0-dev)
#
# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
#
@@ -76,10 +76,6 @@ if len(sys.argv) <= 1 or sys.argv[1] == '--help':
print ' void setPackageData(PackageID pid, data)'
print ' deleteFinished()'
print ' void restartFailed()'
- print ' bool isCaptchaWaiting()'
- print ' CaptchaTask getCaptchaTask(bool exclusive)'
- print ' string getCaptchaTaskStatus(TaskID tid)'
- print ' void setCaptchaResult(TaskID tid, string result)'
print ' getEvents(string uuid)'
print ' getAccounts(bool refresh)'
print ' getAccountTypes()'
@@ -93,6 +89,10 @@ if len(sys.argv) <= 1 or sys.argv[1] == '--help':
print ' string call(ServiceCall info)'
print ' getAllInfo()'
print ' getInfoByPlugin(PluginName plugin)'
+ print ' bool isCaptchaWaiting()'
+ print ' CaptchaTask getCaptchaTask(bool exclusive)'
+ print ' string getCaptchaTaskStatus(TaskID tid)'
+ print ' void setCaptchaResult(TaskID tid, string result)'
print ''
sys.exit(0)
@@ -462,30 +462,6 @@ elif cmd == 'restartFailed':
sys.exit(1)
pp.pprint(client.restartFailed())
-elif cmd == 'isCaptchaWaiting':
- if len(args) != 0:
- print 'isCaptchaWaiting requires 0 args'
- sys.exit(1)
- pp.pprint(client.isCaptchaWaiting())
-
-elif cmd == 'getCaptchaTask':
- if len(args) != 1:
- print 'getCaptchaTask requires 1 args'
- sys.exit(1)
- pp.pprint(client.getCaptchaTask(eval(args[0]),))
-
-elif cmd == 'getCaptchaTaskStatus':
- if len(args) != 1:
- print 'getCaptchaTaskStatus requires 1 args'
- sys.exit(1)
- pp.pprint(client.getCaptchaTaskStatus(eval(args[0]),))
-
-elif cmd == 'setCaptchaResult':
- if len(args) != 2:
- print 'setCaptchaResult requires 2 args'
- sys.exit(1)
- pp.pprint(client.setCaptchaResult(eval(args[0]),args[1],))
-
elif cmd == 'getEvents':
if len(args) != 1:
print 'getEvents requires 1 args'
@@ -564,6 +540,30 @@ elif cmd == 'getInfoByPlugin':
sys.exit(1)
pp.pprint(client.getInfoByPlugin(eval(args[0]),))
+elif cmd == 'isCaptchaWaiting':
+ if len(args) != 0:
+ print 'isCaptchaWaiting requires 0 args'
+ sys.exit(1)
+ pp.pprint(client.isCaptchaWaiting())
+
+elif cmd == 'getCaptchaTask':
+ if len(args) != 1:
+ print 'getCaptchaTask requires 1 args'
+ sys.exit(1)
+ pp.pprint(client.getCaptchaTask(eval(args[0]),))
+
+elif cmd == 'getCaptchaTaskStatus':
+ if len(args) != 1:
+ print 'getCaptchaTaskStatus requires 1 args'
+ sys.exit(1)
+ pp.pprint(client.getCaptchaTaskStatus(eval(args[0]),))
+
+elif cmd == 'setCaptchaResult':
+ if len(args) != 2:
+ print 'setCaptchaResult requires 2 args'
+ sys.exit(1)
+ pp.pprint(client.setCaptchaResult(eval(args[0]),args[1],))
+
else:
print 'Unrecognized method %s' % cmd
sys.exit(1)
diff --git a/module/remote/thriftbackend/thriftgen/pyload/Pyload.py b/module/remote/thriftbackend/thriftgen/pyload/Pyload.py
index a1bc63f75..78a42f16a 100644
--- a/module/remote/thriftbackend/thriftgen/pyload/Pyload.py
+++ b/module/remote/thriftbackend/thriftgen/pyload/Pyload.py
@@ -1,5 +1,5 @@
#
-# Autogenerated by Thrift Compiler (0.8.0-dev)
+# Autogenerated by Thrift Compiler (0.9.0-dev)
#
# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
#
@@ -9,7 +9,7 @@
from thrift.Thrift import TType, TMessageType, TException
from ttypes import *
from thrift.Thrift import TProcessor
-from thrift.protocol.TBase import TBase, TExceptionBase, TApplicationException
+from thrift.protocol.TBase import TBase, TExceptionBase
class Iface(object):
@@ -319,31 +319,6 @@ class Iface(object):
def restartFailed(self, ):
pass
- def isCaptchaWaiting(self, ):
- pass
-
- def getCaptchaTask(self, exclusive):
- """
- Parameters:
- - exclusive
- """
- pass
-
- def getCaptchaTaskStatus(self, tid):
- """
- Parameters:
- - tid
- """
- pass
-
- def setCaptchaResult(self, tid, result):
- """
- Parameters:
- - tid
- - result
- """
- pass
-
def getEvents(self, uuid):
"""
Parameters:
@@ -426,6 +401,31 @@ class Iface(object):
"""
pass
+ def isCaptchaWaiting(self, ):
+ pass
+
+ def getCaptchaTask(self, exclusive):
+ """
+ Parameters:
+ - exclusive
+ """
+ pass
+
+ def getCaptchaTaskStatus(self, tid):
+ """
+ Parameters:
+ - tid
+ """
+ pass
+
+ def setCaptchaResult(self, tid, result):
+ """
+ Parameters:
+ - tid
+ - result
+ """
+ pass
+
class Client(Iface):
def __init__(self, iprot, oprot=None):
@@ -1919,121 +1919,6 @@ class Client(Iface):
self._iprot.readMessageEnd()
return
- def isCaptchaWaiting(self, ):
- self.send_isCaptchaWaiting()
- return self.recv_isCaptchaWaiting()
-
- def send_isCaptchaWaiting(self, ):
- self._oprot.writeMessageBegin('isCaptchaWaiting', TMessageType.CALL, self._seqid)
- args = isCaptchaWaiting_args()
- args.write(self._oprot)
- self._oprot.writeMessageEnd()
- self._oprot.trans.flush()
-
- def recv_isCaptchaWaiting(self, ):
- (fname, mtype, rseqid) = self._iprot.readMessageBegin()
- if mtype == TMessageType.EXCEPTION:
- x = TApplicationException()
- x.read(self._iprot)
- self._iprot.readMessageEnd()
- raise x
- result = isCaptchaWaiting_result()
- result.read(self._iprot)
- self._iprot.readMessageEnd()
- if result.success is not None:
- return result.success
- raise TApplicationException(TApplicationException.MISSING_RESULT, "isCaptchaWaiting failed: unknown result");
-
- def getCaptchaTask(self, exclusive):
- """
- Parameters:
- - exclusive
- """
- self.send_getCaptchaTask(exclusive)
- return self.recv_getCaptchaTask()
-
- def send_getCaptchaTask(self, exclusive):
- self._oprot.writeMessageBegin('getCaptchaTask', TMessageType.CALL, self._seqid)
- args = getCaptchaTask_args()
- args.exclusive = exclusive
- args.write(self._oprot)
- self._oprot.writeMessageEnd()
- self._oprot.trans.flush()
-
- def recv_getCaptchaTask(self, ):
- (fname, mtype, rseqid) = self._iprot.readMessageBegin()
- if mtype == TMessageType.EXCEPTION:
- x = TApplicationException()
- x.read(self._iprot)
- self._iprot.readMessageEnd()
- raise x
- result = getCaptchaTask_result()
- result.read(self._iprot)
- self._iprot.readMessageEnd()
- if result.success is not None:
- return result.success
- raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTask failed: unknown result");
-
- def getCaptchaTaskStatus(self, tid):
- """
- Parameters:
- - tid
- """
- self.send_getCaptchaTaskStatus(tid)
- return self.recv_getCaptchaTaskStatus()
-
- def send_getCaptchaTaskStatus(self, tid):
- self._oprot.writeMessageBegin('getCaptchaTaskStatus', TMessageType.CALL, self._seqid)
- args = getCaptchaTaskStatus_args()
- args.tid = tid
- args.write(self._oprot)
- self._oprot.writeMessageEnd()
- self._oprot.trans.flush()
-
- def recv_getCaptchaTaskStatus(self, ):
- (fname, mtype, rseqid) = self._iprot.readMessageBegin()
- if mtype == TMessageType.EXCEPTION:
- x = TApplicationException()
- x.read(self._iprot)
- self._iprot.readMessageEnd()
- raise x
- result = getCaptchaTaskStatus_result()
- result.read(self._iprot)
- self._iprot.readMessageEnd()
- if result.success is not None:
- return result.success
- raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTaskStatus failed: unknown result");
-
- def setCaptchaResult(self, tid, result):
- """
- Parameters:
- - tid
- - result
- """
- self.send_setCaptchaResult(tid, result)
- self.recv_setCaptchaResult()
-
- def send_setCaptchaResult(self, tid, result):
- self._oprot.writeMessageBegin('setCaptchaResult', TMessageType.CALL, self._seqid)
- args = setCaptchaResult_args()
- args.tid = tid
- args.result = result
- args.write(self._oprot)
- self._oprot.writeMessageEnd()
- self._oprot.trans.flush()
-
- def recv_setCaptchaResult(self, ):
- (fname, mtype, rseqid) = self._iprot.readMessageBegin()
- if mtype == TMessageType.EXCEPTION:
- x = TApplicationException()
- x.read(self._iprot)
- self._iprot.readMessageEnd()
- raise x
- result = setCaptchaResult_result()
- result.read(self._iprot)
- self._iprot.readMessageEnd()
- return
-
def getEvents(self, uuid):
"""
Parameters:
@@ -2418,6 +2303,121 @@ class Client(Iface):
return result.success
raise TApplicationException(TApplicationException.MISSING_RESULT, "getInfoByPlugin failed: unknown result");
+ def isCaptchaWaiting(self, ):
+ self.send_isCaptchaWaiting()
+ return self.recv_isCaptchaWaiting()
+
+ def send_isCaptchaWaiting(self, ):
+ self._oprot.writeMessageBegin('isCaptchaWaiting', TMessageType.CALL, self._seqid)
+ args = isCaptchaWaiting_args()
+ args.write(self._oprot)
+ self._oprot.writeMessageEnd()
+ self._oprot.trans.flush()
+
+ def recv_isCaptchaWaiting(self, ):
+ (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+ if mtype == TMessageType.EXCEPTION:
+ x = TApplicationException()
+ x.read(self._iprot)
+ self._iprot.readMessageEnd()
+ raise x
+ result = isCaptchaWaiting_result()
+ result.read(self._iprot)
+ self._iprot.readMessageEnd()
+ if result.success is not None:
+ return result.success
+ raise TApplicationException(TApplicationException.MISSING_RESULT, "isCaptchaWaiting failed: unknown result");
+
+ def getCaptchaTask(self, exclusive):
+ """
+ Parameters:
+ - exclusive
+ """
+ self.send_getCaptchaTask(exclusive)
+ return self.recv_getCaptchaTask()
+
+ def send_getCaptchaTask(self, exclusive):
+ self._oprot.writeMessageBegin('getCaptchaTask', TMessageType.CALL, self._seqid)
+ args = getCaptchaTask_args()
+ args.exclusive = exclusive
+ args.write(self._oprot)
+ self._oprot.writeMessageEnd()
+ self._oprot.trans.flush()
+
+ def recv_getCaptchaTask(self, ):
+ (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+ if mtype == TMessageType.EXCEPTION:
+ x = TApplicationException()
+ x.read(self._iprot)
+ self._iprot.readMessageEnd()
+ raise x
+ result = getCaptchaTask_result()
+ result.read(self._iprot)
+ self._iprot.readMessageEnd()
+ if result.success is not None:
+ return result.success
+ raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTask failed: unknown result");
+
+ def getCaptchaTaskStatus(self, tid):
+ """
+ Parameters:
+ - tid
+ """
+ self.send_getCaptchaTaskStatus(tid)
+ return self.recv_getCaptchaTaskStatus()
+
+ def send_getCaptchaTaskStatus(self, tid):
+ self._oprot.writeMessageBegin('getCaptchaTaskStatus', TMessageType.CALL, self._seqid)
+ args = getCaptchaTaskStatus_args()
+ args.tid = tid
+ args.write(self._oprot)
+ self._oprot.writeMessageEnd()
+ self._oprot.trans.flush()
+
+ def recv_getCaptchaTaskStatus(self, ):
+ (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+ if mtype == TMessageType.EXCEPTION:
+ x = TApplicationException()
+ x.read(self._iprot)
+ self._iprot.readMessageEnd()
+ raise x
+ result = getCaptchaTaskStatus_result()
+ result.read(self._iprot)
+ self._iprot.readMessageEnd()
+ if result.success is not None:
+ return result.success
+ raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTaskStatus failed: unknown result");
+
+ def setCaptchaResult(self, tid, result):
+ """
+ Parameters:
+ - tid
+ - result
+ """
+ self.send_setCaptchaResult(tid, result)
+ self.recv_setCaptchaResult()
+
+ def send_setCaptchaResult(self, tid, result):
+ self._oprot.writeMessageBegin('setCaptchaResult', TMessageType.CALL, self._seqid)
+ args = setCaptchaResult_args()
+ args.tid = tid
+ args.result = result
+ args.write(self._oprot)
+ self._oprot.writeMessageEnd()
+ self._oprot.trans.flush()
+
+ def recv_setCaptchaResult(self, ):
+ (fname, mtype, rseqid) = self._iprot.readMessageBegin()
+ if mtype == TMessageType.EXCEPTION:
+ x = TApplicationException()
+ x.read(self._iprot)
+ self._iprot.readMessageEnd()
+ raise x
+ result = setCaptchaResult_result()
+ result.read(self._iprot)
+ self._iprot.readMessageEnd()
+ return
+
class Processor(Iface, TProcessor):
def __init__(self, handler):
@@ -2476,10 +2476,6 @@ class Processor(Iface, TProcessor):
self._processMap["setPackageData"] = Processor.process_setPackageData
self._processMap["deleteFinished"] = Processor.process_deleteFinished
self._processMap["restartFailed"] = Processor.process_restartFailed
- self._processMap["isCaptchaWaiting"] = Processor.process_isCaptchaWaiting
- self._processMap["getCaptchaTask"] = Processor.process_getCaptchaTask
- self._processMap["getCaptchaTaskStatus"] = Processor.process_getCaptchaTaskStatus
- self._processMap["setCaptchaResult"] = Processor.process_setCaptchaResult
self._processMap["getEvents"] = Processor.process_getEvents
self._processMap["getAccounts"] = Processor.process_getAccounts
self._processMap["getAccountTypes"] = Processor.process_getAccountTypes
@@ -2493,6 +2489,10 @@ class Processor(Iface, TProcessor):
self._processMap["call"] = Processor.process_call
self._processMap["getAllInfo"] = Processor.process_getAllInfo
self._processMap["getInfoByPlugin"] = Processor.process_getInfoByPlugin
+ self._processMap["isCaptchaWaiting"] = Processor.process_isCaptchaWaiting
+ self._processMap["getCaptchaTask"] = Processor.process_getCaptchaTask
+ self._processMap["getCaptchaTaskStatus"] = Processor.process_getCaptchaTaskStatus
+ self._processMap["setCaptchaResult"] = Processor.process_setCaptchaResult
def process(self, iprot, oprot):
(name, type, seqid) = iprot.readMessageBegin()
@@ -3104,50 +3104,6 @@ class Processor(Iface, TProcessor):
oprot.writeMessageEnd()
oprot.trans.flush()
- def process_isCaptchaWaiting(self, seqid, iprot, oprot):
- args = isCaptchaWaiting_args()
- args.read(iprot)
- iprot.readMessageEnd()
- result = isCaptchaWaiting_result()
- result.success = self._handler.isCaptchaWaiting()
- oprot.writeMessageBegin("isCaptchaWaiting", TMessageType.REPLY, seqid)
- result.write(oprot)
- oprot.writeMessageEnd()
- oprot.trans.flush()
-
- def process_getCaptchaTask(self, seqid, iprot, oprot):
- args = getCaptchaTask_args()
- args.read(iprot)
- iprot.readMessageEnd()
- result = getCaptchaTask_result()
- result.success = self._handler.getCaptchaTask(args.exclusive)
- oprot.writeMessageBegin("getCaptchaTask", TMessageType.REPLY, seqid)
- result.write(oprot)
- oprot.writeMessageEnd()
- oprot.trans.flush()
-
- def process_getCaptchaTaskStatus(self, seqid, iprot, oprot):
- args = getCaptchaTaskStatus_args()
- args.read(iprot)
- iprot.readMessageEnd()
- result = getCaptchaTaskStatus_result()
- result.success = self._handler.getCaptchaTaskStatus(args.tid)
- oprot.writeMessageBegin("getCaptchaTaskStatus", TMessageType.REPLY, seqid)
- result.write(oprot)
- oprot.writeMessageEnd()
- oprot.trans.flush()
-
- def process_setCaptchaResult(self, seqid, iprot, oprot):
- args = setCaptchaResult_args()
- args.read(iprot)
- iprot.readMessageEnd()
- result = setCaptchaResult_result()
- self._handler.setCaptchaResult(args.tid, args.result)
- oprot.writeMessageBegin("setCaptchaResult", TMessageType.REPLY, seqid)
- result.write(oprot)
- oprot.writeMessageEnd()
- oprot.trans.flush()
-
def process_getEvents(self, seqid, iprot, oprot):
args = getEvents_args()
args.read(iprot)
@@ -3296,6 +3252,50 @@ class Processor(Iface, TProcessor):
oprot.writeMessageEnd()
oprot.trans.flush()
+ def process_isCaptchaWaiting(self, seqid, iprot, oprot):
+ args = isCaptchaWaiting_args()
+ args.read(iprot)
+ iprot.readMessageEnd()
+ result = isCaptchaWaiting_result()
+ result.success = self._handler.isCaptchaWaiting()
+ oprot.writeMessageBegin("isCaptchaWaiting", TMessageType.REPLY, seqid)
+ result.write(oprot)
+ oprot.writeMessageEnd()
+ oprot.trans.flush()
+
+ def process_getCaptchaTask(self, seqid, iprot, oprot):
+ args = getCaptchaTask_args()
+ args.read(iprot)
+ iprot.readMessageEnd()
+ result = getCaptchaTask_result()
+ result.success = self._handler.getCaptchaTask(args.exclusive)
+ oprot.writeMessageBegin("getCaptchaTask", TMessageType.REPLY, seqid)
+ result.write(oprot)
+ oprot.writeMessageEnd()
+ oprot.trans.flush()
+
+ def process_getCaptchaTaskStatus(self, seqid, iprot, oprot):
+ args = getCaptchaTaskStatus_args()
+ args.read(iprot)
+ iprot.readMessageEnd()
+ result = getCaptchaTaskStatus_result()
+ result.success = self._handler.getCaptchaTaskStatus(args.tid)
+ oprot.writeMessageBegin("getCaptchaTaskStatus", TMessageType.REPLY, seqid)
+ result.write(oprot)
+ oprot.writeMessageEnd()
+ oprot.trans.flush()
+
+ def process_setCaptchaResult(self, seqid, iprot, oprot):
+ args = setCaptchaResult_args()
+ args.read(iprot)
+ iprot.readMessageEnd()
+ result = setCaptchaResult_result()
+ self._handler.setCaptchaResult(args.tid, args.result)
+ oprot.writeMessageBegin("setCaptchaResult", TMessageType.REPLY, seqid)
+ result.write(oprot)
+ oprot.writeMessageEnd()
+ oprot.trans.flush()
+
# HELPER FUNCTIONS AND STRUCTURES
@@ -4941,139 +4941,6 @@ class restartFailed_result(TBase):
)
-class isCaptchaWaiting_args(TBase):
-
- __slots__ = [
- ]
-
- thrift_spec = (
- )
-
-
-class isCaptchaWaiting_result(TBase):
- """
- Attributes:
- - success
- """
-
- __slots__ = [
- 'success',
- ]
-
- thrift_spec = (
- (0, TType.BOOL, 'success', None, None, ), # 0
- )
-
- def __init__(self, success=None,):
- self.success = success
-
-
-class getCaptchaTask_args(TBase):
- """
- Attributes:
- - exclusive
- """
-
- __slots__ = [
- 'exclusive',
- ]
-
- thrift_spec = (
- None, # 0
- (1, TType.BOOL, 'exclusive', None, None, ), # 1
- )
-
- def __init__(self, exclusive=None,):
- self.exclusive = exclusive
-
-
-class getCaptchaTask_result(TBase):
- """
- Attributes:
- - success
- """
-
- __slots__ = [
- 'success',
- ]
-
- thrift_spec = (
- (0, TType.STRUCT, 'success', (CaptchaTask, CaptchaTask.thrift_spec), None, ), # 0
- )
-
- def __init__(self, success=None,):
- self.success = success
-
-
-class getCaptchaTaskStatus_args(TBase):
- """
- Attributes:
- - tid
- """
-
- __slots__ = [
- 'tid',
- ]
-
- thrift_spec = (
- None, # 0
- (1, TType.I32, 'tid', None, None, ), # 1
- )
-
- def __init__(self, tid=None,):
- self.tid = tid
-
-
-class getCaptchaTaskStatus_result(TBase):
- """
- Attributes:
- - success
- """
-
- __slots__ = [
- 'success',
- ]
-
- thrift_spec = (
- (0, TType.STRING, 'success', None, None, ), # 0
- )
-
- def __init__(self, success=None,):
- self.success = success
-
-
-class setCaptchaResult_args(TBase):
- """
- Attributes:
- - tid
- - result
- """
-
- __slots__ = [
- 'tid',
- 'result',
- ]
-
- thrift_spec = (
- None, # 0
- (1, TType.I32, 'tid', None, None, ), # 1
- (2, TType.STRING, 'result', None, None, ), # 2
- )
-
- def __init__(self, tid=None, result=None,):
- self.tid = tid
- self.result = result
-
-
-class setCaptchaResult_result(TBase):
-
- __slots__ = [
- ]
-
- thrift_spec = (
- )
-
-
class getEvents_args(TBase):
"""
Attributes:
@@ -5532,3 +5399,136 @@ class getInfoByPlugin_result(TBase):
def __init__(self, success=None,):
self.success = success
+
+class isCaptchaWaiting_args(TBase):
+
+ __slots__ = [
+ ]
+
+ thrift_spec = (
+ )
+
+
+class isCaptchaWaiting_result(TBase):
+ """
+ Attributes:
+ - success
+ """
+
+ __slots__ = [
+ 'success',
+ ]
+
+ thrift_spec = (
+ (0, TType.BOOL, 'success', None, None, ), # 0
+ )
+
+ def __init__(self, success=None,):
+ self.success = success
+
+
+class getCaptchaTask_args(TBase):
+ """
+ Attributes:
+ - exclusive
+ """
+
+ __slots__ = [
+ 'exclusive',
+ ]
+
+ thrift_spec = (
+ None, # 0
+ (1, TType.BOOL, 'exclusive', None, None, ), # 1
+ )
+
+ def __init__(self, exclusive=None,):
+ self.exclusive = exclusive
+
+
+class getCaptchaTask_result(TBase):
+ """
+ Attributes:
+ - success
+ """
+
+ __slots__ = [
+ 'success',
+ ]
+
+ thrift_spec = (
+ (0, TType.STRUCT, 'success', (CaptchaTask, CaptchaTask.thrift_spec), None, ), # 0
+ )
+
+ def __init__(self, success=None,):
+ self.success = success
+
+
+class getCaptchaTaskStatus_args(TBase):
+ """
+ Attributes:
+ - tid
+ """
+
+ __slots__ = [
+ 'tid',
+ ]
+
+ thrift_spec = (
+ None, # 0
+ (1, TType.I32, 'tid', None, None, ), # 1
+ )
+
+ def __init__(self, tid=None,):
+ self.tid = tid
+
+
+class getCaptchaTaskStatus_result(TBase):
+ """
+ Attributes:
+ - success
+ """
+
+ __slots__ = [
+ 'success',
+ ]
+
+ thrift_spec = (
+ (0, TType.STRING, 'success', None, None, ), # 0
+ )
+
+ def __init__(self, success=None,):
+ self.success = success
+
+
+class setCaptchaResult_args(TBase):
+ """
+ Attributes:
+ - tid
+ - result
+ """
+
+ __slots__ = [
+ 'tid',
+ 'result',
+ ]
+
+ thrift_spec = (
+ None, # 0
+ (1, TType.I32, 'tid', None, None, ), # 1
+ (2, TType.STRING, 'result', None, None, ), # 2
+ )
+
+ def __init__(self, tid=None, result=None,):
+ self.tid = tid
+ self.result = result
+
+
+class setCaptchaResult_result(TBase):
+
+ __slots__ = [
+ ]
+
+ thrift_spec = (
+ )
+
diff --git a/module/remote/thriftbackend/thriftgen/pyload/constants.py b/module/remote/thriftbackend/thriftgen/pyload/constants.py
index f5ef663f1..f8960dc63 100644
--- a/module/remote/thriftbackend/thriftgen/pyload/constants.py
+++ b/module/remote/thriftbackend/thriftgen/pyload/constants.py
@@ -1,5 +1,5 @@
#
-# Autogenerated by Thrift Compiler (0.8.0-dev)
+# Autogenerated by Thrift Compiler (0.9.0-dev)
#
# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
#
diff --git a/module/remote/thriftbackend/thriftgen/pyload/ttypes.py b/module/remote/thriftbackend/thriftgen/pyload/ttypes.py
index 626bd1c29..1299b515d 100644
--- a/module/remote/thriftbackend/thriftgen/pyload/ttypes.py
+++ b/module/remote/thriftbackend/thriftgen/pyload/ttypes.py
@@ -1,5 +1,5 @@
#
-# Autogenerated by Thrift Compiler (0.8.0-dev)
+# Autogenerated by Thrift Compiler (0.9.0-dev)
#
# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
#
@@ -92,6 +92,61 @@ class ElementType(TBase):
"File": 1,
}
+class Input(TBase):
+ NONE = 0
+ TEXT = 1
+ TEXTBOX = 2
+ PASSWORD = 3
+ BOOL = 4
+ CLICK = 5
+ CHOICE = 6
+ MULTIPLE = 7
+ LIST = 8
+ TABLE = 9
+
+ _VALUES_TO_NAMES = {
+ 0: "NONE",
+ 1: "TEXT",
+ 2: "TEXTBOX",
+ 3: "PASSWORD",
+ 4: "BOOL",
+ 5: "CLICK",
+ 6: "CHOICE",
+ 7: "MULTIPLE",
+ 8: "LIST",
+ 9: "TABLE",
+ }
+
+ _NAMES_TO_VALUES = {
+ "NONE": 0,
+ "TEXT": 1,
+ "TEXTBOX": 2,
+ "PASSWORD": 3,
+ "BOOL": 4,
+ "CLICK": 5,
+ "CHOICE": 6,
+ "MULTIPLE": 7,
+ "LIST": 8,
+ "TABLE": 9,
+ }
+
+class Output(TBase):
+ CAPTCHA = 1
+ QUESTION = 2
+ NOTIFICATION = 4
+
+ _VALUES_TO_NAMES = {
+ 1: "CAPTCHA",
+ 2: "QUESTION",
+ 4: "NOTIFICATION",
+ }
+
+ _NAMES_TO_VALUES = {
+ "CAPTCHA": 1,
+ "QUESTION": 2,
+ "NOTIFICATION": 4,
+ }
+
class DownloadInfo(TBase):
"""
@@ -403,6 +458,57 @@ class PackageData(TBase):
self.fids = fids
+class InteractionTask(TBase):
+ """
+ Attributes:
+ - iid
+ - input
+ - structure
+ - preset
+ - output
+ - data
+ - title
+ - description
+ - plugin
+ """
+
+ __slots__ = [
+ 'iid',
+ 'input',
+ 'structure',
+ 'preset',
+ 'output',
+ 'data',
+ 'title',
+ 'description',
+ 'plugin',
+ ]
+
+ thrift_spec = (
+ None, # 0
+ (1, TType.I32, 'iid', None, None, ), # 1
+ (2, TType.I32, 'input', None, None, ), # 2
+ (3, TType.LIST, 'structure', (TType.STRING,None), None, ), # 3
+ (4, TType.LIST, 'preset', (TType.STRING,None), None, ), # 4
+ (5, TType.I32, 'output', None, None, ), # 5
+ (6, TType.LIST, 'data', (TType.STRING,None), None, ), # 6
+ (7, TType.STRING, 'title', None, None, ), # 7
+ (8, TType.STRING, 'description', None, None, ), # 8
+ (9, TType.STRING, 'plugin', None, None, ), # 9
+ )
+
+ def __init__(self, iid=None, input=None, structure=None, preset=None, output=None, data=None, title=None, description=None, plugin=None,):
+ self.iid = iid
+ self.input = input
+ self.structure = structure
+ self.preset = preset
+ self.output = output
+ self.data = data
+ self.title = title
+ self.description = description
+ self.plugin = plugin
+
+
class CaptchaTask(TBase):
"""
Attributes:
diff --git a/module/setup.py b/module/setup.py
index 4a1c59da6..85b33b1ee 100644
--- a/module/setup.py
+++ b/module/setup.py
@@ -17,7 +17,7 @@
@author: RaNaN
"""
from getpass import getpass
-import gettext
+import module.common.pylgettext as gettext
import os
from os import makedirs
from os.path import abspath
@@ -34,33 +34,37 @@ class Setup():
"""
pyLoads initial setup configuration assistent
"""
- def __init__(self, path, config):
+ def __init__(self, path, config):
self.path = path
self.config = config
-
def start(self):
-
langs = self.config.getMetaData("general", "language")["type"].split(";")
lang = self.ask(u"Choose your Language / Wähle deine Sprache", "en", langs)
- translation = gettext.translation("setup", join(self.path, "locale"), languages=["en", lang])
+ gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])
+ translation = gettext.translation("setup", join(self.path, "locale"), languages=[lang, "en"], fallback=True)
translation.install(True)
-# print ""
-# print _("Would you like to configure pyLoad via Webinterface?")
-# print _("You need a Browser and a connection to this PC for it.")
-# viaweb = self.ask(_("Start initial webinterface for configuration?"), "y", bool=True)
-# if viaweb:
-# try:
-# from module.web import ServerThread
-# ServerThread.setup = self
-# from module.web import webinterface
-# webinterface.run_simple()
-# return False
-# except Exception, e:
-# print "Setup failed with this error: ", e
-# print "Falling back to commandline setup."
+ #Input shorthand for yes
+ self.yes = _("y")
+ #Input shorthand for no
+ self.no = _("n")
+
+ # print ""
+ # print _("Would you like to configure pyLoad via Webinterface?")
+ # print _("You need a Browser and a connection to this PC for it.")
+ # viaweb = self.ask(_("Start initial webinterface for configuration?"), "y", bool=True)
+ # if viaweb:
+ # try:
+ # from module.web import ServerThread
+ # ServerThread.setup = self
+ # from module.web import webinterface
+ # webinterface.run_simple()
+ # return False
+ # except Exception, e:
+ # print "Setup failed with this error: ", e
+ # print "Falling back to commandline setup."
print ""
@@ -69,13 +73,14 @@ class Setup():
print ""
print _("The value in brackets [] always is the default value,")
print _("in case you don't want to change it or you are unsure what to choose, just hit enter.")
- print _("Don't forget: You can always rerun this assistent with --setup or -s parameter, when you start pyLoadCore.")
+ print _(
+ "Don't forget: You can always rerun this assistent with --setup or -s parameter, when you start pyLoadCore.")
print _("If you have any problems with this assistent hit STRG-C,")
print _("to abort and don't let him start with pyLoadCore automatically anymore.")
print ""
print _("When you are ready for system check, hit enter.")
raw_input()
-
+
basic, ssl, captcha, gui, web, js = self.system_check()
print ""
@@ -90,8 +95,7 @@ class Setup():
print ""
print _("## Status ##")
print ""
-
-
+
avail = []
if self.check_module("Crypto"): avail.append(_("container decrypting"))
if ssl: avail.append(_("ssl connection"))
@@ -99,64 +103,64 @@ class Setup():
if gui: avail.append(_("GUI"))
if web: avail.append(_("Webinterface"))
if js: avail.append(_("extended Click'N'Load"))
-
+
string = ""
-
+
for av in avail:
- string += ", "+av
+ string += ", " + av
print _("Features available:") + string[1:]
print ""
-
+
if len(avail) < 5:
print _("Featues missing: ")
print
-
+
if not self.check_module("Crypto"):
print _("no py-crypto available")
print _("You need this if you want to decrypt container files.")
print ""
-
+
if not ssl:
print _("no SSL available")
print _("This is needed if you want to establish a secure connection to core or webinterface.")
print _("If you only want to access locally to pyLoad ssl is not usefull.")
print ""
-
+
if not captcha:
print _("no Captcha Recognition available")
print _("Only needed for some hosters and as freeuser.")
print ""
-
+
if not gui:
print _("Gui not available")
print _("The Graphical User Interface.")
print ""
-
+
if not js:
print _("no JavaScript engine found")
print _("You will need this for some Click'N'Load links. Install Spidermonkey, ossp-js, pyv8 or rhino")
-
+
print _("You can abort the setup now and fix some dependicies if you want.")
- con = self.ask(_("Continue with setup?"), "y", bool=True)
+ con = self.ask(_("Continue with setup?"), self.yes, bool=True)
if not con:
return False
print ""
- print _("Do you want to change the config path? Current is %s" % abspath(""))
- print _("If you use pyLoad on a server or the home partition lives on an iternal flash it may be a good idea to change it.")
- path = self.ask(_("Change config path?"), "n", bool=True)
+ print _("Do you want to change the config path? Current is %s") % abspath("")
+ print _(
+ "If you use pyLoad on a server or the home partition lives on an iternal flash it may be a good idea to change it.")
+ path = self.ask(_("Change config path?"), self.no, bool=True)
if path:
self.conf_path()
#calls exit when changed
-
-
+
print ""
print _("Do you want to configure login data and basic settings?")
print _("This is recommend for first run.")
- con = self.ask(_("Make basic setup?"), "y", bool=True)
+ con = self.ask(_("Make basic setup?"), self.yes, bool=True)
if con:
self.conf_basic()
@@ -164,14 +168,14 @@ class Setup():
if ssl:
print ""
print _("Do you want to configure ssl?")
- ssl = self.ask(_("Configure ssl?"), "n", bool=True)
+ ssl = self.ask(_("Configure ssl?"), self.no, bool=True)
if ssl:
self.conf_ssl()
if web:
print ""
print _("Do you want to configure webinterface?")
- web = self.ask(_("Configure webinterface?"), "y", bool=True)
+ web = self.ask(_("Configure webinterface?"), self.yes, bool=True)
if web:
self.conf_web()
@@ -197,7 +201,6 @@ class Setup():
print _("Python Version: OK")
python = True
-
curl = self.check_module("pycurl")
self.print_dep("pycurl", curl)
@@ -207,10 +210,10 @@ class Setup():
basic = python and curl and sqlite
print ""
-
+
crypto = self.check_module("Crypto")
self.print_dep("pycrypto", crypto)
-
+
ssl = self.check_module("OpenSSL")
self.print_dep("py-OpenSSL", ssl)
@@ -218,12 +221,12 @@ class Setup():
pil = self.check_module("Image")
self.print_dep("py-imaging", pil)
-
+
if os.name == "nt":
tesser = self.check_prog([join(pypath, "tesseract", "tesseract.exe"), "-v"])
else:
tesser = self.check_prog(["tesseract", "-v"])
-
+
self.print_dep("tesseract", tesser)
captcha = pil and tesser
@@ -238,6 +241,7 @@ class Setup():
try:
import jinja2
+
v = jinja2.__version__
if v and "unknown" not in v:
if not v.startswith("2.5") and not v.startswith("2.6"):
@@ -246,15 +250,13 @@ class Setup():
print _("please upgrade or deinstall it, pyLoad includes a sufficient jinja2 libary.")
print
jinja = False
- except :
+ except:
pass
-
self.print_dep("jinja2", jinja)
beaker = self.check_module("beaker")
self.print_dep("beaker", beaker)
-
web = sqlite and beaker
from module.common import JsEngine
@@ -270,11 +272,12 @@ class Setup():
print ""
print _("The following logindata is valid for CLI, GUI and webinterface.")
-
+
from module.database import DatabaseBackend
+
db = DatabaseBackend(None)
db.setup()
- username = self.ask(_("Username"), "User")
+ username = self.ask(_("Username"), "User")
password = self.ask("", "", password=True)
db.addUser(username, password)
db.shutdown()
@@ -282,20 +285,18 @@ class Setup():
print ""
print _("External clients (GUI, CLI or other) need remote access to work over the network.")
print _("However, if you only want to use the webinterface you may disable it to save ram.")
- self.config["remote"]["activated"] = self.ask(_("Enable remote access"), "y", bool=True)
-
+ self.config["remote"]["activated"] = self.ask(_("Enable remote access"), self.yes, bool=True)
print ""
langs = self.config.getMetaData("general", "language")
self.config["general"]["language"] = self.ask(_("Language"), "en", langs["type"].split(";"))
-
self.config["general"]["download_folder"] = self.ask(_("Downloadfolder"), "Downloads")
self.config["download"]["max_downloads"] = self.ask(_("Max parallel downloads"), "3")
#print _("You should disable checksum proofing, if you have low hardware requirements.")
#self.config["general"]["checksum"] = self.ask(_("Proof checksum?"), "y", bool=True)
- reconnect = self.ask(_("Use Reconnect?"), "n", bool=True)
+ reconnect = self.ask(_("Use Reconnect?"), self.no, bool=True)
self.config["reconnect"]["activated"] = reconnect
if reconnect:
self.config["reconnect"]["method"] = self.ask(_("Reconnect script location"), "./reconnect.sh")
@@ -304,9 +305,9 @@ class Setup():
def conf_web(self):
print ""
print _("## Webinterface Setup ##")
-
+
print ""
- self.config["webinterface"]["activated"] = self.ask(_("Activate webinterface?"), "y", bool=True)
+ self.config["webinterface"]["activated"] = self.ask(_("Activate webinterface?"), self.yes, bool=True)
print ""
print _("Listen address, if you use 127.0.0.1 or localhost, the webinterface will only accessible locally.")
self.config["webinterface"]["host"] = self.ask(_("Address"), "0.0.0.0")
@@ -315,17 +316,19 @@ class Setup():
print _("pyLoad offers several server backends, now following a short explanation.")
print "builtin:", _("Default server, best choice if you dont know which one to choose.")
print "threaded:", _("This server offers SSL and is a good alternative to builtin.")
- print "fastcgi:", _("Can be used by apache, lighttpd, requires you to configure them, which is not too easy job.")
+ print "fastcgi:", _(
+ "Can be used by apache, lighttpd, requires you to configure them, which is not too easy job.")
print "lightweight:", _("Very fast alternative written in C, requires libev and linux knowlegde.")
print "\t", _("Get it from here: https://github.com/jonashaag/bjoern, compile it")
print "\t", _("and copy bjoern.so to module/lib")
print
- print _("Attention: In some rare cases the builtin server is not working, if you notice problems with the webinterface")
+ print _(
+ "Attention: In some rare cases the builtin server is not working, if you notice problems with the webinterface")
print _("come back here and change the builtin server to the threaded one here.")
-
- self.config["webinterface"]["server"] = self.ask(_("Server"), "builtin", ["builtin", "threaded", "fastcgi", "lightweight"])
+ self.config["webinterface"]["server"] = self.ask(_("Server"), "builtin",
+ ["builtin", "threaded", "fastcgi", "lightweight"])
def conf_ssl(self):
print ""
@@ -339,17 +342,19 @@ class Setup():
print ""
print _("If you're done and everything went fine, you can activate ssl now.")
- self.config["ssl"]["activated"] = self.ask(_("Activate SSL?"), "y", bool=True)
+ self.config["ssl"]["activated"] = self.ask(_("Activate SSL?"), self.yes, bool=True)
def set_user(self):
-
- translation = gettext.translation("setup", join(self.path, "locale"), languages=["en", self.config["general"]["language"]])
+ gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])
+ translation = gettext.translation("setup", join(self.path, "locale"),
+ languages=[self.config["general"]["language"], "en"], fallback=True)
translation.install(True)
-
+
from module.database import DatabaseBackend
+
db = DatabaseBackend(None)
db.setup()
-
+
noaction = True
try:
while True:
@@ -363,7 +368,7 @@ class Setup():
continue
elif action == "1":
print ""
- username = self.ask(_("Username"), "User")
+ username = self.ask(_("Username"), "User")
password = self.ask("", "", password=True)
db.addUser(username, password)
noaction = False
@@ -394,16 +399,18 @@ class Setup():
def conf_path(self, trans=False):
if trans:
- translation = gettext.translation("setup", join(self.path, "locale"), languages=[self.config["general"]["language"]])
+ gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])
+ translation = gettext.translation("setup", join(self.path, "locale"),
+ languages=[self.config["general"]["language"], "en"], fallback=True)
translation.install(True)
-
+
print _("Setting new configpath, current configuration will not be transfered!")
path = self.ask(_("Configpath"), abspath(""))
try:
path = join(pypath, path)
if not exists(path):
makedirs(path)
- f = open(join(pypath, "module","config", "configdir"), "wb")
+ f = open(join(pypath, "module", "config", "configdir"), "wb")
f.write(path)
f.close()
print _("Configpath changed, setup will now close, please restart to go on.")
@@ -412,7 +419,7 @@ class Setup():
exit()
except Exception, e:
print _("Setting config path failed: %s") % str(e)
-
+
def print_dep(self, name, value):
"""Print Status of dependency"""
if value:
@@ -446,10 +453,10 @@ class Setup():
info += ")"
elif bool:
- if default == "y":
- info = "([y]/n)"
+ if default == self.yes:
+ info = _("[y]/n")
else:
- info = "(y/[n])"
+ info = _("y/[n]")
else:
info = "[%s]" % default
@@ -457,7 +464,6 @@ class Setup():
p1 = True
p2 = False
while p1 != p2:
-
if os.name == "nt":
qst = str("Password: ") #no unicode on windows
else:
@@ -489,15 +495,16 @@ class Setup():
input = default
if bool:
- if re.match(r"(y|yes|j|ja|true)", input.lower().strip()):
+ # yes, true,t are inputs for booleans with value true
+ if input.lower().strip() in [self.yes, _("yes"), _("true"), _("t")]:
return True
- elif re.match(r"(n|no|nein|false)", input.lower().strip()):
+ # no, false,f are inputs for booleans with value false
+ elif input.lower().strip() in [self.no, _("no"), _("false"), _("f")]:
return False
else:
print _("Invalid Input")
continue
-
if not answers:
return input
diff --git a/module/web/pyload_app.py b/module/web/pyload_app.py
index e6c9ab436..9612e495e 100644
--- a/module/web/pyload_app.py
+++ b/module/web/pyload_app.py
@@ -381,7 +381,7 @@ def path(file="", path=""):
@route("/logs", method="POST")
@route("/logs/:item")
@route("/logs/:item", method="POST")
-@login_required('STATUS')
+@login_required('LOGS')
def logs(item=-1):
s = request.environ.get('beaker.session')
diff --git a/module/web/webinterface.py b/module/web/webinterface.py
index 68724e3f6..ec8b2e56c 100644
--- a/module/web/webinterface.py
+++ b/module/web/webinterface.py
@@ -18,8 +18,9 @@
"""
import sys
-import gettext
+import module.common.pylgettext as gettext
+import os
from os.path import join, abspath, dirname, exists
from os import makedirs
@@ -98,8 +99,9 @@ if PREFIX:
else:
env.filters["url"] = lambda x: PREFIX + x if x.startswith("/") else x
+gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])
translation = gettext.translation("django", join(PYLOAD_DIR, "locale"),
- languages=["en", config.get("general", "language")])
+ languages=[config.get("general", "language"), "en"],fallback=True)
translation.install(True)
env.install_gettext_translations(translation)