mirror of
https://github.com/rembo10/headphones.git
synced 2026-01-09 14:48:07 -05:00
@@ -22,8 +22,9 @@ import http.client
|
||||
|
||||
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a byte string in the given
|
||||
encoding.
|
||||
"""Convert a native :class:`str` to a :class:`bytes` instance.
|
||||
|
||||
The encoding can be changed to non-ASCII optionally.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
@@ -31,8 +32,9 @@ def ntob(n, encoding='ISO-8859-1'):
|
||||
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a unicode string with the given
|
||||
encoding.
|
||||
"""Convert a native :class:`str` to a :class:`str` instance.
|
||||
|
||||
This doesn't actually do anything.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
@@ -48,6 +50,7 @@ def tonative(n, encoding='ISO-8859-1'):
|
||||
|
||||
|
||||
def assert_native(n):
|
||||
"""Ensure that input is a native :class:`str`."""
|
||||
if not isinstance(n, str):
|
||||
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class PageHandler(object):
|
||||
"""Callable which sets response.body."""
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
"""Initialize the page handler."""
|
||||
self.callable = callable
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
@@ -36,6 +37,7 @@ class PageHandler(object):
|
||||
|
||||
@args.setter
|
||||
def args(self, args):
|
||||
"""Set the request arguments in order."""
|
||||
cherrypy.serving.request.args = args
|
||||
return cherrypy.serving.request.args
|
||||
|
||||
@@ -46,10 +48,12 @@ class PageHandler(object):
|
||||
|
||||
@kwargs.setter
|
||||
def kwargs(self, kwargs):
|
||||
"""Set the named request keyword arguments as :class:`dict`."""
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
return cherrypy.serving.request.kwargs
|
||||
|
||||
def __call__(self):
|
||||
"""Invoke an HTTP handler callable for :class:`PageHandler`."""
|
||||
try:
|
||||
return self.callable(*self.args, **self.kwargs)
|
||||
except TypeError:
|
||||
@@ -203,15 +207,18 @@ try:
|
||||
import inspect
|
||||
except ImportError:
|
||||
def test_callable_spec(callable, args, kwargs): # noqa: F811
|
||||
"""Do nothing as a no-op."""
|
||||
return None
|
||||
else:
|
||||
def getargspec(callable):
|
||||
"""Get argument specification using :mod:`inspect`."""
|
||||
return inspect.getfullargspec(callable)[:4]
|
||||
|
||||
|
||||
class LateParamPageHandler(PageHandler):
|
||||
"""Page handler callable with delayed request parameters binding.
|
||||
|
||||
"""When passing cherrypy.request.params to the page handler, we do not
|
||||
When passing ``cherrypy.request.params`` to the page handler, we do not
|
||||
want to capture that dict too early; we want to give tools like the
|
||||
decoding tool a chance to modify the params dict in-between the lookup
|
||||
of the handler and the actual calling of the handler. This subclass
|
||||
@@ -221,7 +228,11 @@ class LateParamPageHandler(PageHandler):
|
||||
|
||||
@property
|
||||
def kwargs(self):
|
||||
"""Page handler kwargs (with cherrypy.request.params copied in)."""
|
||||
"""Page handler keyword arguments.
|
||||
|
||||
The returned value contains data merged in
|
||||
from ``cherrypy.request.params``.
|
||||
"""
|
||||
kwargs = cherrypy.serving.request.params.copy()
|
||||
if self._kwargs:
|
||||
kwargs.update(self._kwargs)
|
||||
@@ -229,6 +240,7 @@ class LateParamPageHandler(PageHandler):
|
||||
|
||||
@kwargs.setter
|
||||
def kwargs(self, kwargs):
|
||||
"""Set the named request keyword arguments as :class:`dict`."""
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
self._kwargs = kwargs
|
||||
|
||||
@@ -238,6 +250,7 @@ if sys.version_info < (3, 0):
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
"""Ensure the translator is of the correct length and size."""
|
||||
if not isinstance(t, str) or len(t) != 256:
|
||||
raise ValueError(
|
||||
'The translate argument must be a str of len 256.')
|
||||
@@ -246,6 +259,7 @@ else:
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
"""Ensure the translator is of the correct length and size."""
|
||||
if not isinstance(t, dict):
|
||||
raise ValueError('The translate argument must be a dict.')
|
||||
|
||||
@@ -273,6 +287,7 @@ class Dispatcher(object):
|
||||
|
||||
def __init__(self, dispatch_method_name=None,
|
||||
translate=punctuation_to_underscores):
|
||||
"""Initialize the HTTP request dispatcher."""
|
||||
validate_translator(translate)
|
||||
self.translate = translate
|
||||
if dispatch_method_name:
|
||||
@@ -389,7 +404,9 @@ class Dispatcher(object):
|
||||
object_trail.append([name, node, nodeconf, segleft])
|
||||
|
||||
def set_conf():
|
||||
"""Collapse all object_trail config into cherrypy.request.config.
|
||||
"""Collapse all ``object_trail`` conf into config.
|
||||
|
||||
The config being ``cherrypy.request.config``.
|
||||
"""
|
||||
base = cherrypy.config.copy()
|
||||
# Note that we merge the config from each node
|
||||
@@ -505,10 +522,12 @@ class RoutesDispatcher(object):
|
||||
self.mapper.controller_scan = self.controllers.keys
|
||||
|
||||
def connect(self, name, route, controller, **kwargs):
|
||||
"""Mount an HTTP handler into the router."""
|
||||
self.controllers[name] = controller
|
||||
self.mapper.connect(name, route, controller=name, **kwargs)
|
||||
|
||||
def redirect(self, url):
|
||||
"""Perform an HTTP redirect to the given URL."""
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
def __call__(self, path_info):
|
||||
@@ -602,6 +621,7 @@ class RoutesDispatcher(object):
|
||||
|
||||
|
||||
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
||||
"""Chain an HTTP dispatcher variant implementing XML-RPC."""
|
||||
from cherrypy.lib import xmlrpcutil
|
||||
|
||||
def xmlrpc_dispatch(path_info):
|
||||
|
||||
@@ -137,7 +137,6 @@ from cherrypy.lib import httputil as _httputil
|
||||
|
||||
class CherryPyException(Exception):
|
||||
"""A base class for CherryPy exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class InternalRedirect(CherryPyException):
|
||||
@@ -150,6 +149,7 @@ class InternalRedirect(CherryPyException):
|
||||
"""
|
||||
|
||||
def __init__(self, path, query_string=''):
|
||||
"""Initialize the internal redirect exception."""
|
||||
self.request = cherrypy.serving.request
|
||||
|
||||
self.query_string = query_string
|
||||
@@ -202,6 +202,7 @@ class HTTPRedirect(CherryPyException):
|
||||
"""The encoding when passed urls are not native strings."""
|
||||
|
||||
def __init__(self, urls, status=None, encoding=None):
|
||||
"""Initialize the HTTP redirect exception."""
|
||||
self.urls = abs_urls = [
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a complete URL with host (e.g. "http://www.example.com/test")
|
||||
@@ -227,7 +228,9 @@ class HTTPRedirect(CherryPyException):
|
||||
|
||||
@classproperty
|
||||
def default_status(cls):
|
||||
"""The default redirect status for the request.
|
||||
"""Redirect status for the request.
|
||||
|
||||
This is the default handler.
|
||||
|
||||
RFC 2616 indicates a 301 response code fits our goal; however,
|
||||
browser support for 301 is quite messy. Use 302/303 instead. See
|
||||
@@ -242,8 +245,9 @@ class HTTPRedirect(CherryPyException):
|
||||
return status
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
"""Modify ``cherrypy.response`` to represent ``self``.
|
||||
|
||||
Modifies status, headers, and body.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create
|
||||
an HTTPRedirect object and set its output without *raising* the
|
||||
@@ -366,6 +370,7 @@ class HTTPError(CherryPyException):
|
||||
"""The HTTP Reason-Phrase string."""
|
||||
|
||||
def __init__(self, status=500, message=None):
|
||||
"""Initialize an HTTP error."""
|
||||
self.status = status
|
||||
try:
|
||||
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
|
||||
@@ -381,8 +386,9 @@ class HTTPError(CherryPyException):
|
||||
CherryPyException.__init__(self, status, message)
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
"""Modify ``cherrypy.response`` to represent ``self``.
|
||||
|
||||
Modifies status, headers, and body.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create
|
||||
an HTTPError object and set its output without *raising* the
|
||||
@@ -408,6 +414,7 @@ class HTTPError(CherryPyException):
|
||||
_be_ie_unfriendly(self.code)
|
||||
|
||||
def get_error_page(self, *args, **kwargs):
|
||||
"""Compose an HTML page with error information."""
|
||||
return get_error_page(*args, **kwargs)
|
||||
|
||||
def __call__(self):
|
||||
@@ -432,6 +439,7 @@ class NotFound(HTTPError):
|
||||
"""
|
||||
|
||||
def __init__(self, path=None):
|
||||
"""Initialize an HTTP Not Found error."""
|
||||
if path is None:
|
||||
request = cherrypy.serving.request
|
||||
path = request.script_name + request.path_info
|
||||
@@ -600,7 +608,6 @@ def bare_error(extrabody=None):
|
||||
is set in the body. If extrabody is a string, it will be appended
|
||||
as-is to the body.
|
||||
"""
|
||||
|
||||
# The whole point of this function is to be a last line-of-defense
|
||||
# in handling errors. That is, it must not raise any errors itself;
|
||||
# it cannot be allowed to fail. Therefore, don't add to it!
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""
|
||||
CherryPy logging module.
|
||||
|
||||
Simple config
|
||||
=============
|
||||
|
||||
@@ -126,12 +128,15 @@ class NullHandler(logging.Handler):
|
||||
"""A no-op logging handler to silence the logging.lastResort handler."""
|
||||
|
||||
def handle(self, record):
|
||||
"""Handle a log record doing no-op."""
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a log record doing no-op."""
|
||||
pass
|
||||
|
||||
def createLock(self):
|
||||
"""Lock log write with no-op."""
|
||||
self.lock = None
|
||||
|
||||
|
||||
@@ -167,6 +172,7 @@ class LogManager(object):
|
||||
"""
|
||||
|
||||
def __init__(self, appid=None, logger_root='cherrypy'):
|
||||
"""Initialize a CherryPy log manager."""
|
||||
self.logger_root = logger_root
|
||||
self.appid = appid
|
||||
if appid is None:
|
||||
@@ -217,11 +223,11 @@ class LogManager(object):
|
||||
)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""An alias for ``error``."""
|
||||
"""Record an error log entry."""
|
||||
return self.error(*args, **kwargs)
|
||||
|
||||
def access(self):
|
||||
"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
r"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
|
||||
See the
|
||||
`apache documentation
|
||||
@@ -414,7 +420,10 @@ class LogManager(object):
|
||||
|
||||
|
||||
class WSGIErrorHandler(logging.Handler):
|
||||
"A handler class which writes logging records to environ['wsgi.errors']."
|
||||
"""A handler class writing logs to WSGI env.
|
||||
|
||||
Specifically, the target is ``environ['wsgi.errors']``.
|
||||
"""
|
||||
|
||||
def flush(self):
|
||||
"""Flushes the stream."""
|
||||
@@ -450,6 +459,8 @@ class WSGIErrorHandler(logging.Handler):
|
||||
|
||||
|
||||
class LazyRfc3339UtcTime(object):
|
||||
"""A postponed timestamp string retrieval class."""
|
||||
|
||||
def __str__(self):
|
||||
"""Return datetime in RFC3339 UTC Format."""
|
||||
iso_formatted_now = datetime.datetime.now(
|
||||
|
||||
@@ -72,6 +72,7 @@ from cherrypy.lib import httputil
|
||||
|
||||
|
||||
def setup(req):
|
||||
"""Execute pre-initialization functions."""
|
||||
from mod_python import apache
|
||||
|
||||
# Run any setup functions defined by a "PythonOption cherrypy.setup"
|
||||
@@ -140,6 +141,7 @@ _isSetUp = False
|
||||
|
||||
|
||||
def handler(req):
|
||||
"""Invoke the HTTP handler."""
|
||||
from mod_python import apache
|
||||
try:
|
||||
global _isSetUp
|
||||
@@ -251,6 +253,7 @@ def handler(req):
|
||||
|
||||
|
||||
def send_response(req, status, headers, body, stream=False):
|
||||
"""Send the HTTP response to the client."""
|
||||
# Set response status
|
||||
req.status = int(status[:3])
|
||||
|
||||
@@ -276,17 +279,20 @@ try:
|
||||
import subprocess
|
||||
|
||||
def popen(fullcmd):
|
||||
"""Invoke a subprocess via :mod:`subprocess`."""
|
||||
p = subprocess.Popen(fullcmd, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
close_fds=True)
|
||||
return p.stdout
|
||||
except ImportError:
|
||||
def popen(fullcmd):
|
||||
"""Invoke a subprocess via :mod:`os`."""
|
||||
pipein, pipeout = os.popen4(fullcmd)
|
||||
return pipeout
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
"""Return a subprocess standard output."""
|
||||
fullcmd = '%s %s' % (cmd, args)
|
||||
pipeout = popen(fullcmd)
|
||||
try:
|
||||
@@ -305,6 +311,7 @@ def read_process(cmd, args=''):
|
||||
|
||||
|
||||
class ModPythonServer(object):
|
||||
"""A server wrapper for ``mod_python``."""
|
||||
|
||||
template = """
|
||||
# Apache2 server configuration file for running CherryPy with mod_python.
|
||||
@@ -323,6 +330,7 @@ LoadModule python_module modules/mod_python.so
|
||||
|
||||
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
|
||||
handler='cherrypy._cpmodpy::handler'):
|
||||
"""Initialize a ``mod_python`` server."""
|
||||
self.loc = loc
|
||||
self.port = port
|
||||
self.opts = opts
|
||||
@@ -330,6 +338,7 @@ LoadModule python_module modules/mod_python.so
|
||||
self.handler = handler
|
||||
|
||||
def start(self):
|
||||
"""Start an Apache2/httpd server."""
|
||||
opts = ''.join([' PythonOption %s %s\n' % (k, v)
|
||||
for k, v in self.opts])
|
||||
conf_data = self.template % {'port': self.port,
|
||||
@@ -347,5 +356,6 @@ LoadModule python_module modules/mod_python.so
|
||||
return response
|
||||
|
||||
def stop(self):
|
||||
"""Stop an Apache2/httpd server."""
|
||||
os.popen('apache -k stop')
|
||||
self.ready = False
|
||||
|
||||
@@ -220,7 +220,9 @@ def process_multipart(entity):
|
||||
|
||||
|
||||
def process_multipart_form_data(entity):
|
||||
"""Read all multipart/form-data parts into entity.parts or entity.params.
|
||||
"""Read ``multipart/form-data`` parts.
|
||||
|
||||
This function saves them into ``entity.parts`` or ``entity.params``.
|
||||
"""
|
||||
process_multipart(entity)
|
||||
|
||||
@@ -248,7 +250,7 @@ def process_multipart_form_data(entity):
|
||||
|
||||
|
||||
def _old_process_multipart(entity):
|
||||
"""The behavior of 3.2 and lower.
|
||||
"""Behavior of 3.2 and lower.
|
||||
|
||||
Deprecated and will be changed in 3.3.
|
||||
"""
|
||||
@@ -411,6 +413,7 @@ class Entity(object):
|
||||
"""
|
||||
|
||||
def __init__(self, fp, headers, params=None, parts=None):
|
||||
"""Initialize an HTTP entity."""
|
||||
# Make an instance-specific copy of the class processors
|
||||
# so Tools, etc. can replace them per-request.
|
||||
self.processors = self.processors.copy()
|
||||
@@ -479,24 +482,30 @@ class Entity(object):
|
||||
self.filename = unquote(str(filename), encoding)
|
||||
|
||||
def read(self, size=None, fp_out=None):
|
||||
"""Read bytes from the connection."""
|
||||
return self.fp.read(size, fp_out)
|
||||
|
||||
def readline(self, size=None):
|
||||
"""Read a line of bytes from the connection."""
|
||||
return self.fp.readline(size)
|
||||
|
||||
def readlines(self, sizehint=None):
|
||||
"""Read some byte lines from the connection."""
|
||||
return self.fp.readlines(sizehint)
|
||||
|
||||
def __iter__(self):
|
||||
"""Set up the iterator."""
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
"""Return the next line of bytes."""
|
||||
line = self.readline()
|
||||
if not line:
|
||||
raise StopIteration
|
||||
return line
|
||||
|
||||
def next(self):
|
||||
"""Return the next line of bytes.""" # FIXME: python 2?
|
||||
return self.__next__()
|
||||
|
||||
def read_into_file(self, fp_out=None):
|
||||
@@ -564,7 +573,9 @@ class Entity(object):
|
||||
proc(self)
|
||||
|
||||
def default_proc(self):
|
||||
"""Called if a more-specific processor is not found for the
|
||||
"""Process unknown data as a fallback.
|
||||
|
||||
Called if a more-specific processor is not found for the
|
||||
``Content-Type``.
|
||||
"""
|
||||
# Leave the fp alone for someone else to read. This works fine
|
||||
@@ -614,6 +625,7 @@ class Part(Entity):
|
||||
"""
|
||||
|
||||
def __init__(self, fp, headers, boundary):
|
||||
"""Initialize an entity part."""
|
||||
Entity.__init__(self, fp, headers)
|
||||
self.boundary = boundary
|
||||
self.file = None
|
||||
@@ -621,11 +633,13 @@ class Part(Entity):
|
||||
|
||||
@classmethod
|
||||
def from_fp(cls, fp, boundary):
|
||||
"""Initialize an entity part from a file handle."""
|
||||
headers = cls.read_headers(fp)
|
||||
return cls(fp, headers, boundary)
|
||||
|
||||
@classmethod
|
||||
def read_headers(cls, fp):
|
||||
"""Read HTTP headers from a file handle."""
|
||||
headers = httputil.HeaderMap()
|
||||
while True:
|
||||
line = fp.readline()
|
||||
@@ -713,7 +727,9 @@ class Part(Entity):
|
||||
return fp_out
|
||||
|
||||
def default_proc(self):
|
||||
"""Called if a more-specific processor is not found for the
|
||||
"""Process unknown data as a fallback.
|
||||
|
||||
Called if a more-specific processor is not found for the
|
||||
``Content-Type``.
|
||||
"""
|
||||
if self.filename:
|
||||
@@ -743,9 +759,11 @@ inf = float('inf')
|
||||
|
||||
|
||||
class SizedReader:
|
||||
"""A buffered/sized reader."""
|
||||
|
||||
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
|
||||
has_trailers=False):
|
||||
"""Initialize buffered file handle reader."""
|
||||
# Wrap our fp in a buffer so peek() works
|
||||
self.fp = fp
|
||||
self.length = length
|
||||
@@ -773,7 +791,6 @@ class SizedReader:
|
||||
object that supports the 'write' method; all bytes read will be
|
||||
written to the fp, and None is returned.
|
||||
"""
|
||||
|
||||
if self.length is None:
|
||||
if size is None:
|
||||
remaining = inf
|
||||
@@ -889,6 +906,7 @@ class SizedReader:
|
||||
return lines
|
||||
|
||||
def finish(self):
|
||||
"""Finalize reading the HTTP trailer headers."""
|
||||
self.done = True
|
||||
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
|
||||
self.trailers = {}
|
||||
@@ -946,6 +964,7 @@ class RequestBody(Entity):
|
||||
"""
|
||||
|
||||
def __init__(self, fp, headers, params=None, request_params=None):
|
||||
"""Initialize a request body entity."""
|
||||
Entity.__init__(self, fp, headers, params)
|
||||
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
|
||||
|
||||
@@ -42,6 +42,7 @@ class Hook(object):
|
||||
callable on each call."""
|
||||
|
||||
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
|
||||
"""Initialize the hook instance."""
|
||||
self.callback = callback
|
||||
|
||||
if failsafe is None:
|
||||
@@ -55,7 +56,14 @@ class Hook(object):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __lt__(self, other):
|
||||
"""
|
||||
"""Check if this hook's priority is lower than the other's.
|
||||
|
||||
:param other: Another object to compare priority with.
|
||||
:type other: Hook
|
||||
|
||||
:returns: Whether the other Hook's priority is higher than this one's.
|
||||
:rtype: bool
|
||||
|
||||
Hooks sort by priority, ascending, such that
|
||||
hooks of lower priority are run first.
|
||||
"""
|
||||
@@ -66,6 +74,7 @@ class Hook(object):
|
||||
return self.callback(**self.kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
"""Render a string representation of :class:`Hook` instance."""
|
||||
cls = self.__class__
|
||||
return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
|
||||
% (cls.__module__, cls.__name__, self.callback,
|
||||
@@ -78,12 +87,14 @@ class HookMap(dict):
|
||||
"""A map of call points to lists of callbacks (Hook objects)."""
|
||||
|
||||
def __new__(cls, points=None):
|
||||
"""Construct a fresh hook map instance."""
|
||||
d = dict.__new__(cls)
|
||||
for p in points or []:
|
||||
d[p] = []
|
||||
return d
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
"""Initialize a hook map instance post-construction."""
|
||||
pass
|
||||
|
||||
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
|
||||
@@ -124,6 +135,7 @@ class HookMap(dict):
|
||||
raise
|
||||
|
||||
def __copy__(self):
|
||||
"""Duplicate object per the copy protocol."""
|
||||
newmap = self.__class__()
|
||||
# We can't just use 'update' because we want copies of the
|
||||
# mutable values (each is a list) as well.
|
||||
@@ -133,6 +145,7 @@ class HookMap(dict):
|
||||
copy = __copy__
|
||||
|
||||
def __repr__(self):
|
||||
"""Render a string representation of :class:`HookMap`."""
|
||||
cls = self.__class__
|
||||
return '%s.%s(points=%r)' % (
|
||||
cls.__module__,
|
||||
@@ -541,7 +554,9 @@ class Request(object):
|
||||
self.stage = 'close'
|
||||
|
||||
def run(self, method, path, query_string, req_protocol, headers, rfile):
|
||||
r"""Process the Request. (Core)
|
||||
r"""Process the request.
|
||||
|
||||
*(core)*
|
||||
|
||||
method, path, query_string, and req_protocol should be pulled directly
|
||||
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
|
||||
@@ -815,6 +830,7 @@ class ResponseBody(object):
|
||||
'if you wish to return unicode.')
|
||||
|
||||
def __get__(self, obj, objclass=None):
|
||||
"""Return a response body through the descriptor protocol."""
|
||||
if obj is None:
|
||||
# When calling on the class instead of an instance...
|
||||
return self
|
||||
@@ -822,7 +838,10 @@ class ResponseBody(object):
|
||||
return obj._body
|
||||
|
||||
def __set__(self, obj, value):
|
||||
# Convert the given value to an iterable object.
|
||||
"""Set a response body through the descriptor protocol.
|
||||
|
||||
Convert the given value to an iterable object.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
elif isinstance(value, list):
|
||||
@@ -874,6 +893,7 @@ class Response(object):
|
||||
"""If False, buffer the response body."""
|
||||
|
||||
def __init__(self):
|
||||
"""Intialize the HTTP response instance."""
|
||||
self.status = None
|
||||
self.header_list = None
|
||||
self._body = []
|
||||
@@ -896,8 +916,14 @@ class Response(object):
|
||||
return new_body
|
||||
|
||||
def _flush_body(self):
|
||||
"""Discard self.body but consume any generator such that any
|
||||
finalization can occur, such as is required by caching.tee_output()."""
|
||||
"""Exhaust the body iterator.
|
||||
|
||||
:rtype: None
|
||||
|
||||
Discard ``self.body`` but consume any generator such that any
|
||||
finalization can occur, such as is required by
|
||||
``caching.tee_output()``.
|
||||
"""
|
||||
consume(iter(self.body))
|
||||
|
||||
def finalize(self):
|
||||
@@ -951,6 +977,8 @@ class Response(object):
|
||||
|
||||
|
||||
class LazyUUID4(object):
|
||||
"""A delayed UUID4 string maker."""
|
||||
|
||||
def __str__(self):
|
||||
"""Return UUID4 and keep it for future calls."""
|
||||
return str(self.uuid4)
|
||||
|
||||
@@ -57,6 +57,7 @@ class Tool(object):
|
||||
namespace = 'tools'
|
||||
|
||||
def __init__(self, point, callable, name=None, priority=50):
|
||||
"""Initialize a CherryPy tool instance."""
|
||||
self._point = point
|
||||
self.callable = callable
|
||||
self._name = name
|
||||
@@ -66,10 +67,12 @@ class Tool(object):
|
||||
|
||||
@property
|
||||
def on(self):
|
||||
"""Flag whether the tool is enabled."""
|
||||
raise AttributeError(_attr_error)
|
||||
|
||||
@on.setter
|
||||
def on(self, value):
|
||||
"""Set a flag for whether the tool is enabled."""
|
||||
raise AttributeError(_attr_error)
|
||||
|
||||
def _setargs(self):
|
||||
@@ -133,7 +136,7 @@ class Tool(object):
|
||||
return tool_decorator
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
"""Wire this tool into ``cherrypy.request``.
|
||||
|
||||
The standard CherryPy request object will automatically call
|
||||
this method when the tool is "turned on" in config.
|
||||
@@ -159,6 +162,7 @@ class HandlerTool(Tool):
|
||||
"""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
"""Initialize a handler tool."""
|
||||
Tool.__init__(self, 'before_handler', callable, name)
|
||||
|
||||
def handler(self, *args, **kwargs):
|
||||
@@ -183,7 +187,7 @@ class HandlerTool(Tool):
|
||||
cherrypy.serving.request.handler = None
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
"""Wire this tool into ``cherrypy.request``.
|
||||
|
||||
The standard CherryPy request object will automatically call
|
||||
this method when the tool is "turned on" in config.
|
||||
@@ -217,12 +221,14 @@ class HandlerWrapperTool(Tool):
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None,
|
||||
priority=50):
|
||||
"""Initialize a handler wrapper tool."""
|
||||
self.newhandler = newhandler
|
||||
self._point = point
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
|
||||
def callable(self, *args, **kwargs):
|
||||
"""Decorate a request handler with a handler tool callable."""
|
||||
innerfunc = cherrypy.serving.request.handler
|
||||
|
||||
def wrap(*args, **kwargs):
|
||||
@@ -234,13 +240,14 @@ class ErrorTool(Tool):
|
||||
"""Tool which is used to replace the default request.error_response."""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
"""Initialize an error tool."""
|
||||
Tool.__init__(self, None, callable, name)
|
||||
|
||||
def _wrapper(self):
|
||||
self.callable(**self._merged_args())
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
"""Wire this tool into ``cherrypy.request``.
|
||||
|
||||
The standard CherryPy request object will automatically call
|
||||
this method when the tool is "turned on" in config.
|
||||
@@ -270,6 +277,7 @@ class SessionTool(Tool):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a session tool."""
|
||||
# _sessions.init must be bound after headers are read
|
||||
Tool.__init__(self, 'before_request_body', _sessions.init)
|
||||
|
||||
@@ -277,7 +285,7 @@ class SessionTool(Tool):
|
||||
cherrypy.serving.session.acquire_lock()
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
"""Wire this tool into ``cherrypy.request``.
|
||||
|
||||
The standard CherryPy request object will automatically call
|
||||
this method when the tool is "turned on" in config.
|
||||
@@ -360,6 +368,7 @@ class XMLRPCController(object):
|
||||
|
||||
@expose
|
||||
def default(self, *vpath, **params):
|
||||
"""Process the unhandled XML-RPC methods."""
|
||||
rpcparams, rpcmethod = _xmlrpc.process_body()
|
||||
|
||||
subhandler = self
|
||||
@@ -384,7 +393,7 @@ class XMLRPCController(object):
|
||||
|
||||
|
||||
class SessionAuthTool(HandlerTool):
|
||||
pass
|
||||
"""An HTTP session authentication tool."""
|
||||
|
||||
|
||||
class CachingTool(Tool):
|
||||
@@ -402,7 +411,7 @@ class CachingTool(Tool):
|
||||
_wrapper.priority = 90
|
||||
|
||||
def _setup(self):
|
||||
"""Hook caching into cherrypy.request."""
|
||||
"""Wire caching into ``cherrypy.request``."""
|
||||
conf = self._merged_args()
|
||||
|
||||
p = conf.pop('priority', None)
|
||||
@@ -419,9 +428,11 @@ class Toolbox(object):
|
||||
"""
|
||||
|
||||
def __init__(self, namespace):
|
||||
"""Initialize a toolbox instance."""
|
||||
self.namespace = namespace
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""Set an attribute on this :class:`Toolbox` instance."""
|
||||
# If the Tool._name is None, supply it from the attribute name.
|
||||
if isinstance(value, Tool):
|
||||
if value._name is None:
|
||||
@@ -449,7 +460,8 @@ class Toolbox(object):
|
||||
tool._setup()
|
||||
|
||||
def register(self, point, **kwargs):
|
||||
"""
|
||||
"""Register a hook point handler in the toolbox.
|
||||
|
||||
Return a decorator which registers the function
|
||||
at the given hook point.
|
||||
"""
|
||||
|
||||
@@ -18,8 +18,7 @@ from cherrypy.lib import is_closable_iterator
|
||||
|
||||
|
||||
def downgrade_wsgi_ux_to_1x(environ):
|
||||
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
|
||||
"""
|
||||
"""Return new environ dict for WSGI 1.x from provided WSGI u.x environ."""
|
||||
env1x = {}
|
||||
|
||||
url_encoding = environ[ntou('wsgi.url_encoding')]
|
||||
@@ -54,10 +53,11 @@ class VirtualHost(object):
|
||||
|
||||
cherrypy.tree.graft(vhost)
|
||||
"""
|
||||
default = None
|
||||
"""Required.
|
||||
|
||||
The default WSGI application.
|
||||
default = None
|
||||
"""The default WSGI application.
|
||||
|
||||
Required.
|
||||
"""
|
||||
|
||||
use_x_forwarded_host = True
|
||||
@@ -67,7 +67,6 @@ class VirtualHost(object):
|
||||
|
||||
domains = {}
|
||||
"""A dict of {host header value: application} pairs.
|
||||
|
||||
The incoming "Host" request header is looked up in this dict, and,
|
||||
if a match is found, the corresponding WSGI application will be
|
||||
called instead of the default. Note that you often need separate
|
||||
@@ -76,11 +75,30 @@ class VirtualHost(object):
|
||||
"""
|
||||
|
||||
def __init__(self, default, domains=None, use_x_forwarded_host=True):
|
||||
"""
|
||||
Initialize a virtual host app.
|
||||
|
||||
:param default: The default WSGI application
|
||||
:type default: WSGI application
|
||||
:param use_x_forwarded_host: If True (the default), any
|
||||
"X-Forwarded-Host" request header will be used instead of the
|
||||
"Host" header. This is commonly added by HTTP servers (such as
|
||||
Apache) when proxying.
|
||||
:type use_x_forwarded_host: Bool, optional
|
||||
:param domains: A dict of {host header value: application} pairs.
|
||||
The incoming "Host" request header is looked up in this dict, and,
|
||||
if a match is found, the corresponding WSGI application will be
|
||||
called instead of the default. Note that you often need separate
|
||||
entries for "example.com" and "www.example.com". In addition,
|
||||
"Host" headers may contain the port number.
|
||||
:type domains: Dict, optional
|
||||
"""
|
||||
self.default = default
|
||||
self.domains = domains or {}
|
||||
self.use_x_forwarded_host = use_x_forwarded_host
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Route WSGI requests based on host names."""
|
||||
domain = environ.get('HTTP_HOST', '')
|
||||
if self.use_x_forwarded_host:
|
||||
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
|
||||
@@ -95,10 +113,12 @@ class InternalRedirector(object):
|
||||
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
|
||||
|
||||
def __init__(self, nextapp, recursive=False):
|
||||
"""Initialize an internal redirector."""
|
||||
self.nextapp = nextapp
|
||||
self.recursive = recursive
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Process internal WSGI request redirects."""
|
||||
redirections = []
|
||||
while True:
|
||||
environ = environ.copy()
|
||||
@@ -142,10 +162,12 @@ class ExceptionTrapper(object):
|
||||
"""WSGI middleware that traps exceptions."""
|
||||
|
||||
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
|
||||
"""Initialize exception trapper."""
|
||||
self.nextapp = nextapp
|
||||
self.throws = throws
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Handle exceptions while processing a WSGI request."""
|
||||
return _TrappedResponse(
|
||||
self.nextapp,
|
||||
environ,
|
||||
@@ -230,6 +252,7 @@ class AppResponse(object):
|
||||
"""WSGI response iterable for CherryPy applications."""
|
||||
|
||||
def __init__(self, environ, start_response, cpapp):
|
||||
"""Initialize the WSGI app response."""
|
||||
self.cpapp = cpapp
|
||||
try:
|
||||
self.environ = environ
|
||||
@@ -271,9 +294,11 @@ class AppResponse(object):
|
||||
raise
|
||||
|
||||
def __iter__(self):
|
||||
"""Make an app response iterator."""
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
"""Iterate over the app response."""
|
||||
return next(self.iter_response)
|
||||
|
||||
def close(self):
|
||||
@@ -346,6 +371,7 @@ class AppResponse(object):
|
||||
}
|
||||
|
||||
def recode_path_qs(self, path, qs):
|
||||
"""Recode app response path query string."""
|
||||
# This isn't perfect; if the given PATH_INFO is in the
|
||||
# wrong encoding, it may fail to match the appropriate config
|
||||
# section URI. But meh.
|
||||
@@ -416,6 +442,7 @@ class CPWSGIApp(object):
|
||||
"""
|
||||
|
||||
def __init__(self, cpapp, pipeline=None):
|
||||
"""Initialize a framework WSGI app wrapper."""
|
||||
self.cpapp = cpapp
|
||||
self.pipeline = self.pipeline[:]
|
||||
if pipeline:
|
||||
@@ -431,6 +458,7 @@ class CPWSGIApp(object):
|
||||
return self.response_class(environ, start_response, self.cpapp)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Process a WSGI request."""
|
||||
head = self.head
|
||||
if head is None:
|
||||
# Create and nest the WSGI apps in our pipeline (in reverse order).
|
||||
|
||||
39
lib/cherrypy/_private_api/compat/headers.py
Normal file
39
lib/cherrypy/_private_api/compat/headers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""HTTP header parsing helpers."""
|
||||
|
||||
|
||||
def _parse_param(s):
|
||||
while s[:1] == ';':
|
||||
s = s[1:]
|
||||
end = s.find(';')
|
||||
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
|
||||
end = s.find(';', end + 1)
|
||||
if end < 0:
|
||||
end = len(s)
|
||||
f = s[:end]
|
||||
yield f.strip()
|
||||
s = s[end:]
|
||||
|
||||
|
||||
def parse_header(line):
|
||||
"""Parse a ``Content-Type`` like header.
|
||||
|
||||
Return the main ``Content-Type`` and a dictionary of options.
|
||||
|
||||
Copied from removed stdlib :mod:`cgi` module. See
|
||||
`cherrypy/cherrypy#2014 (comment)
|
||||
<https://github.com/cherrypy/cherrypy/issues/2014#issuecomment-1883774891>`_
|
||||
for background.
|
||||
"""
|
||||
parts = _parse_param(';' + line)
|
||||
key = parts.__next__()
|
||||
pdict = {}
|
||||
for p in parts:
|
||||
i = p.find('=')
|
||||
if i >= 0:
|
||||
name = p[:i].strip().lower()
|
||||
value = p[i + 1:].strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
value = value[1:-1]
|
||||
value = value.replace('\\\\', '\\').replace('\\"', '"')
|
||||
pdict[name] = value
|
||||
return key, pdict
|
||||
@@ -33,7 +33,9 @@ __date__ = 'April 2009'
|
||||
|
||||
|
||||
def checkpassword_dict(user_password_dict):
|
||||
"""Returns a checkpassword function which checks credentials
|
||||
"""Check credentials against a dictionary.
|
||||
|
||||
Returns a checkpassword function which checks credentials
|
||||
against a dictionary of the form: {username : password}.
|
||||
|
||||
If you want a simple dictionary-based authentication scheme, use
|
||||
@@ -48,7 +50,9 @@ def checkpassword_dict(user_password_dict):
|
||||
|
||||
|
||||
def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
|
||||
"""A CherryPy tool which hooks at before_handler to perform
|
||||
"""Perform basic auth.
|
||||
|
||||
A CherryPy tool which hooks at before_handler to perform
|
||||
HTTP Basic Access Authentication, as specified in :rfc:`2617`
|
||||
and :rfc:`7617`.
|
||||
|
||||
@@ -69,7 +73,6 @@ def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
|
||||
returns True, else it returns False.
|
||||
|
||||
"""
|
||||
|
||||
fallback_charset = 'ISO-8859-1'
|
||||
|
||||
if '"' in realm:
|
||||
|
||||
@@ -34,6 +34,7 @@ __date__ = 'April 2009'
|
||||
|
||||
|
||||
def md5_hex(s):
|
||||
"""Return hexdigest of md5sum."""
|
||||
return md5(ntob(s, 'utf-8')).hexdigest()
|
||||
|
||||
|
||||
@@ -48,6 +49,7 @@ DEFAULT_CHARSET = 'UTF-8'
|
||||
|
||||
|
||||
def TRACE(msg):
|
||||
"""Log message in TOOLS.AUTH_DIGEST context."""
|
||||
cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
|
||||
|
||||
# Three helper functions for users of the tool, providing three variants
|
||||
@@ -55,8 +57,9 @@ def TRACE(msg):
|
||||
|
||||
|
||||
def get_ha1_dict_plain(user_password_dict):
|
||||
"""Return a get_ha1 function which obtains a plaintext password from a
|
||||
dictionary of the form: {username : password}.
|
||||
"""Return a get_ha1 function which obtains a plaintext password.
|
||||
|
||||
user_password_dict is a dictionary of the form: {username : password}.
|
||||
|
||||
If you want a simple dictionary-based authentication scheme, with plaintext
|
||||
passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
|
||||
@@ -72,8 +75,9 @@ def get_ha1_dict_plain(user_password_dict):
|
||||
|
||||
|
||||
def get_ha1_dict(user_ha1_dict):
|
||||
"""Return a get_ha1 function which obtains a HA1 password hash from a
|
||||
dictionary of the form: {username : HA1}.
|
||||
"""Return a get_ha1 function which obtains a HA1 password hash.
|
||||
|
||||
user_ha1_dict is a dictionary of the form: {username : HA1}.
|
||||
|
||||
If you want a dictionary-based authentication scheme, but with
|
||||
pre-computed HA1 hashes instead of plain-text passwords, use
|
||||
@@ -87,7 +91,9 @@ def get_ha1_dict(user_ha1_dict):
|
||||
|
||||
|
||||
def get_ha1_file_htdigest(filename):
|
||||
"""Return a get_ha1 function which obtains a HA1 password hash from a
|
||||
"""Return a get_ha1 function.
|
||||
|
||||
The returned function obtains a HA1 password hash from a
|
||||
flat file with lines of the same format as that produced by the Apache
|
||||
htdigest utility. For example, for realm 'wonderland', username 'alice',
|
||||
and password '4x5istwelve', the htdigest line would be::
|
||||
@@ -113,7 +119,9 @@ def get_ha1_file_htdigest(filename):
|
||||
|
||||
|
||||
def synthesize_nonce(s, key, timestamp=None):
|
||||
"""Synthesize a nonce value which resists spoofing and can be checked
|
||||
"""Synthesize a nonce value.
|
||||
|
||||
A nonce value resists spoofing and can be checked
|
||||
for staleness. Returns a string suitable as the value for 'nonce' in
|
||||
the www-authenticate header.
|
||||
|
||||
@@ -135,7 +143,7 @@ def synthesize_nonce(s, key, timestamp=None):
|
||||
|
||||
|
||||
def H(s):
|
||||
"""The hash function H."""
|
||||
"""Return an ``md5`` HEX hash."""
|
||||
return md5_hex(s)
|
||||
|
||||
|
||||
@@ -152,7 +160,8 @@ def _try_decode_header(header, charset):
|
||||
|
||||
|
||||
class HttpDigestAuthorization(object):
|
||||
"""
|
||||
"""Digest Authorization implementation.
|
||||
|
||||
Parses a Digest Authorization header and performs
|
||||
re-calculation of the digest.
|
||||
"""
|
||||
@@ -160,10 +169,12 @@ class HttpDigestAuthorization(object):
|
||||
scheme = 'digest'
|
||||
|
||||
def errmsg(self, s):
|
||||
"""Make an error message for HTTP Digest Authorization."""
|
||||
return 'Digest Authorization header: %s' % s
|
||||
|
||||
@classmethod
|
||||
def matches(cls, header):
|
||||
"""Check if header scheme matches auth implementation."""
|
||||
scheme, _, _ = header.partition(' ')
|
||||
return scheme.lower() == cls.scheme
|
||||
|
||||
@@ -171,6 +182,7 @@ class HttpDigestAuthorization(object):
|
||||
self, auth_header, http_method,
|
||||
debug=False, accept_charset=DEFAULT_CHARSET[:],
|
||||
):
|
||||
"""Initialize an HTTP Digest Authorization parser."""
|
||||
self.http_method = http_method
|
||||
self.debug = debug
|
||||
|
||||
@@ -229,10 +241,12 @@ class HttpDigestAuthorization(object):
|
||||
'neither cnonce nor nc can be present'))
|
||||
|
||||
def __str__(self):
|
||||
"""Render an HTTP Digest Auth header as a string."""
|
||||
return 'authorization : %s' % self.auth_header
|
||||
|
||||
def validate_nonce(self, s, key):
|
||||
"""Validate the nonce.
|
||||
|
||||
Returns True if nonce was generated by synthesize_nonce() and the
|
||||
timestamp is not spoofed, else returns False.
|
||||
|
||||
@@ -298,7 +312,7 @@ class HttpDigestAuthorization(object):
|
||||
return H(a2)
|
||||
|
||||
def request_digest(self, ha1, entity_body=''):
|
||||
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
|
||||
"""Calculate the Request-Digest. See :rfc:`2617` section 3.2.2.1.
|
||||
|
||||
ha1
|
||||
The HA1 string obtained from the credentials store.
|
||||
@@ -351,7 +365,7 @@ def www_authenticate(
|
||||
realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
|
||||
stale=False, accept_charset=DEFAULT_CHARSET[:],
|
||||
):
|
||||
"""Constructs a WWW-Authenticate header for Digest authentication."""
|
||||
"""Construct a WWW-Authenticate header for Digest authentication."""
|
||||
if qop not in valid_qops:
|
||||
raise ValueError("Unsupported value for qop: '%s'" % qop)
|
||||
if algorithm not in valid_algorithms:
|
||||
@@ -374,7 +388,9 @@ def www_authenticate(
|
||||
|
||||
|
||||
def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
|
||||
"""A CherryPy tool that hooks at before_handler to perform
|
||||
"""Perform HTTP Digest Access Authentication.
|
||||
|
||||
A CherryPy tool that hooks at ``before_handler`` to perform
|
||||
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
|
||||
|
||||
If the request has an 'authorization' header with a 'Digest' scheme,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""
|
||||
CherryPy cache tooling.
|
||||
|
||||
CherryPy implements a simple caching system as a pluggable Tool. This tool
|
||||
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
|
||||
yet, but it's probably good enough for most sites.
|
||||
@@ -161,6 +163,7 @@ class MemoryCache(Cache):
|
||||
debug = False
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize in-memory cache store."""
|
||||
self.clear()
|
||||
|
||||
# Run self.expire_cache in a separate daemon thread.
|
||||
@@ -442,7 +445,6 @@ def expires(secs=0, force=False, debug=False):
|
||||
|
||||
If any are already present, none of the above response headers are set.
|
||||
"""
|
||||
|
||||
response = cherrypy.serving.response
|
||||
headers = response.headers
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
To use this module, or the coverage tools in the test suite,
|
||||
you need to download 'coverage.py', either Gareth Rees' `original
|
||||
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
|
||||
implementation
|
||||
<https://web.archive.org/web/20231108042640/https://garethrees.org/2001/12/04/python-coverage/>`_
|
||||
or Ned Batchelder's `enhanced version:
|
||||
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
|
||||
|
||||
@@ -22,7 +23,7 @@ it will call ``serve()`` for you.
|
||||
|
||||
import re
|
||||
import sys
|
||||
import cgi
|
||||
import html
|
||||
import os
|
||||
import os.path
|
||||
import urllib.parse
|
||||
@@ -38,6 +39,7 @@ try:
|
||||
the_coverage = coverage(data_file=localFile)
|
||||
|
||||
def start():
|
||||
"""Start collecting coverage."""
|
||||
the_coverage.start()
|
||||
except ImportError:
|
||||
# Setting the_coverage to None will raise errors
|
||||
@@ -50,6 +52,7 @@ except ImportError:
|
||||
'coverage.py could not be imported.')
|
||||
|
||||
def start():
|
||||
"""Start collecting coverage."""
|
||||
pass
|
||||
start.priority = 20
|
||||
|
||||
@@ -284,8 +287,10 @@ def get_tree(base, exclude, coverage=the_coverage):
|
||||
|
||||
|
||||
class CoverStats(object):
|
||||
"""HTTP handler for the coverage stats."""
|
||||
|
||||
def __init__(self, coverage, root=None):
|
||||
"""Initialize the coverage stats application."""
|
||||
self.coverage = coverage
|
||||
if root is None:
|
||||
# Guess initial depth. Files outside this path will not be
|
||||
@@ -295,12 +300,13 @@ class CoverStats(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
"""Render the coverage stats index page."""
|
||||
return TEMPLATE_FRAMESET % self.root.lower()
|
||||
|
||||
@cherrypy.expose
|
||||
def menu(self, base='/', pct='50', showpct='',
|
||||
exclude=r'python\d\.\d|test|tut\d|tutorial'):
|
||||
|
||||
"""Render HTML menu web page."""
|
||||
# The coverage module uses all-lower-case names.
|
||||
base = base.lower().rstrip(os.sep)
|
||||
|
||||
@@ -334,6 +340,7 @@ class CoverStats(object):
|
||||
yield '</body></html>'
|
||||
|
||||
def annotated_file(self, filename, statements, excluded, missing):
|
||||
"""Annotate given file with coverage information."""
|
||||
with open(filename, 'r') as source:
|
||||
lines = source.readlines()
|
||||
buffer = []
|
||||
@@ -352,12 +359,13 @@ class CoverStats(object):
|
||||
buffer.append((lineno, line))
|
||||
if empty_the_buffer:
|
||||
for lno, pastline in buffer:
|
||||
yield template % (lno, cgi.escape(pastline))
|
||||
yield template % (lno, html.escape(pastline))
|
||||
buffer = []
|
||||
yield template % (lineno, cgi.escape(line))
|
||||
yield template % (lineno, html.escape(line))
|
||||
|
||||
@cherrypy.expose
|
||||
def report(self, name):
|
||||
"""Render coverage stats as HTML."""
|
||||
filename, statements, excluded, missing, _ = self.coverage.analysis2(
|
||||
name)
|
||||
pc = _percent(statements, missing)
|
||||
@@ -374,6 +382,7 @@ class CoverStats(object):
|
||||
|
||||
|
||||
def serve(path=localFile, port=8080, root=None):
|
||||
"""Serve the coverage app over HTTP."""
|
||||
if coverage is None:
|
||||
raise ImportError('The coverage module could not be imported.')
|
||||
from coverage import coverage
|
||||
|
||||
@@ -249,6 +249,7 @@ appstats.update({
|
||||
|
||||
|
||||
def proc_time(s):
|
||||
"""Compute current HTTP request processing time."""
|
||||
return time.time() - s['Start Time']
|
||||
|
||||
|
||||
@@ -256,20 +257,24 @@ class ByteCountWrapper(object):
|
||||
"""Wraps a file-like object, counting the number of bytes read."""
|
||||
|
||||
def __init__(self, rfile):
|
||||
"""Initialize a read byte counter."""
|
||||
self.rfile = rfile
|
||||
self.bytes_read = 0
|
||||
|
||||
def read(self, size=-1):
|
||||
"""Read from file, counting bytes."""
|
||||
data = self.rfile.read(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
def readline(self, size=-1):
|
||||
"""Read a line from file, counting bytes."""
|
||||
data = self.rfile.readline(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
def readlines(self, sizehint=0):
|
||||
"""Read a list of lines from file, counting bytes."""
|
||||
# Shamelessly stolen from StringIO
|
||||
total = 0
|
||||
lines = []
|
||||
@@ -283,22 +288,27 @@ class ByteCountWrapper(object):
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
"""Close the underlying file object."""
|
||||
self.rfile.close()
|
||||
|
||||
def __iter__(self):
|
||||
"""Make a file reader iterator."""
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
"""Return next portion of bytes from the iterated file."""
|
||||
data = self.rfile.next()
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
|
||||
def average_uriset_time(s):
|
||||
"""Compute average request processing time within a URI set."""
|
||||
return s['Count'] and (s['Sum'] / s['Count']) or 0
|
||||
|
||||
|
||||
def _get_threading_ident():
|
||||
"""Discover the current thread identifier."""
|
||||
if sys.version_info >= (3, 3):
|
||||
return threading.get_ident()
|
||||
return threading._get_ident()
|
||||
@@ -308,10 +318,11 @@ class StatsTool(cherrypy.Tool):
|
||||
"""Record various information about the current request."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the statistics gathering tool."""
|
||||
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
"""Plug this tool into ``cherrypy.request``.
|
||||
|
||||
The standard CherryPy request object will automatically call
|
||||
this method when the tool is "turned on" in config.
|
||||
@@ -404,14 +415,17 @@ missing = object()
|
||||
|
||||
|
||||
def locale_date(v):
|
||||
"""Format given date per current locale."""
|
||||
return time.strftime('%c', time.gmtime(v))
|
||||
|
||||
|
||||
def iso_format(v):
|
||||
"""Format given date as ISO string."""
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
|
||||
|
||||
|
||||
def pause_resume(ns):
|
||||
"""Produce pause or resume HTML form maker."""
|
||||
def _pause_resume(enabled):
|
||||
pause_disabled = ''
|
||||
resume_disabled = ''
|
||||
@@ -433,6 +447,7 @@ def pause_resume(ns):
|
||||
|
||||
|
||||
class StatsPage(object):
|
||||
"""An app rendering the gathered statistics."""
|
||||
|
||||
formatting = {
|
||||
'CherryPy Applications': {
|
||||
@@ -474,6 +489,7 @@ class StatsPage(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
"""Render the app stats index page."""
|
||||
# Transform the raw data into pretty output for HTML
|
||||
yield """
|
||||
<html>
|
||||
@@ -672,12 +688,14 @@ table.stats2 th {
|
||||
if json is not None:
|
||||
@cherrypy.expose
|
||||
def data(self):
|
||||
"""Render statistics as JSON."""
|
||||
s = extrapolate_statistics(logging.statistics)
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||
return json.dumps(s, sort_keys=True, indent=4).encode('utf-8')
|
||||
|
||||
@cherrypy.expose
|
||||
def pause(self, namespace):
|
||||
"""Pause gathering the statistics."""
|
||||
logging.statistics.get(namespace, {})['Enabled'] = False
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
pause.cp_config = {'tools.allow.on': True,
|
||||
@@ -685,6 +703,7 @@ table.stats2 th {
|
||||
|
||||
@cherrypy.expose
|
||||
def resume(self, namespace):
|
||||
"""Resume gathering the statistics."""
|
||||
logging.statistics.get(namespace, {})['Enabled'] = True
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
resume.cp_config = {'tools.allow.on': True,
|
||||
|
||||
@@ -172,7 +172,6 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
default, 'remote' is set to 'X-Forwarded-For'. If you do not want to
|
||||
rewrite remote.ip, set the 'remote' arg to an empty string.
|
||||
"""
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
if scheme:
|
||||
@@ -288,23 +287,45 @@ class SessionAuth(object):
|
||||
debug = False
|
||||
|
||||
def check_username_and_password(self, username, password):
|
||||
pass
|
||||
"""Assert the login credentials.
|
||||
|
||||
:param username: A user name sent from the login form.
|
||||
:type username: str
|
||||
:param password: A pass word sent from the login form.
|
||||
:type password: str
|
||||
|
||||
:returns: A non-empty error string if the authentication fails.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
def anonymous(self):
|
||||
"""Provide a temporary user name for anonymous users."""
|
||||
pass
|
||||
|
||||
def on_login(self, username):
|
||||
pass
|
||||
"""Process a successful login event.
|
||||
|
||||
:param username: The logged in user name.
|
||||
:type username: str
|
||||
"""
|
||||
|
||||
def on_logout(self, username):
|
||||
pass
|
||||
"""Process a successful logout event.
|
||||
|
||||
:param username: The logged out user name.
|
||||
:type username: str
|
||||
"""
|
||||
|
||||
def on_check(self, username):
|
||||
pass
|
||||
"""Process a successful check event.
|
||||
|
||||
:param username: The checked user name.
|
||||
:type username: str
|
||||
"""
|
||||
|
||||
def login_screen(self, from_page='..', username='', error_msg='',
|
||||
**kwargs):
|
||||
"""Render the login HTML page."""
|
||||
return (str("""<html><body>
|
||||
Message: %(error_msg)s
|
||||
<form method="post" action="do_login">
|
||||
@@ -385,6 +406,7 @@ Message: %(error_msg)s
|
||||
cherrypy.log(template % context, 'TOOLS.SESSAUTH')
|
||||
|
||||
def run(self):
|
||||
"""Perform session authentication."""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
@@ -592,19 +614,21 @@ def accept(media=None, debug=False):
|
||||
|
||||
|
||||
class MonitoredHeaderMap(_httputil.HeaderMap):
|
||||
"""An access-tracked HTTP header mapping."""
|
||||
|
||||
def transform_key(self, key):
|
||||
"""Normalize and track an HTTP header name."""
|
||||
self.accessed_headers.add(key)
|
||||
return super(MonitoredHeaderMap, self).transform_key(key)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a monitored HTTP header mapping."""
|
||||
self.accessed_headers = set()
|
||||
super(MonitoredHeaderMap, self).__init__()
|
||||
|
||||
|
||||
def autovary(ignore=None, debug=False):
|
||||
"""Auto-populate the Vary response header based on request.header access.
|
||||
"""
|
||||
"""Populate ``Vary`` response header based on ``request.header`` access."""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
req_h = request.headers
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"""Encoding module."""
|
||||
import struct
|
||||
import time
|
||||
import io
|
||||
@@ -41,32 +42,41 @@ def decode(encoding=None, default_encoding='utf-8'):
|
||||
|
||||
|
||||
class UTF8StreamEncoder:
|
||||
"""UTF8 Stream Encoder."""
|
||||
|
||||
def __init__(self, iterator):
|
||||
"""Initialize a UTF-8 stream encoder instance."""
|
||||
self._iterator = iterator
|
||||
|
||||
def __iter__(self):
|
||||
"""Make a UTF-8-encoded stream iterator."""
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
"""UTF-8-encode the next chunk of the stream."""
|
||||
return self.__next__()
|
||||
|
||||
def __next__(self):
|
||||
"""UTF-8-encode the next chunk of the stream."""
|
||||
res = next(self._iterator)
|
||||
if isinstance(res, str):
|
||||
res = res.encode('utf-8')
|
||||
return res
|
||||
|
||||
def close(self):
|
||||
"""Close the underlying byte stream."""
|
||||
if is_closable_iterator(self._iterator):
|
||||
self._iterator.close()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Return the underlying byte stream attribute value."""
|
||||
if attr.startswith('__'):
|
||||
raise AttributeError(self, attr)
|
||||
return getattr(self._iterator, attr)
|
||||
|
||||
|
||||
class ResponseEncoder:
|
||||
"""An HTTP response payload encoder."""
|
||||
|
||||
default_encoding = 'utf-8'
|
||||
failmsg = 'Response body could not be encoded with %r.'
|
||||
@@ -77,6 +87,7 @@ class ResponseEncoder:
|
||||
debug = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize HTTP response payload encoder."""
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@@ -124,6 +135,7 @@ class ResponseEncoder:
|
||||
return True
|
||||
|
||||
def find_acceptable_charset(self):
|
||||
"""Deduce acceptable charset for HTTP response."""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
@@ -219,6 +231,7 @@ class ResponseEncoder:
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Set up encoding for the HTTP response."""
|
||||
response = cherrypy.serving.response
|
||||
self.body = self.oldhandler(*args, **kwargs)
|
||||
|
||||
@@ -330,6 +343,7 @@ def compress(body, compress_level):
|
||||
|
||||
|
||||
def decompress(body):
|
||||
"""Decompress a blob of bytes."""
|
||||
import gzip
|
||||
|
||||
zbuf = io.BytesIO()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"""Garbage collection inspection tooling."""
|
||||
import gc
|
||||
import inspect
|
||||
import sys
|
||||
@@ -19,6 +20,7 @@ class ReferrerTree(object):
|
||||
peek_length = 40
|
||||
|
||||
def __init__(self, ignore=None, maxdepth=2, maxparents=10):
|
||||
"""Initialize a referrer tree structure."""
|
||||
self.ignore = ignore or []
|
||||
self.ignore.append(inspect.currentframe().f_back)
|
||||
self.maxdepth = maxdepth
|
||||
@@ -99,18 +101,23 @@ class ReferrerTree(object):
|
||||
|
||||
|
||||
def get_instances(cls):
|
||||
"""Return GC instances."""
|
||||
return [x for x in gc.get_objects() if isinstance(x, cls)]
|
||||
|
||||
|
||||
class RequestCounter(SimplePlugin):
|
||||
"""An HTTP request counter plugin."""
|
||||
|
||||
def start(self):
|
||||
"""Initialize the internal counter."""
|
||||
self.count = 0
|
||||
|
||||
def before_request(self):
|
||||
"""Increment the counter before HTTP request."""
|
||||
self.count += 1
|
||||
|
||||
def after_request(self):
|
||||
"""Decrement the counter after HTTP request."""
|
||||
self.count -= 1
|
||||
|
||||
|
||||
@@ -119,6 +126,7 @@ request_counter.subscribe()
|
||||
|
||||
|
||||
def get_context(obj):
|
||||
"""Compute object's runtime context information."""
|
||||
if isinstance(obj, _cprequest.Request):
|
||||
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
|
||||
elif isinstance(obj, _cprequest.Response):
|
||||
@@ -144,10 +152,12 @@ class GCRoot(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
"""Render the index page HTML content."""
|
||||
return 'Hello, world!'
|
||||
|
||||
@cherrypy.expose
|
||||
def stats(self):
|
||||
"""Render garbage collection statistics page HTML content."""
|
||||
output = ['Statistics:']
|
||||
|
||||
for trial in range(10):
|
||||
|
||||
13
lib/cherrypy/lib/headers.py
Normal file
13
lib/cherrypy/lib/headers.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Deprecated HTTP header parsing helpers."""
|
||||
# for compatibility, expose accidentally here
|
||||
import warnings
|
||||
|
||||
from .._private_api.compat.headers import _parse_param, parse_header # noqa
|
||||
|
||||
|
||||
warnings.warn(
|
||||
'Import `cherrypy._private_api.compat.headers` '
|
||||
'instead of `cherrypy.lib.headers`',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -12,7 +12,6 @@ import email.utils
|
||||
import re
|
||||
import builtins
|
||||
from binascii import b2a_base64
|
||||
from cgi import parse_header
|
||||
from email.header import decode_header
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import unquote_plus
|
||||
@@ -22,6 +21,9 @@ import jaraco.collections
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, ntou
|
||||
|
||||
from .._private_api.compat.headers import parse_header
|
||||
|
||||
|
||||
response_codes = BaseHTTPRequestHandler.responses.copy()
|
||||
|
||||
# From https://github.com/cherrypy/cherrypy/issues/361
|
||||
@@ -78,7 +80,6 @@ def get_ranges(headervalue, content_length):
|
||||
|
||||
If this function returns an empty list, you should return HTTP 416.
|
||||
"""
|
||||
|
||||
if not headervalue:
|
||||
return None
|
||||
|
||||
@@ -130,25 +131,31 @@ class HeaderElement(object):
|
||||
"""An element (with parameters) from an HTTP header's element list."""
|
||||
|
||||
def __init__(self, value, params=None):
|
||||
"""Initialize an HTTP header value representation."""
|
||||
self.value = value
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
def __cmp__(self, other):
|
||||
"""Compare current HTTP header to another by value only."""
|
||||
return builtins.cmp(self.value, other.value)
|
||||
|
||||
def __lt__(self, other):
|
||||
"""Check if this header value is less than the other."""
|
||||
return self.value < other.value
|
||||
|
||||
def __str__(self):
|
||||
"""Render the HTTP header value as a string."""
|
||||
p = [';%s=%s' % (k, v) for k, v in self.params.items()]
|
||||
return str('%s%s' % (self.value, ''.join(p)))
|
||||
|
||||
def __bytes__(self):
|
||||
"""Turn the HTTP header value string representation to bytes."""
|
||||
return ntob(self.__str__())
|
||||
|
||||
def __unicode__(self):
|
||||
"""Render the HTTP header value as a string."""
|
||||
return ntou(self.__str__())
|
||||
|
||||
@staticmethod
|
||||
@@ -180,6 +187,7 @@ class AcceptElement(HeaderElement):
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, elementstr):
|
||||
"""Make an :class:`AcceptElement` instance from a string."""
|
||||
qvalue = None
|
||||
# The first "q" parameter (if any) separates the initial
|
||||
# media-range parameter(s) (if any) from the accept-params.
|
||||
@@ -197,7 +205,7 @@ class AcceptElement(HeaderElement):
|
||||
|
||||
@property
|
||||
def qvalue(self):
|
||||
'The qvalue, or priority, of this value.'
|
||||
"""The qvalue, or priority, of this value."""
|
||||
val = self.params.get('q', '1')
|
||||
if isinstance(val, HeaderElement):
|
||||
val = val.value
|
||||
@@ -215,12 +223,17 @@ class AcceptElement(HeaderElement):
|
||||
) from val_err
|
||||
|
||||
def __cmp__(self, other):
|
||||
"""Compare current header to another by qvalues then strings."""
|
||||
diff = builtins.cmp(self.qvalue, other.qvalue)
|
||||
if diff == 0:
|
||||
diff = builtins.cmp(str(self), str(other))
|
||||
return diff
|
||||
|
||||
def __lt__(self, other):
|
||||
"""Check if this header qvalue is less than the other.
|
||||
|
||||
This method uses string comparison as the second factor.
|
||||
"""
|
||||
if self.qvalue == other.qvalue:
|
||||
return str(self) < str(other)
|
||||
else:
|
||||
@@ -231,7 +244,9 @@ RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
|
||||
|
||||
|
||||
def header_elements(fieldname, fieldvalue):
|
||||
"""Return a sorted HeaderElement list from a comma-separated header string.
|
||||
"""Return a sorted :class:`HeaderElement` list.
|
||||
|
||||
Constucted from a comma-separated header string.
|
||||
"""
|
||||
if not fieldvalue:
|
||||
return []
|
||||
@@ -283,7 +298,6 @@ def valid_status(status):
|
||||
... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED]
|
||||
True
|
||||
"""
|
||||
|
||||
if not status:
|
||||
status = 200
|
||||
|
||||
@@ -322,7 +336,6 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
|
||||
"""Parse a query given as a string argument.
|
||||
|
||||
Arguments:
|
||||
|
||||
qs: URL-encoded query string to be parsed
|
||||
|
||||
keep_blank_values: flag indicating whether blank values in
|
||||
@@ -391,6 +404,7 @@ class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict):
|
||||
|
||||
@staticmethod
|
||||
def transform_key(key):
|
||||
"""Title-case an HTTP header name."""
|
||||
if key is None:
|
||||
# TODO(#1830): why?
|
||||
return 'None'
|
||||
@@ -444,7 +458,8 @@ class HeaderMap(CaseInsensitiveDict):
|
||||
|
||||
@classmethod
|
||||
def encode_header_items(cls, header_items):
|
||||
"""
|
||||
"""Emit tuples of wire-ready HTTP headers.
|
||||
|
||||
Prepare the sequence of name, value tuples into a form suitable for
|
||||
transmitting on the wire for HTTP.
|
||||
"""
|
||||
@@ -456,6 +471,7 @@ class HeaderMap(CaseInsensitiveDict):
|
||||
|
||||
@classmethod
|
||||
def encode_header_item(cls, item):
|
||||
"""Encode an HTTP header for sending over the wire."""
|
||||
if isinstance(item, str):
|
||||
item = cls.encode(item)
|
||||
|
||||
@@ -501,6 +517,7 @@ class Host(object):
|
||||
name = 'unknown.tld'
|
||||
|
||||
def __init__(self, ip, port, name=None):
|
||||
"""Initialize a TCP service representation."""
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
if name is None:
|
||||
@@ -508,11 +525,13 @@ class Host(object):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
"""Render a :class:`Host` instance representation."""
|
||||
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
|
||||
|
||||
|
||||
class SanitizedHost(str):
|
||||
r"""
|
||||
r"""A normalized host header value.
|
||||
|
||||
Wraps a raw host header received from the network in
|
||||
a sanitized version that elides dangerous characters.
|
||||
|
||||
@@ -526,9 +545,11 @@ class SanitizedHost(str):
|
||||
>>> isinstance(SanitizedHost('foobar'), SanitizedHost)
|
||||
False
|
||||
"""
|
||||
|
||||
dangerous = re.compile(r'[\n\r]')
|
||||
|
||||
def __new__(cls, raw):
|
||||
"""Construct a new :class:`SanitizedHost` instance."""
|
||||
sanitized = cls._sanitize(raw)
|
||||
if sanitized == raw:
|
||||
return raw
|
||||
@@ -538,4 +559,5 @@ class SanitizedHost(str):
|
||||
|
||||
@classmethod
|
||||
def _sanitize(cls, raw):
|
||||
"""Clean up the CR LF chars from input."""
|
||||
return cls.dangerous.sub('', raw)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"""JSON tools."""
|
||||
import cherrypy
|
||||
from cherrypy import _json as json
|
||||
from cherrypy._cpcompat import text_or_bytes, ntou
|
||||
@@ -15,7 +16,8 @@ def json_processor(entity):
|
||||
|
||||
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
||||
force=True, debug=False, processor=json_processor):
|
||||
"""Add a processor to parse JSON request entities:
|
||||
"""Add a processor to parse JSON request entities.
|
||||
|
||||
The default processor places the parsed data into request.json.
|
||||
|
||||
Incoming request entities which match the given content_type(s) will
|
||||
@@ -56,6 +58,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
||||
|
||||
|
||||
def json_handler(*args, **kwargs):
|
||||
"""Convert decorated HTTP handler-returned object to JSON string."""
|
||||
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
|
||||
return json.encode(value)
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
"""Lock acquisition helpers."""
|
||||
import datetime
|
||||
|
||||
|
||||
class NeverExpires(object):
|
||||
"""A representation of a never expiring object."""
|
||||
|
||||
def expired(self):
|
||||
"""Communicate that the object hasn't expired."""
|
||||
return False
|
||||
|
||||
|
||||
class Timer(object):
|
||||
"""A simple timer that will indicate when an expiration time has passed."""
|
||||
|
||||
def __init__(self, expiration):
|
||||
'Create a timer that expires at `expiration` (UTC datetime)'
|
||||
"""Create a timer that expires at `expiration` (UTC datetime)."""
|
||||
self.expiration = expiration
|
||||
|
||||
@classmethod
|
||||
@@ -20,18 +25,21 @@ class Timer(object):
|
||||
)
|
||||
|
||||
def expired(self):
|
||||
"""Check whether the timer has expired."""
|
||||
return datetime.datetime.now(
|
||||
datetime.timezone.utc,
|
||||
) >= self.expiration
|
||||
|
||||
|
||||
class LockTimeout(Exception):
|
||||
'An exception when a lock could not be acquired before a timeout period'
|
||||
"""Exception when a lock could not be acquired before a timeout period."""
|
||||
|
||||
|
||||
class LockChecker(object):
|
||||
"""Keep track of the time and detect if a timeout has expired."""
|
||||
|
||||
def __init__(self, session_id, timeout):
|
||||
"""Initialize a lock acquisition tracker."""
|
||||
self.session_id = session_id
|
||||
if timeout:
|
||||
self.timer = Timer.after(timeout)
|
||||
@@ -39,6 +47,7 @@ class LockChecker(object):
|
||||
self.timer = NeverExpires()
|
||||
|
||||
def expired(self):
|
||||
"""Check whether the lock checker has expired."""
|
||||
if self.timer.expired():
|
||||
raise LockTimeout(
|
||||
'Timeout acquiring lock for %(session_id)s' % vars(self))
|
||||
|
||||
@@ -69,8 +69,10 @@ _count = 0
|
||||
|
||||
|
||||
class Profiler(object):
|
||||
"""A profiling app."""
|
||||
|
||||
def __init__(self, path=None):
|
||||
"""Prepare the profiling app resources."""
|
||||
if not path:
|
||||
path = os.path.join(os.path.dirname(__file__), 'profile')
|
||||
self.path = path
|
||||
@@ -88,13 +90,19 @@ class Profiler(object):
|
||||
return result
|
||||
|
||||
def statfiles(self):
|
||||
""":rtype: list of available profiles.
|
||||
"""Compose a list of statistics file names.
|
||||
|
||||
:returns: A list of available profiles.
|
||||
:rtype: list[str]
|
||||
"""
|
||||
return [f for f in os.listdir(self.path)
|
||||
if f.startswith('cp_') and f.endswith('.prof')]
|
||||
|
||||
def stats(self, filename, sortby='cumulative'):
|
||||
""":rtype stats(index): output of print_stats() for the given profile.
|
||||
"""Generate statistics from given profile.
|
||||
|
||||
:returns: The sorted stats index printout.
|
||||
:rtype: str
|
||||
"""
|
||||
sio = io.StringIO()
|
||||
if sys.version_info >= (2, 5):
|
||||
@@ -120,6 +128,7 @@ class Profiler(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
"""Render the profiling viewer index page."""
|
||||
return """<html>
|
||||
<head><title>CherryPy profile data</title></head>
|
||||
<frameset cols='200, 1*'>
|
||||
@@ -131,6 +140,7 @@ class Profiler(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def menu(self):
|
||||
"""Render the profiler menu page html layout."""
|
||||
yield '<h2>Profiling runs</h2>'
|
||||
yield '<p>Click on one of the runs below to see profiling data.</p>'
|
||||
runs = self.statfiles()
|
||||
@@ -141,19 +151,23 @@ class Profiler(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def report(self, filename):
|
||||
"""Render a statistics report."""
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
return self.stats(filename)
|
||||
|
||||
|
||||
class ProfileAggregator(Profiler):
|
||||
"""A profiling aggregator app."""
|
||||
|
||||
def __init__(self, path=None):
|
||||
"""Prepare the profiling aggregator app resources."""
|
||||
Profiler.__init__(self, path)
|
||||
global _count
|
||||
self.count = _count = _count + 1
|
||||
self.profiler = profile.Profile()
|
||||
|
||||
def run(self, func, *args, **params):
|
||||
"""Dump aggeregated profile data into ``self.path``."""
|
||||
path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
|
||||
result = self.profiler.runcall(func, *args, **params)
|
||||
self.profiler.dump_stats(path)
|
||||
@@ -161,6 +175,7 @@ class ProfileAggregator(Profiler):
|
||||
|
||||
|
||||
class make_app:
|
||||
"""Profiling WSGI middleware wrapper."""
|
||||
|
||||
def __init__(self, nextapp, path=None, aggregate=False):
|
||||
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
|
||||
@@ -194,6 +209,7 @@ class make_app:
|
||||
self.profiler = Profiler(path)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Process a WSGI request."""
|
||||
def gather():
|
||||
result = []
|
||||
for line in self.nextapp(environ, start_response):
|
||||
@@ -203,6 +219,7 @@ class make_app:
|
||||
|
||||
|
||||
def serve(path=None, port=8080):
|
||||
"""Serve the web app with profiler activated."""
|
||||
if profile is None or pstats is None:
|
||||
msg = ('Your installation of Python does not have a profile module. '
|
||||
"If you're on Debian, try "
|
||||
|
||||
@@ -92,10 +92,12 @@ class NamespaceSet(dict):
|
||||
handler(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
"""Render representation of a :class:`NamespaceSet` instance."""
|
||||
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
|
||||
dict.__repr__(self))
|
||||
|
||||
def __copy__(self):
|
||||
"""Make a copy of this instance."""
|
||||
newobj = self.__class__()
|
||||
newobj.update(self)
|
||||
return newobj
|
||||
@@ -113,6 +115,7 @@ class Config(dict):
|
||||
namespaces = NamespaceSet()
|
||||
|
||||
def __init__(self, file=None, **kwargs):
|
||||
"""Initialize a CherryPy :class:`Config`."""
|
||||
self.reset()
|
||||
if file is not None:
|
||||
self.update(file)
|
||||
@@ -141,20 +144,24 @@ class Config(dict):
|
||||
self.namespaces(config)
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
"""Assign a config setting."""
|
||||
dict.__setitem__(self, k, v)
|
||||
self.namespaces({k: v})
|
||||
|
||||
|
||||
class Parser(configparser.ConfigParser):
|
||||
"""A parser for the INI-style config file.
|
||||
|
||||
"""Sub-class of ConfigParser that keeps the case of options and that
|
||||
Sub-class of ConfigParser that keeps the case of options and that
|
||||
raises an exception if the file cannot be read.
|
||||
"""
|
||||
|
||||
def optionxform(self, optionstr):
|
||||
"""Keep the option names unedited."""
|
||||
return optionstr
|
||||
|
||||
def read(self, filenames):
|
||||
"""Read the config from files on disk."""
|
||||
if isinstance(filenames, text_or_bytes):
|
||||
filenames = [filenames]
|
||||
for filename in filenames:
|
||||
@@ -186,6 +193,7 @@ class Parser(configparser.ConfigParser):
|
||||
return result
|
||||
|
||||
def dict_from_file(self, file):
|
||||
"""Generate a dict from a file."""
|
||||
if hasattr(file, 'read'):
|
||||
self.read_file(file)
|
||||
else:
|
||||
@@ -235,10 +243,9 @@ class _Builder:
|
||||
return self.build(o.value)
|
||||
|
||||
def _build_call35(self, o):
|
||||
"""
|
||||
Workaround for python 3.5 _ast.Call signature, docs found here
|
||||
https://greentreesnakes.readthedocs.org/en/latest/nodes.html
|
||||
"""
|
||||
"""Emulate ``build_Call`` under Python 3.5."""
|
||||
# Workaround for python 3.5. _ast.Call signature, docs found at
|
||||
# https://greentreesnakes.readthedocs.org/en/latest/nodes.html
|
||||
import ast
|
||||
callee = self.build(o.func)
|
||||
args = []
|
||||
@@ -375,7 +382,6 @@ def modules(modulePath):
|
||||
|
||||
def attributes(full_attribute_name):
|
||||
"""Load a module and retrieve an attribute of that module."""
|
||||
|
||||
# Parse out the path, module, and attribute
|
||||
last_dot = full_attribute_name.rfind('.')
|
||||
attr_name = full_attribute_name[last_dot + 1:]
|
||||
|
||||
@@ -177,6 +177,7 @@ class Session(object):
|
||||
# --------------------- Session management methods --------------------- #
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
"""Initialize the session tool."""
|
||||
self.id_observers = []
|
||||
self._data = {}
|
||||
|
||||
@@ -321,16 +322,19 @@ class Session(object):
|
||||
# -------------------- Application accessor methods -------------------- #
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Retrieve a session-stored object."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Store an object in the session."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Delete object stored in the session."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
del self._data[key]
|
||||
@@ -349,13 +353,15 @@ class Session(object):
|
||||
return self._data.pop(key, default)
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if the session has an object by key."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return key in self._data
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""D.get(k[,d]) -> D[k] if k in D, else d.
|
||||
"""Retrieve a session-stored object.
|
||||
|
||||
D.get(k[,d]) -> D[k] if k in D, else d.
|
||||
d defaults to None.
|
||||
"""
|
||||
if not self.loaded:
|
||||
@@ -363,8 +369,9 @@ class Session(object):
|
||||
return self._data.get(key, default)
|
||||
|
||||
def update(self, d):
|
||||
"""D.update(E) -> None.
|
||||
"""Update multiple session-stored objects in one go.
|
||||
|
||||
D.update(E) -> None.
|
||||
Update D from E: for k in E: D[k] = E[k].
|
||||
"""
|
||||
if not self.loaded:
|
||||
@@ -372,14 +379,18 @@ class Session(object):
|
||||
self._data.update(d)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
|
||||
"""Set a default session key value.
|
||||
|
||||
D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.setdefault(key, default)
|
||||
|
||||
def clear(self):
|
||||
"""D.clear() -> None.
|
||||
"""Clean up the session-stored data.
|
||||
|
||||
D.clear() -> None.
|
||||
Remove all items from D.
|
||||
"""
|
||||
if not self.loaded:
|
||||
@@ -387,25 +398,35 @@ class Session(object):
|
||||
self._data.clear()
|
||||
|
||||
def keys(self):
|
||||
"""D.keys() -> list of D's keys."""
|
||||
"""Return an iterable of session keys.
|
||||
|
||||
D.keys() -> list of D's keys.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.keys()
|
||||
|
||||
def items(self):
|
||||
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
|
||||
"""Return an iterable of items as tuples.
|
||||
|
||||
D.items() -> list of D's (key, value) pairs, as 2-tuples.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.items()
|
||||
|
||||
def values(self):
|
||||
"""D.values() -> list of D's values."""
|
||||
"""Return an iterable of session objects.
|
||||
|
||||
D.values() -> list of D's values.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.values()
|
||||
|
||||
|
||||
class RamSession(Session):
|
||||
"""A memory-baked session store implementation."""
|
||||
|
||||
# Class-level objects. Don't rebind these!
|
||||
cache = {}
|
||||
@@ -413,7 +434,6 @@ class RamSession(Session):
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
|
||||
now = self.now()
|
||||
for _id, (data, expiration_time) in self.cache.copy().items():
|
||||
if expiration_time <= now:
|
||||
@@ -466,8 +486,7 @@ class RamSession(Session):
|
||||
|
||||
|
||||
class FileSession(Session):
|
||||
|
||||
"""Implementation of the File backend for sessions
|
||||
"""Implementation of the file backend for sessions.
|
||||
|
||||
storage_path
|
||||
The folder where session data will be saved. Each session
|
||||
@@ -485,6 +504,7 @@ class FileSession(Session):
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
"""Prepare the file session store."""
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
kwargs.setdefault('lock_timeout', None)
|
||||
@@ -614,6 +634,7 @@ class FileSession(Session):
|
||||
|
||||
|
||||
class MemcachedSession(Session):
|
||||
"""A Memcached-baked session store."""
|
||||
|
||||
# The most popular memcached client for Python isn't thread-safe.
|
||||
# Wrap all .get and .set operations in a single lock.
|
||||
@@ -687,7 +708,6 @@ class MemcachedSession(Session):
|
||||
|
||||
def save():
|
||||
"""Save any changed session data."""
|
||||
|
||||
if not hasattr(cherrypy.serving, 'session'):
|
||||
return
|
||||
request = cherrypy.serving.request
|
||||
@@ -783,7 +803,6 @@ def init(storage_type=None, path=None, path_header=None, name='session_id',
|
||||
and may be specific to the storage type. See the subclass of Session
|
||||
you're using for more information.
|
||||
"""
|
||||
|
||||
# Py27 compat
|
||||
storage_class = kwargs.pop('storage_class', RamSession)
|
||||
|
||||
@@ -898,7 +917,8 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
|
||||
|
||||
|
||||
def _add_MSIE_max_age_workaround(cookie, timeout):
|
||||
"""
|
||||
"""Inject a Microsoft Internet Explorer ``max-age`` workaround.
|
||||
|
||||
We'd like to use the "max-age" param as indicated in
|
||||
http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
|
||||
save it to disk and the session is lost if people close
|
||||
|
||||
@@ -38,6 +38,7 @@ class SimplePlugin(object):
|
||||
"""
|
||||
|
||||
def __init__(self, bus):
|
||||
"""Initialize a simple plugin."""
|
||||
self.bus = bus
|
||||
|
||||
def subscribe(self):
|
||||
@@ -93,6 +94,7 @@ class SignalHandler(object):
|
||||
del k, v
|
||||
|
||||
def __init__(self, bus):
|
||||
"""Initialize a signal handler plugin."""
|
||||
self.bus = bus
|
||||
# Set default handlers
|
||||
self.handlers = {'SIGTERM': self.bus.exit,
|
||||
@@ -117,8 +119,7 @@ class SignalHandler(object):
|
||||
self.bus.exit()
|
||||
|
||||
def _is_daemonized(self):
|
||||
"""Return boolean indicating if the current process is
|
||||
running as a daemon.
|
||||
"""Check if current process is running as a daemon.
|
||||
|
||||
The criteria to determine the `daemon` condition is to verify
|
||||
if the current pid is not the same as the one that got used on
|
||||
@@ -223,6 +224,7 @@ class DropPrivileges(SimplePlugin):
|
||||
"""
|
||||
|
||||
def __init__(self, bus, umask=None, uid=None, gid=None):
|
||||
"""Initialize the privilege dropping plugin."""
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.finalized = False
|
||||
self.uid = uid
|
||||
@@ -288,6 +290,7 @@ class DropPrivileges(SimplePlugin):
|
||||
self._umask = val
|
||||
|
||||
def start(self):
|
||||
"""Drop the process privileges."""
|
||||
# uid/gid
|
||||
def current_ids():
|
||||
"""Return the current (uid, gid) if available."""
|
||||
@@ -353,6 +356,7 @@ class Daemonizer(SimplePlugin):
|
||||
|
||||
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
|
||||
stderr='/dev/null'):
|
||||
"""Initialize the daemonizer plugin."""
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
@@ -360,6 +364,7 @@ class Daemonizer(SimplePlugin):
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
"""Attempt to daemonize the process."""
|
||||
if self.finalized:
|
||||
self.bus.log('Already deamonized.')
|
||||
|
||||
@@ -382,6 +387,7 @@ class Daemonizer(SimplePlugin):
|
||||
def daemonize(
|
||||
stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
|
||||
logger=lambda msg: None):
|
||||
"""Daemonize the process."""
|
||||
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
|
||||
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
|
||||
@@ -428,11 +434,13 @@ class PIDFile(SimplePlugin):
|
||||
"""Maintain a PID file via a WSPBus."""
|
||||
|
||||
def __init__(self, bus, pidfile):
|
||||
"""Initialize the PID file plugin."""
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.pidfile = pidfile
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
"""Write a PID file to disk."""
|
||||
pid = os.getpid()
|
||||
if self.finalized:
|
||||
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
|
||||
@@ -444,6 +452,7 @@ class PIDFile(SimplePlugin):
|
||||
start.priority = 70
|
||||
|
||||
def exit(self):
|
||||
"""Delete the PID file from disk."""
|
||||
try:
|
||||
os.remove(self.pidfile)
|
||||
self.bus.log('PID file removed: %r.' % self.pidfile)
|
||||
@@ -462,11 +471,12 @@ class PerpetualTimer(threading.Timer):
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Override parent constructor to allow 'bus' to be provided."
|
||||
"""Override parent constructor to allow 'bus' to be provided."""
|
||||
self.bus = kwargs.pop('bus', None)
|
||||
super(PerpetualTimer, self).__init__(*args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
"""Run an infinitely repeated callable."""
|
||||
while True:
|
||||
self.finished.wait(self.interval)
|
||||
if self.finished.isSet():
|
||||
@@ -494,6 +504,7 @@ class BackgroundTask(threading.Thread):
|
||||
"""
|
||||
|
||||
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
|
||||
"""Initialize a background task parameters."""
|
||||
super(BackgroundTask, self).__init__()
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
@@ -506,9 +517,11 @@ class BackgroundTask(threading.Thread):
|
||||
self.daemon = True
|
||||
|
||||
def cancel(self):
|
||||
"""Set a task cancellation flag."""
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
"""Start running the repeated background task in the loop."""
|
||||
self.running = True
|
||||
while self.running:
|
||||
time.sleep(self.interval)
|
||||
@@ -539,6 +552,7 @@ class Monitor(SimplePlugin):
|
||||
"""
|
||||
|
||||
def __init__(self, bus, callback, frequency=60, name=None):
|
||||
"""Initialize the monitor plugin."""
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.callback = callback
|
||||
self.frequency = frequency
|
||||
@@ -611,6 +625,7 @@ class Autoreloader(Monitor):
|
||||
"""A regular expression by which to match filenames."""
|
||||
|
||||
def __init__(self, bus, frequency=1, match='.*'):
|
||||
"""Initialize the auto-reloader monitor plugin."""
|
||||
self.mtimes = {}
|
||||
self.files = set()
|
||||
self.match = match
|
||||
@@ -717,6 +732,7 @@ class ThreadManager(SimplePlugin):
|
||||
"""A map of {thread ident: index number} pairs."""
|
||||
|
||||
def __init__(self, bus):
|
||||
"""Initialize the thread manager plugin."""
|
||||
self.threads = {}
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.bus.listeners.setdefault('acquire_thread', set())
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
r"""
|
||||
Server interfaces.
|
||||
|
||||
Starting in CherryPy 3.1, cherrypy.server is implemented as an
|
||||
:ref:`Engine Plugin<plugins>`. It's an instance of
|
||||
:class:`cherrypy._cpserver.Server`, which is a subclass of
|
||||
@@ -127,6 +129,8 @@ import portend
|
||||
|
||||
|
||||
class Timeouts:
|
||||
"""Timeout constants."""
|
||||
|
||||
occupied = 5
|
||||
free = 1
|
||||
|
||||
@@ -146,6 +150,7 @@ class ServerAdapter(object):
|
||||
"""
|
||||
|
||||
def __init__(self, bus, httpserver=None, bind_addr=None):
|
||||
"""Initialize the HTTP server plugin."""
|
||||
self.bus = bus
|
||||
self.httpserver = httpserver
|
||||
self.bind_addr = bind_addr
|
||||
@@ -153,10 +158,12 @@ class ServerAdapter(object):
|
||||
self.running = False
|
||||
|
||||
def subscribe(self):
|
||||
"""Subscribe control methods to the bus lifecycle events."""
|
||||
self.bus.subscribe('start', self.start)
|
||||
self.bus.subscribe('stop', self.stop)
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unsubcribe control methods to the bus lifecycle events."""
|
||||
self.bus.unsubscribe('start', self.start)
|
||||
self.bus.unsubscribe('stop', self.stop)
|
||||
|
||||
@@ -212,7 +219,9 @@ class ServerAdapter(object):
|
||||
return '%s://%s' % (scheme, host)
|
||||
|
||||
def _start_http_thread(self):
|
||||
"""HTTP servers MUST be running in new threads, so that the
|
||||
"""Start the HTTP server thread.
|
||||
|
||||
HTTP servers MUST be running in new threads, so that the
|
||||
main thread persists to receive KeyboardInterrupt's. If an
|
||||
exception is raised in the httpserver's thread then it's
|
||||
trapped here, and the bus (and therefore our httpserver)
|
||||
@@ -258,9 +267,10 @@ class ServerAdapter(object):
|
||||
|
||||
@property
|
||||
def bound_addr(self):
|
||||
"""
|
||||
The bind address, or if it's an ephemeral port and the
|
||||
socket has been bound, return the actual port bound.
|
||||
"""The bind address.
|
||||
|
||||
If it's an ephemeral port and the socket has been bound,
|
||||
return the actual port bound.
|
||||
"""
|
||||
host, port = self.bind_addr
|
||||
if port == 0 and self.httpserver.socket:
|
||||
@@ -292,6 +302,7 @@ class FlupCGIServer(object):
|
||||
"""Adapter for a flup.server.cgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the flup CGI Server plugin."""
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
@@ -315,6 +326,7 @@ class FlupFCGIServer(object):
|
||||
"""Adapter for a flup.server.fcgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the FCGI server parameters."""
|
||||
if kwargs.get('bindAddress', None) is None:
|
||||
import socket
|
||||
if not hasattr(socket, 'fromfd'):
|
||||
@@ -360,6 +372,7 @@ class FlupSCGIServer(object):
|
||||
"""Adapter for a flup.server.scgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the SCGI server parameters."""
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
@@ -395,7 +408,8 @@ class FlupSCGIServer(object):
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _safe_wait(host, port):
|
||||
"""
|
||||
"""Warn when bind interface is ambiguous.
|
||||
|
||||
On systems where a loopback interface is not available and the
|
||||
server is bound to all interfaces, it's difficult to determine
|
||||
whether the server is in fact occupying the port. In this case,
|
||||
|
||||
@@ -17,10 +17,12 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
|
||||
|
||||
def __init__(self, bus):
|
||||
"""Initialize the console control handler."""
|
||||
self.is_set = False
|
||||
plugins.SimplePlugin.__init__(self, bus)
|
||||
|
||||
def start(self):
|
||||
"""Register handling of the console control events."""
|
||||
if self.is_set:
|
||||
self.bus.log('Handler for console events already set.', level=20)
|
||||
return
|
||||
@@ -34,6 +36,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
self.is_set = True
|
||||
|
||||
def stop(self):
|
||||
"""Unregister the console control handlers."""
|
||||
if not self.is_set:
|
||||
self.bus.log('Handler for console events already off.', level=20)
|
||||
return
|
||||
@@ -78,6 +81,7 @@ class Win32Bus(wspbus.Bus):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a Win32 bus implementation."""
|
||||
self.events = {}
|
||||
wspbus.Bus.__init__(self)
|
||||
|
||||
@@ -94,10 +98,12 @@ class Win32Bus(wspbus.Bus):
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""The bus state."""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
"""Set the bus state."""
|
||||
self._state = value
|
||||
event = self._get_state_event(value)
|
||||
win32event.PulseEvent(event)
|
||||
@@ -144,6 +150,7 @@ control_codes = _ControlCodes({'graceful': 138})
|
||||
|
||||
|
||||
def signal_child(service, command):
|
||||
"""Send a control command to a service."""
|
||||
if command == 'stop':
|
||||
win32serviceutil.StopService(service)
|
||||
elif command == 'restart':
|
||||
@@ -165,16 +172,19 @@ class PyWebService(win32serviceutil.ServiceFramework):
|
||||
_svc_description_ = 'Python Web Service'
|
||||
|
||||
def SvcDoRun(self):
|
||||
"""Start the service."""
|
||||
from cherrypy import process
|
||||
process.bus.start()
|
||||
process.bus.block()
|
||||
|
||||
def SvcStop(self):
|
||||
"""Stop the service."""
|
||||
from cherrypy import process
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
process.bus.exit()
|
||||
|
||||
def SvcOther(self, control):
|
||||
"""Send a command to the service."""
|
||||
from cherrypy import process
|
||||
process.bus.publish(control_codes.key_for(control))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user