pesto-25/0000755000175100017510000000000011613356014013260 5ustar oliveroliver00000000000000pesto-25/MANIFEST.in0000644000175100017510000000012511613355542015021 0ustar oliveroliver00000000000000include *.txt recursive-include doc *.rst *.py Makefile recursive-include tests *.py pesto-25/pesto/0000755000175100017510000000000011613356014014412 5ustar oliveroliver00000000000000pesto-25/pesto/utils.py0000644000175100017510000005603011613355542016135 0ustar oliveroliver00000000000000# Copyright (c) 2009, 2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ General utility functions used within pesto. These functions are reserved for internal usage and it is not recommended that users of the API call these functions directly """ from collections import deque, defaultdict try: from collections import MutableMapping except ImportError: from UserDict import DictMixin class MutableMapping(DictMixin): """ Emulate MutableMapping in Python<2.6 """ from cStringIO import StringIO, OutputType as cStringIO_OutputType from itertools import chain, repeat from shutil import copyfileobj from tempfile import TemporaryFile class MultiDict(MutableMapping): """ Dictionary-like object that supports multiple values per key. Insertion order is preserved. Synopsis:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d['a'] 1 >>> d['b'] 3 >>> d.getlist('a') [1, 2] >>> d.getlist('b') [3] >>> list(d.iterallitems()) [('a', 1), ('a', 2), ('b', 3)] """ setdefault = MutableMapping.setdefault __contains__ = MutableMapping.__contains__ def __init__(self, *args, **kwargs): """ MultiDicts can be constructed in the following ways: 1. From a sequence of ``(key, value)`` pairs:: >>> MultiDict([('a', 1), ('a', 2)]) MultiDict([('a', 1), ('a', 2)]) 2. Initialized from another MultiDict:: >>> d = MultiDict([('a', 1), ('a', 2)]) >>> MultiDict(d) MultiDict([('a', 1), ('a', 2)]) 3. Initialized from a regular dict:: >>> MultiDict({'a': 1}) MultiDict([('a', 1)]) 4. From keyword arguments:: >>> MultiDict(a=1) MultiDict([('a', 1)]) """ if len(args) > 1: raise TypeError, self.__class__.__name__ \ + " expected at most 2 arguments, got " + repr(1 + len(args)) self.clear() self.update(*args, **kwargs) def __getitem__(self, key): """ Return the first item associated with ``key``:: >>> d = MultiDict([('a', 1), ('a', 2)]) >>> d['a'] 1 """ try: return self._dict[key][0] except IndexError: raise KeyError(key) def __setitem__(self, key, value): """ Set the items associated with key to a single item, ``value``. >>> d = MultiDict() >>> d['b'] = 3 >>> d MultiDict([('b', 3)]) """ _order = [(k, v) for k, v in self._order if k != key] _order.append((key, value)) self._order = _order self._dict[key] = [value] def __delitem__(self, key): """ Delete all values associated with ``key`` """ del self._dict[key] self._order = [(k, v) for (k, v) in self._order if k != key] def __iter__(self): """ Return an iterator over all keys """ return (k for k in self._dict) def get(self, key, default=None): """ Return the first available value for key ``key``, or ``default`` if no such value exists:: >>> d = MultiDict([('a', 1), ('a', 2)]) >>> d.get('a') 1 >>> print d.get('b') None """ return self._dict.get(key, [default])[0] def getlist(self, key): """ Return a list of all values for key ``key``. """ return self._dict.get(key, []) def copy(self): """ Return a shallow copy of the dictionary:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> copy = d.copy() >>> copy MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> copy is d False """ return self.__class__(self) def fromkeys(cls, seq, value=None): """ Create a new MultiDict with keys from seq and values set to value. Example:: >>> MultiDict.fromkeys(['a', 'b']) MultiDict([('a', None), ('b', None)]) Keys can be repeated:: >>> d = MultiDict.fromkeys(['a', 'b', 'a']) >>> d.getlist('a') [None, None] >>> d.getlist('b') [None] """ return cls(zip(seq, repeat(value))) fromkeys = classmethod(fromkeys) def items(self): """ Return a list of ``(key, value)`` tuples. Only the first ``(key, value)`` is returned where keys have multiple values:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.items() [('a', 1), ('b', 3)] """ return list(self.iteritems()) def iteritems(self): """ Like ``items``, but return an iterator rather than a list. """ seen = set() for k, v in self._order: if k in seen: continue yield k, v seen.add(k) def listitems(self): """ Like ``items``, but returns lists of values:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.listitems() [('a', [1, 2]), ('b', [3])] """ return list(self.iterlistitems()) def iterlistitems(self): """ Like ``listitems``, but return an iterator rather than a list. """ for k in self.iterkeys(): yield k, self._dict[k] def allitems(self): """ Return a list of ``(key, value)`` pairs for each item in the MultiDict. Items with multiple keys will have multiple key-value pairs returned:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.allitems() [('a', 1), ('a', 2), ('b', 3)] """ return list(self.iterallitems()) def iterallitems(self): """ Like ``allitems``, but return an iterator rather than a list. """ return iter(self._order) def keys(self): """ Return dictionary keys. Each key is returned only once, even if multiple values are present. >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.keys() ['a', 'b'] """ return list(self.iterkeys()) def iterkeys(self): """ Iterator for dictionary keys. Each key is returned only once, even if multiple values are present. >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.keys() ['a', 'b'] """ return (k for k, v in self.iteritems()) def values(self): """ Return values from the dictionary. Where keys have multiple values, only the first is returned:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.values() [1, 3] """ return list(self.itervalues()) def itervalues(self): """ Like ``values``, but return an iterator rather than a list. """ return (v for k, v in self.iteritems()) def listvalues(self): """ Return a list of values. Each item returned is a list of values associated with a single key. Example usage:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.listvalues() [[1, 2], [3]] """ return list(self.iterlistvalues()) def iterlistvalues(self): """ Like ``listvalues``, but returns an iterator over the results instead of a list. """ return (self._dict[k] for k in self.iterkeys()) def pop(self, key, *args): """ Dictionary ``pop`` method. Return and remove the value associated with ``key``. If more than one value is associated with ``key``, only the first is returned. Example usage:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.pop('a') 1 >>> d MultiDict([('a', 2), ('b', 3)]) >>> d.pop('a') 2 >>> d MultiDict([('b', 3)]) >>> d.pop('a') Traceback (most recent call last): ... KeyError: 'a' """ if len(args) > 1: raise TypeError, "pop expected at most 2 arguments, got "\ + repr(1 + len(args)) try: value = self._dict[key].pop(0) except (KeyError, IndexError): if args: return args[0] raise KeyError(key) self._order.remove((key, value)) return value def popitem(self): """ Return and remove a ``(key, value)`` pair from the dictionary. The item popped is always the most recently added key and the first value associated with it:: >>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)]) >>> d.popitem() ('b', 3) >>> d.popitem() ('a', 1) >>> d.popitem() ('a', 2) >>> d.popitem() #doctest: +ELLIPSIS Traceback (most recent call last): ... KeyError: 'popitem(): dictionary is empty' """ try: key = self._order[-1][0] except IndexError: raise KeyError('popitem(): dictionary is empty') return key, self.pop(key) def update(self, *args, **kwargs): """ Update the MultiDict from another MultiDict, regular dictionary or a iterable of ``(key, value)`` pairs. New keys overwrite old keys - use :meth:`extend` if you want new keys to be added to old keys. Updating from another MultiDict:: >>> d = MultiDict([('name', 'eric'), ('occupation', 'lumberjack')]) >>> d.update(MultiDict([('mood', 'okay')])) >>> d MultiDict([('name', 'eric'), ('occupation', 'lumberjack'), ('mood', 'okay')]) from a dictionary:: >>> d = MultiDict([('name', 'eric'), ('hobby', 'shopping')]) >>> d.update({'hobby': 'pressing wild flowers'}) >>> d MultiDict([('name', 'eric'), ('hobby', 'pressing wild flowers')]) an iterable of ``(key, value)`` pairs:: >>> d = MultiDict([('name', 'eric'), ('occupation', 'lumberjack')]) >>> d.update([('hobby', 'shopping'), ('hobby', 'pressing wild flowers')]) >>> d MultiDict([('name', 'eric'), ('occupation', 'lumberjack'), ('hobby', 'shopping'), ('hobby', 'pressing wild flowers')]) or keyword arguments:: >>> d = MultiDict([('name', 'eric'), ('occupation', 'lumberjack')]) >>> d.update(mood='okay') """ if len(args) > 1: raise TypeError, "update expected at most 1 argument, got "\ + repr(1 + len(args)) if args: other = args[0] else: other = [] return self._update(other, True, **kwargs) def extend(self, *args, **kwargs): """ Extend the MultiDict with another MultiDict, regular dictionary or a iterable of ``(key, value)`` pairs. This is similar to :meth:`update` except that new keys are added to old keys. """ if len(args) > 1: raise TypeError, "extend expected at most 1 argument, got "\ + repr(1 + len(args)) if args: other = args[0] else: other = [] return self._update(other, False, **kwargs) def _update(self, *args, **kwargs): """ Update the MultiDict from another object and optionally kwargs. :param other: the other MultiDict, dict, or iterable (first positional arg) :param replace: if ``True``, entries will replace rather than extend existing entries (second positional arg) """ other, replace = args if isinstance(other, self.__class__): items = other.allitems() elif isinstance(other, dict): items = list(other.iteritems()) else: items = list(other) if kwargs: items += list(kwargs.items()) if replace: replaced = set(k for k, v in items if k in self._dict) self._order = [(k, v) for (k, v) in self._order if k not in replaced] for key in replaced: self._dict[key] = [] for k, v in items: self._dict.setdefault(k, []).append(v) self._order.append((k, v)) def __repr__(self): """ ``__repr__`` representation of object """ return '%s(%r)' % (self.__class__.__name__, self.allitems()) def __str__(self): """ ``__str__`` representation of object """ return repr(self) def __len__(self): """ Return the total number of keys stored. """ return len(self._dict) def __eq__(self, other): return isinstance(other, MultiDict) \ and self._order == other._order def __ne__(self, other): return not (self == other) def clear(self): self._order = [] self._dict = {} class ReadlinesMixin(object): """ Mixin that defines readlines and the iterator interface in terms of underlying readline method. """ def readlines(self, sizehint=0): size = 0 lines = [] for line in iter(self.readline, ''): lines.append(line) size += len(line) if 0 < sizehint <= size: break return lines def __iter__(self): return self def next(self): return self.readline() class PutbackInput(ReadlinesMixin): r""" Wrap a file-like object to allow data read to be returned to the buffer. Only supports serial read-access, ie no seek or write methods. Example:: >>> from StringIO import StringIO >>> s = StringIO("the rain in spain\nfalls mainly\non the plain\n") >>> p = PutbackInput(s) >>> line = p.readline() >>> line 'the rain in spain\n' >>> p.putback(line) >>> p.readline() 'the rain in spain\n' """ def __init__(self, io): """ Initialize a ``PutbackInput`` object from input stream ``io``. """ self._io = io self._putback = deque() def read(self, size=-1): """ Return up to ``size`` bytes from the input stream. """ if size < 0: result = ''.join(self._putback) + self._io.read() self._putback.clear() return result buf = [] remaining = size while remaining > 0 and self._putback: chunk = self._putback.popleft() excess = len(chunk) - remaining if excess > 0: chunk, p = chunk[:-excess], chunk[-excess:] self.putback(p) buf.append(chunk) remaining -= len(chunk) if remaining > 0: buf.append(self._io.read(remaining)) return ''.join(buf) def readline(self, size=-1): """ Read a single line of up to ``size`` bytes from the input stream. """ remaining = size buf = [] while self._putback and (size < 0 or remaining > 0): chunk = self._putback.popleft() if size > 0: excess = len(chunk) - remaining if excess > 0: chunk, p = chunk[:-excess], chunk[-excess:] self.putback(p) pos = chunk.find('\n') if pos >= 0: chunk, p = chunk[:(pos+1)], chunk[(pos+1):] self.putback(p) buf.append(chunk) return ''.join(buf) buf.append(chunk) remaining -= len(chunk) if size > 0: buf.append(self._io.readline(remaining)) else: buf.append(self._io.readline()) return ''.join(buf) def putback(self, data): """ Put ``data`` back into the stream """ self._putback.appendleft(data) def peek(self, size): """ Peek ahead ``size`` bytes from the stream without consuming any data """ peeked = self.read(size) self.putback(peeked) return peeked class SizeLimitedInput(ReadlinesMixin): r""" Wrap an IO object to prevent reading beyond ``length`` bytes. Example:: >>> from StringIO import StringIO >>> s = StringIO("the rain in spain\nfalls mainly\non the plain\n") >>> s = SizeLimitedInput(s, 24) >>> len(s.read()) 24 >>> s.seek(0) >>> s.read() 'the rain in spain\nfalls ' >>> s.seek(0) >>> s.readline() 'the rain in spain\n' >>> s.readline() 'falls ' """ def __init__(self, io, length): self._io = io self.length = length self.pos = 0 def check_available(self, requested): """ Return the minimum of ``requested`` and the number of bytes available in the stream. If ``requested`` is negative, return the number of bytes available in the stream. """ if requested < 0: return self.length - self.pos else: return min(self.length - self.pos, requested) def tell(self): """ Return the position of the file pointer in the stream. """ return self.pos def seek(self, pos, whence=0): """ Seek to position ``pos``. This is a wrapper for the underlying IO's ``seek`` method. """ self._io.seek(pos, whence) self.pos = self._io.tell() def read(self, size=-1): """ Return up to ``size`` bytes from the input stream. """ size = self.check_available(size) result = self._io.read(size) self.pos += len(result) return result def readline(self, size=-1): """ Read a single line of up to ``size`` bytes from the input stream. """ size = self.check_available(size) result = self._io.readline(self.check_available(size)) self.pos += len(result) return result class DelimitedInput(ReadlinesMixin): r""" Wrap a PutbackInput to read as far as a delimiter (after which subsequent reads will return empty strings, as if EOF was reached) Examples:: >>> from StringIO import StringIO >>> s = StringIO('one--two--three') >>> s.seek(0) >>> p = PutbackInput(s) >>> DelimitedInput(p, '--').read() 'one' >>> DelimitedInput(p, '--').read() 'two' >>> DelimitedInput(p, '--').read() 'three' >>> DelimitedInput(p, '--').read() '' """ def __init__(self, io, delimiter, consume_delimiter=True): """ Initialize an instance of ``DelimitedInput``. """ if not getattr(io, 'putback', None): raise TypeError("Need an instance of PutbackInput") self._io = io self.delimiter = delimiter self.consume_delimiter = consume_delimiter self.delimiter_found = False def read(self, size=-1): """ Return up to ``size`` bytes of data from the stream until EOF or ``delimiter`` is reached. """ if self.delimiter_found: return '' MAX_BLOCK_SIZE = 8 * 1024 if size == -1: return ''.join(iter(lambda: self.read(MAX_BLOCK_SIZE), '')) data = self._io.read(size + len(self.delimiter)) pos = data.find(self.delimiter) if pos >= 0: if self.consume_delimiter: putback = data[pos+len(self.delimiter):] else: putback = data[pos:] self.delimiter_found = True self._io.putback(putback) return data[:pos] elif len(data) == size + len(self.delimiter): self._io.putback(data[-len(self.delimiter):]) return data[:-len(self.delimiter)] else: return data def readline(self, size=-1): """ Read a single line of up to ``size`` bytes from the input stream, or up to ``delimiter`` if this is encountered before a complete line is read. """ if self.delimiter_found: return '' line = self._io.readline(size) extra = self._io.read(len(self.delimiter)) if self.delimiter not in line+extra: self._io.putback(extra) return line data = line + extra pos = data.find(self.delimiter) if pos >= 0: if self.consume_delimiter: putback = data[pos+len(self.delimiter):] else: putback = data[pos:] self.delimiter_found = True self._io.putback(putback) return data[:pos] elif len(data) == size + len(self.delimiter): self._io.putback(data[-len(self.delimiter):]) return data[:-len(self.delimiter)] else: return data class ExpandableOutput(object): """ Write-only output object. Will store data in a StringIO, until more than ``bufsize`` bytes are written, at which point it will switch to storing data in a real file object. """ def __init__(self, bufsize=16384): """ Initialize an ``ExpandableOutput`` instance. """ self._io = StringIO() self.bufsize = bufsize self.write = self.write_stringio self.exceeded_bufsize = False def write_stringio(self, data): """ ``write``, optimized for the StringIO backend. """ if isinstance(self._io, cStringIO_OutputType) and self.tell() + len(data) > self.bufsize: self.switch_to_file_storage() return self.write_file(data) return self._io.write(data) def write_file(self, data): """ ``write``, optimized for the TemporaryFile backend """ return self._io.write(data) def switch_to_file_storage(self): """ Switch the storage backend to an instance of ``TemporaryFile``. """ self.exceeded_bufsize = True oldio = self._io try: self._io.seek(0) self._io = TemporaryFile() copyfileobj(oldio, self._io) finally: oldio.close() self.write = self.write_file def __getattr__(self, attr): """ Delegate to ``self._io``. """ return getattr(self._io, attr) def __enter__(self): """ Support for context manager ``__enter__``/``__exit__`` blocks """ return self def __exit__(self, type, value, traceback): """ Support for context manager ``__enter__``/``__exit__`` blocks """ self._io.close() # propagate exceptions return False pesto-25/pesto/response.py0000644000175100017510000007144411613355542016641 0ustar oliveroliver00000000000000# Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.response -------------- Response object for WSGI applications """ import cgi import datetime import re import urllib import copy from itertools import chain from pesto import cookie __all__ = [ 'STATUS_CONTINUE', 'STATUS_SWITCHING_PROTOCOLS', 'STATUS_OK', 'STATUS_CREATED', 'STATUS_ACCEPTED', 'STATUS_NON_AUTHORITATIVE_INFORMATION', 'STATUS_NO_CONTENT', 'STATUS_RESET_CONTENT', 'STATUS_PARTIAL_CONTENT', 'STATUS_MULTIPLE_CHOICES', 'STATUS_MOVED_PERMANENTLY', 'STATUS_FOUND', 'STATUS_SEE_OTHER', 'STATUS_NOT_MODIFIED', 'STATUS_USE_PROXY', 'STATUS_TEMPORARY_REDIRECT', 'STATUS_BAD_REQUEST', 'STATUS_UNAUTHORIZED', 'STATUS_PAYMENT_REQUIRED', 'STATUS_FORBIDDEN', 'STATUS_NOT_FOUND', 'STATUS_METHOD_NOT_ALLOWED', 'STATUS_NOT_ACCEPTABLE', 'STATUS_PROXY_AUTHENTICATION_REQUIRED', 'STATUS_REQUEST_TIME_OUT', 'STATUS_CONFLICT', 'STATUS_GONE', 'STATUS_LENGTH_REQUIRED', 'STATUS_PRECONDITION_FAILED', 'STATUS_REQUEST_ENTITY_TOO_LARGE', 'STATUS_REQUEST_URI_TOO_LARGE', 'STATUS_UNSUPPORTED_MEDIA_TYPE', 'STATUS_REQUESTED_RANGE_NOT_SATISFIABLE', 'STATUS_EXPECTATION_FAILED', 'STATUS_INTERNAL_SERVER_ERROR', 'STATUS_NOT_IMPLEMENTED', 'STATUS_BAD_GATEWAY', 'STATUS_SERVICE_UNAVAILABLE', 'STATUS_GATEWAY_TIME_OUT', 'STATUS_HTTP_VERSION_NOT_SUPPORTED', 'Response' ] # All HTTP/1.1 status codes as listed in http://www.ietf.org/rfc/rfc2616.txt HTTP_STATUS_CODES = { 100 : 'Continue', 101 : 'Switching Protocols', 200 : 'OK', 201 : 'Created', 202 : 'Accepted', 203 : 'Non-Authoritative Information', 204 : 'No Content', 205 : 'Reset Content', 206 : 'Partial Content', 300 : 'Multiple Choices', 301 : 'Moved Permanently', 302 : 'Found', 303 : 'See Other', 304 : 'Not Modified', 305 : 'Use Proxy', 307 : 'Temporary Redirect', 400 : 'Bad Request', 401 : 'Unauthorized', 402 : 'Payment Required', 403 : 'Forbidden', 404 : 'Not Found', 405 : 'Method Not Allowed', 406 : 'Not Acceptable', 407 : 'Proxy Authentication Required', 408 : 'Request Time-out', 409 : 'Conflict', 410 : 'Gone', 411 : 'Length Required', 412 : 'Precondition Failed', 413 : 'Request Entity Too Large', 414 : 'Request-URI Too Large', 415 : 'Unsupported Media Type', 416 : 'Requested range not satisfiable', 417 : 'Expectation Failed', 500 : 'Internal Server Error', 501 : 'Not Implemented', 502 : 'Bad Gateway', 503 : 'Service Unavailable', 504 : 'Gateway Time-out', 505 : 'HTTP Version not supported', } # Symbolic names for the HTTP status codes STATUS_CONTINUE = 100 STATUS_SWITCHING_PROTOCOLS = 101 STATUS_OK = 200 STATUS_CREATED = 201 STATUS_ACCEPTED = 202 STATUS_NON_AUTHORITATIVE_INFORMATION = 203 STATUS_NO_CONTENT = 204 STATUS_RESET_CONTENT = 205 STATUS_PARTIAL_CONTENT = 206 STATUS_MULTIPLE_CHOICES = 300 STATUS_MOVED_PERMANENTLY = 301 STATUS_FOUND = 302 STATUS_SEE_OTHER = 303 STATUS_NOT_MODIFIED = 304 STATUS_USE_PROXY = 305 STATUS_TEMPORARY_REDIRECT = 307 STATUS_BAD_REQUEST = 400 STATUS_UNAUTHORIZED = 401 STATUS_PAYMENT_REQUIRED = 402 STATUS_FORBIDDEN = 403 STATUS_NOT_FOUND = 404 STATUS_METHOD_NOT_ALLOWED = 405 STATUS_NOT_ACCEPTABLE = 406 STATUS_PROXY_AUTHENTICATION_REQUIRED = 407 STATUS_REQUEST_TIME_OUT = 408 STATUS_CONFLICT = 409 STATUS_GONE = 410 STATUS_LENGTH_REQUIRED = 411 STATUS_PRECONDITION_FAILED = 412 STATUS_REQUEST_ENTITY_TOO_LARGE = 413 STATUS_REQUEST_URI_TOO_LARGE = 414 STATUS_UNSUPPORTED_MEDIA_TYPE = 415 STATUS_REQUESTED_RANGE_NOT_SATISFIABLE = 416 STATUS_EXPECTATION_FAILED = 417 STATUS_INTERNAL_SERVER_ERROR = 500 STATUS_NOT_IMPLEMENTED = 501 STATUS_BAD_GATEWAY = 502 STATUS_SERVICE_UNAVAILABLE = 503 STATUS_GATEWAY_TIME_OUT = 504 STATUS_HTTP_VERSION_NOT_SUPPORTED = 505 def encoder(stream, charset): r""" Encode a response iterator using the given character set. Example usage:: >>> list(encoder([u'Price \u00a3200'], 'latin1')) ['Price \xa3200'] """ if charset is None: charset = 'UTF-8' for chunk in stream: if isinstance(chunk, unicode): yield chunk.encode(charset) else: yield chunk class Response(object): """ WSGI Response object. """ default_content_type = "text/html; charset=UTF-8" def __init__(self, content=None, status="200 OK", headers=None, onclose=None, add_default_content_type=True, **kwargs): r""" :param content: An iterator over the response content :param status: The WSGI status line, eg ``200 OK`` or ``404 Not Found``. :param headers: A list of headers, eg ``[('Content-Type', 'text/plain'), ('Content-Length', 193)]`` :param add_default_content_type: If true (and the status is not 204 or 304) a default ``Content-Type`` header will be added if one is not provided, using the value of ``pesto.response.Response.default_content_type``. :param \*\*kwargs: Arbitrary headers, provided as keyword arguments. Replace hyphens with underscores where necessary (eg ``content_length`` instead of ``Content-Length``). Example usage:: >>> # Construct a response >>> response = Response( ... content=['hello world\n'], ... status='200 OK', ... headers=[('Content-Type', 'text/plain')] ... ) >>> >>> # We can manipulate the response before outputting it >>> response = response.add_header('X-Header', 'hello!') >>> >>> # To output the response, we call it as a WSGI application >>> from pesto.testing import TestApp >>> print TestApp(response).get('/').text() 200 OK Content-Type: text/plain X-Header: hello! hello world >>> Note that response objects are themselves callable WSGI applications:: def wsgiapp(environ, start_response): response = Response(['hello world'], content_type='text/plain') return response(environ, start_response) """ if content is None: content = [] self._content = content self._status = self.make_status(status) if onclose is None: self.onclose = [] elif callable(onclose): self.onclose = [onclose] else: self.onclose = list(onclose) if headers is None: headers = [] self._headers = sorted(self.make_headers(headers, kwargs)) if self.status_code not in (204, 304) and add_default_content_type: for key, value in self._headers: if key == 'Content-Type': break else: self._headers.insert(0, ('Content-Type', self.default_content_type)) def __call__(self, environ, start_response, exc_info=None): """ WSGI callable. Calls ``start_response`` with assigned headers and returns an iterator over ``content``. """ start_response( self.status, self.headers, exc_info, ) result = _copy_close(self.content, encoder(self.content, self.charset)) if self.onclose: result = ClosingIterator(result, *self.onclose) return result def add_onclose(self, *funcs): """ Add functions to be called as part of the response iterators ``close`` method. """ return self.__class__( self.content, self.status, self.headers, self.onclose + list(funcs), add_default_content_type=False ) @classmethod def from_wsgi(cls, wsgi_callable, environ, start_response): """ Return a ``Response`` object constructed from the result of calling ``wsgi_callable`` with the given ``environ`` and ``start_response`` arguments. """ if isinstance(wsgi_callable, PestoWSGIApplication): return wsgi_callable.pesto_app( Request(environ), *wsgi_callable.app_args, **wsgi_callable.app_kwargs ) responder = StartResponseWrapper(start_response) content = wsgi_callable(environ, responder) if responder.buf.tell(): content = _copy_close(content, chain(content, [responder.buf.getvalue()])) if responder.called: return cls(content, responder.status, headers=responder.headers) # Iterator has not called start_response yet. Call content.next() # to force the application to call start_response try: chunk = content.next() except StopIteration: return cls(content, responder.status, headers=responder.headers) except Exception: close = getattr(content, 'close', None) if close is not None: close() raise content = _copy_close(content, chain([chunk], content)) return cls( content, responder.status, headers=responder.headers ) @classmethod def make_status(cls, status): """ Return a status line from the given status, which may be a simple integer. Example usage:: >>> Response.make_status(200) '200 OK' """ if isinstance(status, int): return '%d %s' % (status, HTTP_STATUS_CODES[status]) return status @classmethod def make_headers(cls, header_list, header_dict): """ Return a list of header (name, value) tuples from the combination of the header_list and header_dict. Example usage:: >>> Response.make_headers( ... [('Content-Type', 'text/html')], ... {'content_length' : 54} ... ) [('Content-Type', 'text/html'), ('Content-Length', '54')] >>> Response.make_headers( ... [('Content-Type', 'text/html')], ... {'x_foo' : ['a1', 'b2']} ... ) [('Content-Type', 'text/html'), ('X-Foo', 'a1'), ('X-Foo', 'b2')] """ headers = header_list + header_dict.items() headers = [ (make_header_name(key), val) for key, val in headers if val is not None ] # Join multiple headers. [see RFC2616, section 4.2] newheaders = [] for key, val in headers: if isinstance(val, list): for item in val: newheaders.append((key, str(item))) else: newheaders.append((key, str(val))) return newheaders @property def content(self): """ Iterator over the response content part """ return self._content @property def headers(self): """ Return a list of response headers in the format ``[(, ), ...]`` """ return self._headers def get_headers(self, name): """ Return the list of headers set with the given name. Synopsis:: >>> r = Response(set_cookie = ['cookie1', 'cookie2']) >>> r.get_headers('set-cookie') ['cookie1', 'cookie2'] """ return [value for header, value in self.headers if header.lower() == name.lower()] def get_header(self, name, default=''): """ Return the concatenated values of the named header(s) or ``default`` if the header has not been set. As specified in RFC2616 (section 4.2), multiple headers will be combined using a single comma. Example usage:: >>> r = Response(set_cookie = ['cookie1', 'cookie2']) >>> r.get_header('set-cookie') 'cookie1,cookie2' """ headers = self.get_headers(name) if not headers: return default return ','.join(headers) @property def status(self): """ HTTP status message for the response, eg ``200 OK`` """ return self._status @property def status_code(self): """ Return the numeric status code for the response as an integer:: >>> Response(status='404 Not Found').status_code 404 >>> Response(status=200).status_code 200 """ return int(self._status.split(' ', 1)[0]) @property def content_type(self): """ Return the value of the ``Content-Type`` header if set, otherwise ``None``. """ for key, val in self.headers: if key.lower() == 'content-type': return val return None def add_header(self, name, value): """ Return a new response object with the given additional header. Synopsis:: >>> r = Response(content_type = 'text/plain') >>> r.headers [('Content-Type', 'text/plain')] >>> r.add_header('Cache-Control', 'no-cache').headers [('Cache-Control', 'no-cache'), ('Content-Type', 'text/plain')] """ return self.replace(self._content, self._status, self._headers + [(name, value)]) def add_headers(self, headers=[], **kwheaders): """ Return a new response object with the given additional headers. Synopsis:: >>> r = Response(content_type = 'text/plain') >>> r.headers [('Content-Type', 'text/plain')] >>> r.add_headers( ... cache_control='no-cache', ... expires='Mon, 26 Jul 1997 05:00:00 GMT' ... ).headers [('Cache-Control', 'no-cache'), ('Content-Type', 'text/plain'), ('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')] """ return self.replace(headers=self.make_headers(self._headers + headers, kwheaders)) def remove_headers(self, *headers): """ Return a new response object with the given headers removed. Synopsis:: >>> r = Response(content_type = 'text/plain', cache_control='no-cache') >>> r.headers [('Cache-Control', 'no-cache'), ('Content-Type', 'text/plain')] >>> r.remove_headers('Cache-Control').headers [('Content-Type', 'text/plain')] """ toremove = [ item.lower() for item in headers ] return self.replace( headers=[ h for h in self._headers if h[0].lower() not in toremove ], ) def add_cookie( self, name, value, maxage=None, expires=None, path=None, secure=None, domain=None, comment=None, http_only=False, version=1 ): """ Return a new response object with the given cookie added. Synopsis:: >>> r = Response(content_type = 'text/plain', cache_control='no-cache') >>> r.headers [('Cache-Control', 'no-cache'), ('Content-Type', 'text/plain')] >>> r.add_cookie('foo', 'bar').headers [('Cache-Control', 'no-cache'), ('Content-Type', 'text/plain'), ('Set-Cookie', 'foo=bar;Version=1')] """ return self.replace( headers=self._headers + [ ( 'Set-Cookie', cookie.Cookie( name, value, maxage, expires, path, secure, domain, comment=comment, http_only=http_only, version=version ) ) ] ) def replace(self, content=None, status=None, headers=None, **kwheaders): """ Return a new response object with any of content, status or headers changed. Synopsis:: >>> Response(allow='GET', foo='bar', add_default_content_type=False).replace(allow='POST').headers [('Allow', 'POST'), ('Foo', 'bar')] >>> Response(allow='GET', add_default_content_type=False).replace(headers=[('allow', 'POST')]).headers [('Allow', 'POST')] >>> Response(location='http://www.google.com').replace(status=301).status '301 Moved Permanently' >>> Response(content=['donald']).replace(content=['pluto']).content ['pluto'] """ res = self if content is not None: close = getattr(self.content, 'close', None) onclose = self.onclose if close: onclose = [close,] + onclose res = res.__class__(content, res._status, res._headers, onclose=onclose, add_default_content_type=False) if headers is not None: res = res.__class__(res._content, res._status, headers, onclose=res.onclose, add_default_content_type=False) if status is not None: res = res.__class__(res._content, status, res._headers, onclose=res.onclose, add_default_content_type=False) if kwheaders: toremove = set(make_header_name(k) for k in kwheaders) kwheaders = self.make_headers([], kwheaders) res = res.__class__( res._content, res._status, [(key, value) for key, value in res._headers if key not in toremove] + kwheaders, onclose=res.onclose, add_default_content_type=False ) return res def buffered(self): """ Return a new response object with the content buffered into a list. This will also add a content-length header. Example usage:: >>> def generate_content(): ... yield "one two " ... yield "three four five" ... >>> Response(content=generate_content()).content # doctest: +ELLIPSIS >>> Response(content=generate_content()).buffered().content ['one two ', 'three four five'] """ content = list(self.content) content_length = sum(map(len, content)) return self.replace(content=content, content_length=content_length) @property def charset( self, _parser=re.compile(r'.*;\s*charset=([\w\d\-]+)', re.I).match ): for key, val in self.headers: if key.lower() == 'content-type': mo = _parser(val) if mo: return mo.group(1) else: return None return None @classmethod def not_found(cls, request=None): """ Returns an HTTP not found response (404). This method also outputs the necessary HTML to be used as the return value for a pesto handler. Synopsis:: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.not_found() ... >>> print TestApp(app).get('/') 404 Not Found\r Content-Type: text/html; charset=UTF-8\r \r

Not found

The requested resource could not be found.

""" return cls( status = STATUS_NOT_FOUND, content = [ "\n" "\n" "

Not found

\n" "

The requested resource could not be found.

\n" "\n" "" ] ) @classmethod def forbidden(cls, message='Sorry, access is denied'): """ Return an HTTP forbidden response (403):: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.forbidden() ... >>> print TestApp(app).get('/') 403 Forbidden\r Content-Type: text/html; charset=UTF-8\r \r

Sorry, access is denied

""" return cls( status = STATUS_FORBIDDEN, content = [ "\n" "\n" "

" + message + "

\n" "\n" "" ] ) @classmethod def bad_request(cls, request=None): """ Returns an HTTP bad request response. Example usage:: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.bad_request() ... >>> print TestApp(app).get('/') 400 Bad Request\r Content-Type: text/html; charset=UTF-8\r \r

The server could not understand your request

""" return cls( status = STATUS_BAD_REQUEST, content = ["" "" "

The server could not understand your request

" "" "" ] ) @classmethod def length_required(cls, request=None): """ Returns an HTTP 411 Length Required request response. Example usage:: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.length_required() ... >>> print TestApp(app).get('/') 411 Length Required\r Content-Type: text/html; charset=UTF-8\r \r

The Content-Length header was missing from your request

""" return cls( status = STATUS_LENGTH_REQUIRED, content = ["" "" "

The Content-Length header was missing from your request

" "" "" ] ) @classmethod def request_entity_too_large(cls, request=None): """ Returns an HTTP 413 Request Entity Too Large response. Example usage:: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.request_entity_too_large() ... >>> print TestApp(app).get('/') 413 Request Entity Too Large\r Content-Type: text/html; charset=UTF-8\r \r

Request Entity Too Large

""" return cls( status = STATUS_REQUEST_ENTITY_TOO_LARGE, content = ["" "" "

Request Entity Too Large

" "" "" ] ) @classmethod def method_not_allowed(cls, valid_methods): """ Returns an HTTP method not allowed response (404):: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.method_not_allowed(valid_methods=("GET", "HEAD")) ... >>> print TestApp(app).get('/') 405 Method Not Allowed\r Allow: GET,HEAD\r Content-Type: text/html; charset=UTF-8\r \r

Method not allowed

``valid_methods`` A list of HTTP methods valid for the URI requested. If ``None``, the dispatcher_app mechanism will be used to autogenerate a list of methods. This expects a dispatcher_app object to be stored in the wsgi environ dictionary at ``pesto.dispatcher_app``. ``returns`` A ``pesto.response.Response`` object """ return cls( status = STATUS_METHOD_NOT_ALLOWED, allow = ",".join(valid_methods), content = ["" "" "

Method not allowed

" "" "" ] ) @classmethod def internal_server_error(cls): """ Return an HTTP internal server error response (500):: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.internal_server_error() ... >>> print TestApp(app).get('/') 500 Internal Server Error\r Content-Type: text/html; charset=UTF-8\r \r

Internal Server Error

``returns`` A ``pesto.response.Response`` object """ return cls( status = STATUS_INTERNAL_SERVER_ERROR, content = ["" "" "

Internal Server Error

" "" "" ] ) @classmethod def redirect(cls, location, request=None, status=STATUS_FOUND): """ Return an HTTP redirect reponse. ``location`` The URI of the new location. If this is relative it will be converted to an absolute URL based on the current request. ``status`` HTTP status code for the redirect, defaulting to ``STATUS_FOUND`` (a temporary redirect) Synopsis:: >>> from pesto.testing import TestApp >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response.redirect("/new-location", request) ... >>> print TestApp(app).get('/') 302 Found\r Content-Type: text/html; charset=UTF-8\r Location: http://localhost/new-location\r \r

Page has moved

http://localhost/new-location

Note that we can also do the following:: >>> from functools import partial >>> from pesto.testing import TestApp >>> from pesto.dispatch import dispatcher_app >>> d = dispatcher_app() >>> d.match('/animals', GET=partial(Response.redirect, '/new-location')) >>> print TestApp(d).get('/animals') 302 Found\r Content-Type: text/html; charset=UTF-8\r Location: http://localhost/new-location\r \r

Page has moved

http://localhost/new-location

""" from pesto.wsgiutils import make_absolute_url if '://' not in location: if request is None: request = currentrequest() location = str(make_absolute_url(request.environ, location)) return Response( status = status, location = location, content = [ "\n", "

Page has moved

\n", "

%s

\n" % (location, location), "", ] ) def make_header_name(name): """ Return a formatted header name from a python idenfier. Example usage:: >>> make_header_name('content_type') 'Content-Type' """ # Common exceptions if name.lower() == 'etag': return 'ETag' return name.replace('_', '-').title() def _copy_close(src, dst, marker=object()): """ Copy the ``close`` attribute from ``src`` to ``dst``, which are assumed to be iterators. If it is not possible to copy the attribute over (eg for ``itertools.chain``, which does not support the close attribute) an instance of ``ClosingIterator`` is returned which will proxy calls to ``close`` as necessary. """ close = getattr(src, 'close', marker) if close is not marker: try: setattr(dst, 'close', close) except AttributeError: return ClosingIterator(dst, close) return dst from pesto.wsgiutils import StartResponseWrapper from pesto.wsgiutils import ClosingIterator#, IteratorWrapper from pesto.core import PestoWSGIApplication from pesto.request import Request, currentrequest pesto-25/pesto/__init__.py0000644000175100017510000000333411613355542016533 0ustar oliveroliver00000000000000""" Pesto ----- Pesto is copyright (c) 2007-2011 Oliver Cope. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of Oliver Cope may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ __docformat__ = 'restructuredtext en' DEFAULT_CHARSET = 'UTF-8' from pesto.core import * from pesto.dispatch import * from pesto.session import * from pesto.response import * from pesto.request import * from pesto.wsgiutils import run_with_cgi pesto-25/pesto/core.py0000644000175100017510000000773311613355542015733 0ustar oliveroliver00000000000000# Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.core ---------- Core WSGI interface functions. """ __all__ = ['currentrequest', 'to_wsgi', 'response'] import sys from itertools import chain, takewhile try: from functools import partial except ImportError: # Roughly equivalent implementation for Python < 2.5 # Taken from http://docs.python.org/library/functools.html def partial(func, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = keywords.copy() newkeywords.update(fkeywords) return func(*(args + fargs), **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc def to_wsgi(pesto_app): """ A decorator function, equivalent to calling ``PestoWSGIApplication(pesto_app)`` directly. """ return PestoWSGIApplication(pesto_app) class PestoWSGIApplication(object): """ A WSGI application wrapper around a Pesto handler function. The handler function should have the following signature: pesto_app(request) -> pesto.response.Response Synopsis:: >>> from pesto.testing import TestApp >>> from pesto.response import Response >>> >>> def handler(request): ... return Response([u"Whoa nelly!"]) ... >>> wsgiapp = PestoWSGIApplication(handler) >>> print TestApp(wsgiapp).get().headers [('Content-Type', 'text/html; charset=UTF-8')] >>> print TestApp(wsgiapp).get() 200 OK\r Content-Type: text/html; charset=UTF-8\r \r Whoa nelly! """ def __init__(self, pesto_app, *app_args, **app_kwargs): """ Initialize an instance of ``PestoWSGIApplication``. """ self.pesto_app = pesto_app self.app_args = app_args self.app_kwargs = app_kwargs self.bound_instance = None def __get__(self, obj, obj_class=None): """ Descriptor protocol __get__ function, allows this decorator to be applied to class methods """ if self.bound_instance: return self self.bound_instance = obj self.pesto_app = partial(self.pesto_app, obj) return self def __call__(self, environ, start_response): """ Return a callable conforming to the WSGI interface. """ return _PestoWSGIAdaptor(self, environ, start_response) class _PestoWSGIAdaptor(object): def __init__(self, decorated_app, environ, start_response): self.decorated_app = decorated_app self.environ = environ self.start_response = start_response self.request = Request(environ) self.content_iter = None def __iter__(self): """ ``__iter__`` method """ return self def next(self): """ Iterator ``next`` method """ if self.content_iter is None: args = (self.request,) + self.decorated_app.app_args try: response = self.decorated_app.pesto_app(*args, **self.decorated_app.app_kwargs) self.content_iter = response(self.environ, self.start_response) except RequestParseError, e: response_close = getattr(self.content_iter, 'close', None) if response_close is not None: response_close() self.content_iter = e.response()(self.environ, self.start_response) return self.content_iter.next() def close(self): """ WSGI iterable ``close`` method """ if self.content_iter is None: return response_close = getattr(self.content_iter, 'close', None) if response_close is not None: return response_close() from pesto import response from pesto.request import currentrequest, Request from pesto.httputils import RequestParseError pesto-25/pesto/wsgiutils.py0000644000175100017510000010430411613355542017025 0ustar oliveroliver00000000000000# Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.wsgiutils --------------- Utility functions for WSGI applications """ __docformat__ = 'restructuredtext en' __all__ = [ 'use_x_forwarded', 'mount_app', 'use_redirect_url', 'make_absolute_url', 'run_with_cgi', 'make_uri_component', 'static_server', 'serve_static_file', 'uri_join', 'make_query', 'with_request_args', ] import inspect import itertools import os import posixpath import re import sys import unicodedata import time import mimetypes try: from email.utils import parsedate_tz, mktime_tz except ImportError: from email.Utils import parsedate_tz, mktime_tz from cStringIO import StringIO from urlparse import urlparse, urlunparse from urllib import quote, quote_plus try: from functools import wraps except ImportError: def wraps(wrappedfunc): """ No-op replacement for ``functools.wraps`` for python < 2.5 """ def call(func): """ Call wrapped function """ return lambda *args, **kwargs: func(*args, **kwargs) return call from pesto.response import Response from pesto.utils import MultiDict def use_x_forwarded(trusted=("127.0.0.1", "localhost")): """ Return a middleware application that modifies the WSGI environment so that the X_FORWARDED_* headers are observed and generated URIs are correct in a proxied environment. Use this whenever the WSGI application server is sitting behind Apache or another proxy server. HTTP_X_FORWARDED_FOR is substituted for REMOTE_ADDR and HTTP_X_FORWARDED_HOST for SERVER_NAME. If HTTP_X_FORWARDED_SSL is set, then the wsgi.url_scheme is modified to ``https`` and ``HTTPS`` is set to ``on``. Example:: >>> from pesto.core import to_wsgi >>> from pesto.testing import TestApp >>> def app(request): ... return Response(["URL is ", request.request_uri, "; REMOTE_ADDR is ", request.remote_addr]) ... >>> app = TestApp(use_x_forwarded()(to_wsgi(app))) >>> response = app.get('/', ... SERVER_NAME='wsgiserver-name', ... SERVER_PORT='8080', ... HTTP_HOST='wsgiserver-name:8080', ... REMOTE_ADDR='127.0.0.1', ... HTTP_X_FORWARDED_HOST='real-name:81', ... HTTP_X_FORWARDED_FOR='1.2.3.4' ... ) >>> response.body 'URL is http://real-name:81/; REMOTE_ADDR is 1.2.3.4' >>> response = app.get('/', ... SERVER_NAME='wsgiserver-name', ... SERVER_PORT='8080', ... HTTP_HOST='wsgiserver-name:8080', ... REMOTE_ADDR='127.0.0.1', ... HTTP_X_FORWARDED_HOST='real-name:443', ... HTTP_X_FORWARDED_FOR='1.2.3.4', ... HTTP_X_FORWARDED_SSL='on' ... ) >>> response.body 'URL is https://real-name/; REMOTE_ADDR is 1.2.3.4' In a non-forwarded environment, the environ dictionary will not be changed:: >>> response = app.get('/', ... SERVER_NAME='wsgiserver-name', ... SERVER_PORT='8080', ... HTTP_HOST='wsgiserver-name:8080', ... REMOTE_ADDR='127.0.0.1', ... ) >>> response.body 'URL is http://wsgiserver-name:8080/; REMOTE_ADDR is 127.0.0.1' """ trusted = dict.fromkeys(trusted, None) def middleware(app): """ Create ``use_x_forwarded`` middleware for WSGI callable ``app`` """ def call(environ, start_response): """ Call the decorated WSGI callable ``app`` with the modified environ """ if environ.get('REMOTE_ADDR') in trusted: try: environ['REMOTE_ADDR'] = environ['HTTP_X_FORWARDED_FOR'] except KeyError: pass is_ssl = bool(environ.get('HTTP_X_FORWARDED_SSL')) if 'HTTP_X_FORWARDED_HOST' in environ: host = environ['HTTP_X_FORWARDED_HOST'] if ':' in host: port = host.split(':')[1] else: port = is_ssl and '443' or '80' environ['HTTP_HOST'] = host environ['SERVER_PORT'] = port if is_ssl: environ['wsgi.url_scheme'] = 'https' environ['HTTPS'] = 'on' return app(environ, start_response) return call return middleware def mount_app(appmap): """ Create a composite application with different mount points. Synopsis:: >>> def app1(e, sr): ... return [1] ... >>> def app2(e, sr): ... return [2] ... >>> app = mount_app({ ... '/path/one' : app1, ... '/path/two' : app2, ... }) """ apps = [] for path, app in appmap.items(): if path and path[-1] == '/': path = path[:-1] apps.append((path, app)) apps.sort() apps.reverse() def mount_app_application(env, start_response): """ WSGI callable that invokes one of the WSGI applications in ``appmap`` depending on the WSGI ``PATH_INFO`` environ variable> """ script_name = env.get("SCRIPT_NAME") path_info = env.get("PATH_INFO") for key, app in apps: if path_info == key or path_info[:len(key) + 1] == key + '/': env["SCRIPT_NAME"] = script_name + key env["PATH_INFO"] = path_info[len(key):] if env["SCRIPT_NAME"] == "/": env["SCRIPT_NAME"] = "" env["PATH_INFO"] = "/" + env["PATH_INFO"] return app(env, start_response) else: return Response.not_found()(env, start_response) return mount_app_application def static_server(document_root, default_charset="ISO-8859-1", bufsize=8192): """ Create a simple WSGI static file server application Synopsis:: >>> from pesto.dispatch import dispatcher_app >>> dispatcher = dispatcher_app() >>> dispatcher.match('/static/', ... GET=static_server('/docroot'), ... HEAD=static_server('/docroot') ... ) """ from pesto.core import to_wsgi document_root = os.path.abspath(os.path.normpath(document_root)) @to_wsgi def static_server_application(request, path=None): """ WSGI static server application """ if path is None: path = request.path_info path = posixpath.normpath(path) while path[0] == '/': path = path[1:] path = os.path.join(document_root, *path.split('/')) path = os.path.normpath(path) if not path.startswith(document_root): return Response.forbidden() return serve_static_file(request, path, default_charset, bufsize) return static_server_application def serve_static_file(request, path, default_charset="ISO-8859-1", bufsize=8192): """ Serve a static file located at ``path``. It is the responsibility of the caller to check that the path is valid and allowed. Synopsis:: >>> from pesto.dispatch import dispatcher_app >>> def view_important_document(request): ... return serve_static_file(request, '/path/to/very_important_document.pdf') ... >>> def download_important_document(request): ... return serve_static_file(request, '/path/to/very_important_document.pdf').add_headers( ... content_disposition='attachment; filename=very_important_document.pdf' ... ) ... """ from pesto.response import STATUS_OK, STATUS_NOT_MODIFIED try: mtime = os.path.getmtime(path) except OSError: return Response.not_found() mod_since = request.get_header('if-modified-since') if mod_since is not None: mod_since = mktime_tz(parsedate_tz(mod_since)) if int(mtime) <= int(mod_since): return Response(status=STATUS_NOT_MODIFIED) typ, enc = mimetypes.guess_type(path) if typ is None: typ = 'application/octet-stream' if typ.startswith('text/'): typ = typ + '; charset=%s' % default_charset if 'wsgi.file_wrapper' in request.environ: content_iterator = lambda fileob: request.environ['wsgi.file_wrapper'](fileob, bufsize) else: content_iterator = lambda fileob: ClosingIterator(iter(lambda: fileob.read(bufsize), ''), fileob.close) try: _file = open(path, 'rb') except IOError: return Response.forbidden() return Response( status = STATUS_OK, content_length = str(os.path.getsize(path)), last_modified = time.strftime('%w, %d %b %Y %H:%M:%S +0000', time.gmtime(mtime)), content_type = typ, content_encoding = enc, content = content_iterator(_file) ) def normpath(path): """ Return ``path`` normalized to remove '../' etc. This differs from ``posixpath.normpath`` in that: * trailing slashes are preserved * multiple consecutive slashes are *always* condensed to a single slash Examples:: >>> normpath('/hello/../goodbye') '/goodbye' >>> normpath('//etc/passwd') '/etc/passwd' >>> normpath('../../../../etc/passwd') 'etc/passwd' >>> normpath('/etc/passwd/') '/etc/passwd/' """ segments = path.split('/') newpath = [] last = len(segments) - 1 for pos, seg in enumerate(segments): if seg == '.': seg = '' if seg == '': allow_empty = ( pos == 0 or pos == last and newpath and newpath[-1] != '' or pos == last and newpath == [''] ) if not allow_empty: continue if seg == '..': if newpath and newpath != ['']: newpath.pop() continue newpath.append(seg) return '/'.join(newpath) def use_redirect_url(use_redirect_querystring=True): """ Replace the ``SCRIPT_NAME`` and ``QUERY_STRING`` WSGI environment variables with ones taken from Apache's ``REDIRECT_URL`` and ``REDIRECT_QUERY_STRING`` environment variable, if set. If an application is mounted as CGI and Apache RewriteRules are used to route requests, the ``SCRIPT_NAME`` and ``QUERY_STRING`` parts of the environment may not be meaningful for reconstructing URLs. In this case Apache puts an extra key, ``REDIRECT_URL`` into the path which contains the full path as requested. See also: * `URL reconstruction section of PEP 333 `_. * `Apache mod_rewrite reference `_. **Example**: assume a handler similar to the below has been made available at the address ``/cgi-bin/myhandler.cgi``:: >>> from pesto import to_wsgi >>> @to_wsgi ... def app(request): ... return Response(["My URL is " + request.request_uri]) ... Apache has been configured to redirect requests using the following RewriteRules in a ``.htaccess`` file in the server's document root, or the equivalents in the apache configuration file:: RewriteEngine On RewriteBase / RewriteRule ^pineapple(.*)$ /cgi-bin/myhandler.cgi [PT] The following code creates a simulation of the request headers Apache will pass to the application with the above rewrite rules. Without the middleware, the output will be as follows:: >>> from pesto.testing import TestApp >>> TestApp(app).get( ... SERVER_NAME = 'example.com', ... REDIRECT_URL = '/pineapple/cake', ... SCRIPT_NAME = '/myhandler.cgi', ... PATH_INFO = '/cake', ... ).body 'My URL is http://example.com/myhandler.cgi/cake' The ``use_redirect_url`` middleware will correctly set the ``SCRIPT_NAME`` and ``QUERY_STRING`` values:: >>> app = use_redirect_url()(app) With this change the application will now output the correct values:: >>> TestApp(app).get( ... SERVER_NAME = 'example.com', ... REDIRECT_URL = '/pineapple/cake', ... SCRIPT_NAME = '/myhandler.cgi', ... PATH_INFO = '/cake', ... ).body 'My URL is http://example.com/pineapple/cake' """ def use_redirect_url(wsgiapp): def use_redirect_url(env, start_response): if "REDIRECT_URL" in env: env['SCRIPT_NAME'] = env["REDIRECT_URL"] path_info = env["PATH_INFO"] if env["SCRIPT_NAME"][-len(path_info):] == path_info: env["SCRIPT_NAME"] = env["SCRIPT_NAME"][:-len(path_info)] if use_redirect_querystring: if "REDIRECT_QUERY_STRING" in env: env["QUERY_STRING"] = env["REDIRECT_QUERY_STRING"] return wsgiapp(env, start_response) return use_redirect_url return use_redirect_url def make_absolute_url(wsgi_environ, url): """ Return an absolute url from ``url``, based on the current url. Synopsis:: >>> from pesto.testing import make_environ >>> environ = make_environ(wsgi_url_scheme='https', SERVER_NAME='example.com', SERVER_PORT='443', PATH_INFO='/foo') >>> make_absolute_url(environ, '/bar') 'https://example.com/bar' >>> make_absolute_url(environ, 'baz') 'https://example.com/foo/baz' >>> make_absolute_url(environ, 'http://anotherhost/bar') 'http://anotherhost/bar' Note that the URL is constructed using the PEP-333 URL reconstruction method (http://www.python.org/dev/peps/pep-0333/#url-reconstruction) and the returned URL is normalized:: >>> environ = make_environ( ... wsgi_url_scheme='https', ... SERVER_NAME='example.com', ... SERVER_PORT='443', ... SCRIPT_NAME='/colors', ... PATH_INFO='/red', ... ) >>> make_absolute_url(environ, 'blue') 'https://example.com/colors/red/blue' >>> make_absolute_url(environ, '../blue') 'https://example.com/colors/blue' >>> make_absolute_url(environ, 'blue/') 'https://example.com/colors/red/blue/' """ env = wsgi_environ.get if '://' not in url: scheme = env('wsgi.url_scheme', 'http') if scheme == 'https': port = ':' + env('SERVER_PORT', '443') else: port = ':' + env('SERVER_PORT', '80') if scheme == 'http' and port == ':80' or scheme == 'https' and port == ':443': port = '' parsed = urlparse(url) url = urlunparse(( env('wsgi.url_scheme',''), env('HTTP_HOST', env('SERVER_NAME', '') + port), normpath( posixpath.join( quote(env('SCRIPT_NAME', '')) + quote(env('PATH_INFO', '')), parsed[2] ) ), parsed[3], parsed[4], parsed[5], )) return url def uri_join(base, link): """ Example:: >>> uri_join('http://example.org/', 'http://example.com/') 'http://example.com/' >>> uri_join('http://example.com/', '../styles/main.css') 'http://example.com/styles/main.css' >>> uri_join('http://example.com/subdir/', '../styles/main.css') 'http://example.com/styles/main.css' >>> uri_join('http://example.com/login', '?error=failed+auth') 'http://example.com/login?error=failed+auth' >>> uri_join('http://example.com/login', 'register') 'http://example.com/register' """ SCHEME, NETLOC, PATH, PARAM, QUERY, FRAGMENT = range(6) plink = urlparse(link) # Link is already absolute, return it unchanged if plink[SCHEME]: return link pbase = urlparse(base) path = pbase[PATH] if plink[PATH]: path = normpath(posixpath.join(posixpath.dirname(pbase[PATH]), plink[PATH])) return urlunparse(( pbase[SCHEME], pbase[NETLOC], path, plink[PARAM], plink[QUERY], plink[FRAGMENT] )) def _qs_frag(key, value, charset=None): u""" Return a fragment of a query string in the format 'key=value'. >>> _qs_frag('search-by', 'author, editor') 'search-by=author%2C+editor' If no encoding is specified, unicode values are encoded using the character set specified by ``pesto.DEFAULT_CHARSET``. """ from pesto import DEFAULT_CHARSET if charset is None: charset = DEFAULT_CHARSET return quote_plus(_make_bytestr(key, charset)) \ + '=' \ + quote_plus(_make_bytestr(value, charset)) def _make_bytestr(ob, charset): u""" Return a byte string conversion of the given object. If the object is a unicode string, encode it with the given encoding. Example:: >>> _make_bytestr(1, 'utf-8') '1' >>> _make_bytestr(u'a', 'utf-8') 'a' """ if isinstance(ob, unicode): return ob.encode(charset) return str(ob) def _repeat_keys(iterable): u""" Return a list of ``(key, scalar_value)`` tuples given an iterable containing ``(key, iterable_or_scalar_value)``. Example:: >>> list( ... _repeat_keys([('a', 'b')]) ... ) [('a', 'b')] >>> list( ... _repeat_keys([('a', ['b', 'c'])]) ... ) [('a', 'b'), ('a', 'c')] """ for key, value in iterable: if isinstance(value, basestring): value = [value] else: try: value = iter(value) except TypeError: value = [value] for subvalue in value: yield key, subvalue def make_query(data=None, separator=';', charset=None, **kwargs): """ Return a query string formed from the given dictionary data. Note that the pairs are separated using a semicolon, in accordance with `the W3C recommendation `_ If no encoding is given, unicode values are encoded using the character set specified by ``pesto.DEFAULT_CHARSET``. Synopsis:: >>> # Basic usage >>> make_query({ 'eat' : u'more cake', 'drink' : u'more tea' }) 'drink=more+tea;eat=more+cake' >>> # Use an ampersand as the separator >>> make_query({ 'eat' : u'more cake', 'drink' : u'more tea' }, separator='&') 'drink=more+tea&eat=more+cake' >>> # Can also be called using ``**kwargs`` style >>> make_query(eat=u'more cake', drink=u'more tea') 'drink=more+tea;eat=more+cake' >>> # Non-string values >>> make_query(eat=[1, 2], drink=3.4) 'drink=3.4;eat=1;eat=2' >>> # Multiple values per key >>> make_query(eat=[u'more', u'cake'], drink=u'more tea') 'drink=more+tea;eat=more;eat=cake' >>> # Anything with a value of ``None`` is excluded from the query >>> make_query(x='1', y=None) 'x=1' """ from pesto import DEFAULT_CHARSET if isinstance(data, MultiDict): items = data.allitems() elif isinstance(data, dict): items = data.items() elif data is None: items = [] else: items = list(data) items += kwargs.items() if charset is None: charset = DEFAULT_CHARSET # Sort data items for a predictable order in tests items.sort() items = _repeat_keys(items) items = ((k, v) for k, v in items if v is not None) return separator.join([ _qs_frag(k, v, charset=charset) for k, v in items ]) def make_wsgi_environ_cgi(): """ Create a wsgi environment dictionary based on the CGI environment (taken from ``os.environ``) """ environ = dict( PATH_INFO = '', SCRIPT_NAME = '', ) environ.update(os.environ) environ['wsgi.version'] = (1, 0) if environ.get('HTTPS','off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True # PEP 333 insists that these be present and non-empty assert 'REQUEST_METHOD' in environ assert 'SERVER_PROTOCOL' in environ assert 'SERVER_NAME' in environ assert 'SERVER_PORT' in environ return environ def run_with_cgi(application, environ=None): """ Run application ``application`` as a CGI script """ if environ is None: environ = make_wsgi_environ_cgi() headers_set = [] headers_sent = [] def write(data): """ WSGI write for CGI: write output to ``sys.stdout`` """ if not headers_set: raise AssertionError("write() before start_response()") elif not headers_sent: # Before the first output, send the stored headers status, response_headers = headers_sent[:] = headers_set sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() def start_response(status, response_headers, exc_info=None): """ WSGI ``start_response`` function """ if exc_info: try: if headers_sent: # Re-raise original exception if headers sent raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None # avoid dangling circular ref elif headers_set: raise AssertionError("Headers already set!") headers_set[:] = [status, response_headers] return write result = application(environ, start_response) try: for data in result: if data: # don't send headers until body appears write(data) if not headers_sent: write('') # send headers now if body was empty finally: if hasattr(result, 'close'): result.close() def make_uri_component(s, separator="-"): """ Turn a string into something suitable for a URI component. Synopsis:: >>> import pesto.wsgiutils >>> pesto.wsgiutils.make_uri_component(u"How now brown cow") 'how-now-brown-cow' Unicode characters are mapped to ASCII equivalents where appropriate, and characters which would normally have to be escaped are translated into hyphens to ease readability of the generated URI. s The (unicode) string to translate. separator A single ASCII character that will be used to replace spaces and other characters that are inadvisable in URIs. returns A lowercase ASCII string, suitable for inclusion as part of a URI. """ if isinstance(s, unicode): s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') s = s.strip().lower() s = re.sub(r'[\s\/]+', separator, s) s = re.sub(r'[^A-Za-z0-9\-\_]', '', s) return s class RequestArgsConversionError(ValueError): """ Raised by ``with_request_args`` when a value cannot be converted to the requested type """ class RequestArgsKeyError(KeyError): """ Raised by ``with_request_args`` when a value cannot be converted to the requested type """ def with_request_args(**argspec): """ Function decorator to map request query/form arguments to function arguments. Synopsis:: >>> from pesto.dispatch import dispatcher_app >>> from pesto import to_wsgi >>> from pesto.testing import TestApp >>> >>> dispatcher = dispatcher_app() >>> >>> @dispatcher.match('/recipes//view', 'GET') ... @with_request_args(id=int) ... def my_handler(request, category, id): ... return Response([ ... u'Recipe #%d in category "%s".' % (id, category) ... ]) ... >>> print TestApp(dispatcher).get('/recipes/rat-stew/view', QUERY_STRING='id=2') 200 OK\r Content-Type: text/html; charset=UTF-8\r \r Recipe #2 in category "rat-stew". If specified arguments are not present in the request (and no default value is given in the function signature), or a ValueError is thrown during type conversion an appropriate error will be raised:: >>> print TestApp(dispatcher).get('/recipes/rat-stew/view') #doctest: +ELLIPSIS Traceback (most recent call last): ... RequestArgsKeyError: 'id' >>> print TestApp(dispatcher).get('/recipes/rat-stew/view?id=elephant') #doctest: +ELLIPSIS Traceback (most recent call last): ... RequestArgsConversionError: Could not convert parameter 'id' to requested type (invalid literal for int() with base 10: 'elephant') A default argument value in the handler function will protect against this:: >>> dispatcher = dispatcher_app() >>> @dispatcher.match('/recipes//view', 'GET') ... @with_request_args(category=unicode, id=int) ... def my_handler(request, category, id=1): ... return Response([ ... u'Recipe #%d in category "%s".' % (id, category) ... ]) ... >>> print TestApp(dispatcher).get('/recipes/mouse-pie/view') 200 OK\r Content-Type: text/html; charset=UTF-8\r \r Recipe #1 in category "mouse-pie". Sometimes it is necessary to map multiple request values to a single argument, for example in a form where two or more input fields have the same name. To do this, put the type-casting function into a list when calling ``with_request_args``:: >>> @to_wsgi ... @with_request_args(actions=[unicode]) ... def app(request, actions): ... return Response([ ... u', '.join(actions) ... ]) ... >>> print TestApp(app).get('/', QUERY_STRING='actions=up;actions=up;actions=and+away%21') 200 OK\r Content-Type: text/html; charset=UTF-8\r \r up, up, and away! """ strict_checking = argspec.pop('strict_checking', False) # Check it's a boolean to raise an error if 'strict_checking' is used as a # name for real argspec argument assert strict_checking in (True, False, 1, 0) def decorator(func): """ Decorate function ``func``. """ f_args, f_varargs, f_varkw, f_defaults = inspect.getargspec(func) # Produce a mapping of { argname: default } if f_defaults is None: f_defaults = [] defaults = dict(zip(f_args[-len(f_defaults):], f_defaults)) def decorated(request, *args, **kwargs): """ Call ``func`` with arguments extracted from ``request``. """ args = (request,) + args given_arguments = dict( zip(f_args[:len(args)], args) ) given_arguments.update(kwargs) newargs = given_arguments.copy() for name, type_fn in argspec.items(): try: try: value = given_arguments[name] except KeyError: if isinstance(type_fn, list): value = request.form.getlist(name) else: value = request.form[name] try: if isinstance(type_fn, list): value = [cast(v) for cast, v in zip(itertools.cycle(type_fn), value)] else: value = type_fn(value) except ValueError, e: if name in defaults and not strict_checking: value = defaults[name] else: raise RequestArgsConversionError( "Could not convert parameter %r to requested type (%s)" % ( name, e.args[0] ) ) except KeyError: try: value = defaults[name] except KeyError: raise RequestArgsKeyError(name) newargs[name] = value return func(**newargs) return wraps(func)(decorated) return decorator def overlay(*args): u""" Run each application given in ``*args`` in turn and return the response from the first that does not return a 404 response. """ def app(environ, start_response): """ WSGI callable that will iterate through each application in ``args`` and return the first that does not return a 404 status. """ for app in args: response = Response.from_wsgi(app, environ, start_response) result = response(environ, start_response) if response.status[:3] != '404': return result else: close = getattr(result, 'close', None) if close is not None: close() return Response.not_found()(environ, start_response) return app class StartResponseWrapper(object): """ Wrapper class for the ``start_response`` callable, which allows middleware applications to intercept and interrogate the proxied start_response arguments. Synopsis:: >>> from pesto.testing import TestApp >>> def my_wsgi_app(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/plain')]) ... return ['Whoa nelly!'] ... >>> def my_other_wsgi_app(environ, start_response): ... responder = StartResponseWrapper(start_response) ... result = my_wsgi_app(environ, responder) ... print "Got status", responder.status ... print "Got headers", responder.headers ... responder.call_start_response() ... return result ... >>> result = TestApp(my_other_wsgi_app).get('/') Got status 200 OK Got headers [('Content-Type', 'text/plain')] See also ``Response.from_wsgi``, which takes a wsgi callable, environ and start_response, and returns a ``Response`` object, allowing the client to further interrogate and customize the WSGI response. Note that it is usually not advised to use this directly in middleware as start_response may not be called directly from the WSGI application, but rather from the iterator it returns. In this case the middleware may need logic to accommodate this. It is usually safer to use ``Response.from_wsgi``, which takes this into account. """ def __init__(self, start_response): self.start_response = start_response self.status = None self.headers = [] self.called = False self.buf = StringIO() self.exc_info = None def __call__(self, status, headers, exc_info=None): """ Dummy WSGI ``start_response`` function that stores the arguments for later use. """ self.status = status self.headers = headers self.exc_info = exc_info self.called = True return self.buf.write def call_start_response(self): """ Invoke the wrapped WSGI ``start_response`` function. """ try: write = self.start_response( self.status, self.headers, self.exc_info ) write(self.buf.getvalue()) return write finally: # Avoid dangling circular ref self.exc_info = None class ClosingIterator(object): """ Wrap an WSGI iterator to allow additional close functions to be called on application exit. Synopsis:: >>> from pesto.testing import TestApp >>> class filelikeobject(object): ... ... def read(self): ... print "file read!" ... return '' ... ... def close(self): ... print "file closed!" ... >>> def app(environ, start_response): ... f = filelikeobject() ... start_response('200 OK', [('Content-Type', 'text/plain')]) ... return ClosingIterator(iter(f.read, ''), f.close) ... >>> m = TestApp(app).get('/') file read! file closed! """ def __init__(self, iterable, *close_funcs): """ Initialize a ``ClosingIterator`` to wrap iterable ``iterable``, and call any functions listed in ``*close_funcs`` on the instance's ``.close`` method. """ self.__dict__['_iterable'] = iterable self.__dict__['_next'] = iter(self._iterable).next self.__dict__['_close_funcs'] = close_funcs iterable_close = getattr(self._iterable, 'close', None) if iterable_close is not None: self.__dict__['_close_funcs'] = (iterable_close,) + close_funcs self.__dict__['_closed'] = False def __iter__(self): """ ``__iter__`` method """ return self def next(self): """ Return the next item from ``iterator`` """ return self._next() def close(self): """ Call all close functions listed in ``*close_funcs``. """ self.__dict__['_closed'] = True for func in self._close_funcs: func() def __getattr__(self, attr): return getattr(self._iterable, attr) def __setattr__(self, attr, value): return getattr(self._iterable, attr, value) def __del__(self): """ Emit a warning if the iterator is deleted with ``close`` having been called. """ if not self._closed: import warnings warnings.warn("%r deleted without close being called" % (self,)) pesto-25/pesto/request.py0000644000175100017510000004552511613355542016474 0ustar oliveroliver00000000000000# Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.request ------------- Request object for WSGI applications. """ import posixpath import re import threading from urllib import quote from urlparse import urlunparse try: from functools import partial except ImportError: # Roughly equivalent implementation for Python < 2.5 # Taken from http://docs.python.org/library/functools.html def partial(func, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = keywords.copy() newkeywords.update(fkeywords) return func(*(args + fargs), **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc from pesto.utils import MultiDict from pesto.httputils import FileUpload from pesto.wsgiutils import make_query from pesto.cookie import parse_cookie_header from pesto import DEFAULT_CHARSET __all__ = ['Request', 'currentrequest'] KB = 1024 MB = 1024 * KB # This object will contain a reference to the current request __local__ = threading.local() def currentrequest(): """ Return the current Request object, or ``None`` if no request object is available. """ try: return __local__.request except AttributeError: return None class Request(object): """ Models an HTTP request, given a WSGI ``environ`` dictionary. """ # Maximum size for application/x-www-form-urlencoded post data, or maximum # field size in multipart/form-data encoded data (not including file # uploads) MAX_SIZE = 16 * KB # Maximum size for multipart/form-data encoded post data MAX_MULTIPART_SIZE = 2 * MB _session = None _form = None _files = None _query = None _cookies = None environ = None charset = DEFAULT_CHARSET def __new__( cls, environ, parse_content_type = re.compile(r'\s*(?:.*);\s*charset=([\w\d\-]+)\s*$') ): u""" Ensure the same instance is returned when called multiple times on the same environ object. Example usage:: >>> from pesto.testing import TestApp >>> env1 = TestApp.make_environ() >>> env2 = TestApp.make_environ() >>> Request(env1) is Request(env1) True >>> Request(env2) is Request(env2) True >>> Request(env1) is Request(env2) False """ try: return environ['pesto.request'] except KeyError: request = object.__new__(cls) __local__.request = request request.environ = environ request.environ['pesto.request'] = request if 'CONTENT_TYPE' in environ: match = parse_content_type.match(environ['CONTENT_TYPE']) if match: request.charset = match.group(1) return request def form(self): """ Return the contents of any submitted form data If the form has been submitted via POST, GET parameters are also available via ``Request.query``. """ if self._form is None: if self.request_method in ('PUT', 'POST'): self._form = MultiDict( parse_post( self.environ, self.environ['wsgi.input'], self.charset, self.MAX_SIZE, self.MAX_MULTIPART_SIZE, ) ) else: self._form = MultiDict( parse_querystring( self.environ['QUERY_STRING'], self.charset ) ) return self._form form = property(form) def files(self): """ Return ``FileUpload`` objects for all uploaded files """ if self._files is None: self._files = MultiDict( (k, v) for k, v in self.form.iterallitems() if isinstance(v, FileUpload) ) return self._files files = property(files) def query(self): """ Return a ``MultiDict`` of any querystring submitted data. This is available regardless of whether the original request was a ``GET`` request. Synopsis:: >>> from pesto.testing import TestApp >>> request = Request(TestApp.make_environ(QUERY_STRING="animal=moose")) >>> request.query.get('animal') u'moose' Note that this property is unaffected by the presence of POST data:: >>> from pesto.testing import TestApp >>> from StringIO import StringIO >>> postdata = 'animal=hippo' >>> request = Request(TestApp.make_environ( ... QUERY_STRING="animal=moose", ... REQUEST_METHOD="POST", ... CONTENT_TYPE = "application/x-www-form-urlencoded; charset=UTF-8", ... CONTENT_LENGTH=len(postdata), ... wsgi_input=postdata ... )) >>> request.form.get('animal') u'hippo' >>> request.query.get('animal') u'moose' """ if self._query is None: self._query = MultiDict( parse_querystring(self.environ.get('QUERY_STRING')) ) return self._query query = property(query) def __getitem__(self, key): """ Return the value of ``key`` from submitted form values. """ marker = [] v = self.get(key, marker) if v is marker: raise KeyError(key) return v def get(self, key, default=None): """ Look up ``key`` in submitted form values """ return self.form.get(key, default) def getlist(self, key): """ Return a list of submitted form values for ``key`` """ return self.form.getlist(key) def __contains__(self, key): """ Return ``True`` if ``key`` is in the submitted form values """ return key in self.form def cookies(self): """ Return a ``pesto.utils.MultiDict`` of cookies read from the request headers:: >>> from pesto.testing import TestApp >>> request = Request(TestApp.make_environ( ... HTTP_COOKIE='''$Version="1"; ... Customer="WILE_E_COYOTE"; ... Part="Rocket_0001"; ... Part="Catapult_0032" ... ''')) >>> [c.value for c in request.cookies.getlist('Customer')] ['WILE_E_COYOTE'] >>> [c.value for c in request.cookies.getlist('Part')] ['Rocket_0001', 'Catapult_0032'] See rfc2109, section 4.4 """ if self._cookies is None: self._cookies = MultiDict( (cookie.name, cookie) for cookie in parse_cookie_header(self.get_header("Cookie")) ) return self._cookies cookies = property( cookies, None, None, cookies.__doc__ ) def get_header(self, name, default=None): """ Return an arbitrary HTTP header from the request. :param name: HTTP header name, eg 'User-Agent' or 'If-Modified-Since'. :param default: default value to return if the header is not set. Technical note: Headers in the original HTTP request are always formatted like this:: If-Modified-Since: Thu, 04 Jan 2007 21:41:08 GMT However, in the WSGI environ dictionary they appear as follows:: { ... 'HTTP_IF_MODIFIED_SINCE': 'Thu, 04 Jan 2007 21:41:08 GMT' ... } Despite this, this method expects the *former* formatting (with hyphens), and is not case sensitive. """ return self.environ.get( 'HTTP_' + name.upper().replace('-', '_'), default ) def request_path(self): """ Return the path component of the requested URI """ scheme, netloc, path, params, query, frag = self.parsed_uri return path request_path = property(request_path, doc=request_path.__doc__) @property def request_uri(self): """ Return the absolute URI, including query parameters. """ return urlunparse(self.parsed_uri) @property def application_uri(self): """ Return the base URI of the WSGI application (ie the URI up to SCRIPT_NAME, but not including PATH_INFO or query information). Synopsis:: >>> from pesto.testing import make_environ >>> request = Request(make_environ(SCRIPT_NAME='/animals', PATH_INFO='/alligator.html')) >>> request.application_uri 'http://localhost/animals' """ uri = self.parsed_uri scheme, netloc, path, params, query, frag = self.parsed_uri return urlunparse((scheme, netloc, self.script_name, '', '', '')) def parsed_uri(self): """ Returns the current URI as a tuple of the form:: ( addressing scheme, network location, path, parameters, query, fragment identifier ) Synopsis:: >>> from pesto.testing import make_environ >>> request = Request(make_environ( ... wsgi_url_scheme = 'https', ... HTTP_HOST = 'example.com', ... SCRIPT_NAME = '/animals', ... PATH_INFO = '/view', ... SERVER_PORT = '443', ... QUERY_STRING = 'name=alligator' ... )) >>> request.parsed_uri ('https', 'example.com', '/animals/view', '', 'name=alligator', '') Note that the port number is stripped if the addressing scheme is 'http' and the port is 80, or the scheme is https and the port is 443:: >>> request = Request(make_environ( ... wsgi_url_scheme = 'http', ... HTTP_HOST = 'example.com:80', ... SCRIPT_NAME = '/animals', ... PATH_INFO = '/view', ... QUERY_STRING = 'name=alligator' ... )) >>> request.parsed_uri ('http', 'example.com', '/animals/view', '', 'name=alligator', '') """ env = self.environ.get script_name = env("SCRIPT_NAME", "") path_info = env("PATH_INFO", "") query_string = env("QUERY_STRING", "") scheme = env('wsgi.url_scheme', 'http') try: host = self.environ['HTTP_HOST'] if ':' in host: host, port = host.split(':', 1) else: port = self.environ['SERVER_PORT'] except KeyError: host = self.environ['SERVER_NAME'] port = self.environ['SERVER_PORT'] if (scheme == 'http' and port == '80') \ or (scheme == 'https' and port == '443'): netloc = host else: netloc = host + ':' + port return ( scheme, netloc, script_name + path_info, '', # Params query_string, '', # Fragment ) parsed_uri = property(parsed_uri, doc=parsed_uri.__doc__) # getters for environ properties def _get_env(self, name, default=None): """ Return a value from the WSGI environment """ return self.environ.get(name, default) env_prop = lambda name, doc, default=None, _get_env=_get_env: property( partial(_get_env, name=name, default=None), doc=doc ) content_type = env_prop('CONTENT_TYPE', "HTTP Content-Type header") document_root = env_prop('DOCUMENT_ROOT', "Server document root") path_info = env_prop('PATH_INFO', "WSGI PATH_INFO value", '') query_string = env_prop('QUERY_STRING', "WSGI QUERY_STRING value") script_name = env_prop('SCRIPT_NAME', "WSGI SCRIPT_NAME value") server_name = env_prop('SERVER_NAME', "WSGI SERVER_NAME value") remote_addr = env_prop('REMOTE_ADDR', "WSGI REMOTE_ADDR value") def referrer(self): """ Return the HTTP referer header, or ``None`` if this is not available. """ return self.get_header('Referer') referrer = property(referrer, doc=referrer.__doc__) def user_agent(self): """ Return the HTTP user agent header, or ``None`` if this is not available. """ return self.get_header('User-Agent') user_agent = property(user_agent, doc=user_agent.__doc__) def request_method(self): """ Return the HTTP method used for the request, eg ``GET`` or ``POST``. """ return self.environ.get("REQUEST_METHOD").upper() request_method = property(request_method, doc=request_method.__doc__) def session(self): """ Return the session associated with this request. Requires a session object to have been inserted into the WSGI environment by a middleware application (see ``pesto.session.base.sessioning_middleware`` for an example). """ return self.environ["pesto.session"] session = property( session, None, None, doc = session.__doc__ ) def make_uri( self, scheme=None, netloc=None, path=None, parameters=None, query=None, fragment=None, script_name=None, path_info=None ): r""" Make a new URI based on the current URI, replacing any of the six URI elements (scheme, netloc, path, parameters, query or fragment) A ``path_info`` argument can also be given instead of the ``path`` argument. In this case the generated URI path will be ``/``. Synopsis: Calling request.make_uri with no arguments will return the current URI:: >>> from pesto.testing import make_environ >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/foo')) >>> request.make_uri() 'http://example.com/foo' Using keyword arguments it is possible to override any part of the URI:: >>> request.make_uri(scheme='ftp') 'ftp://example.com/foo' >>> request.make_uri(path='/bar') 'http://example.com/bar' >>> request.make_uri(query={'page' : '2'}) 'http://example.com/foo?page=2' If you just want to replace the PATH_INFO part of the URI, you can pass ``path_info`` to the ``make_uri``. This will generate a URI relative to wherever your WSGI application is mounted:: >>> # Sample environment for an application mounted at /fruitsalad >>> env = make_environ( ... HTTP_HOST='example.com', ... SCRIPT_NAME='/fruitsalad', ... PATH_INFO='/banana' ... ) >>> Request(env).make_uri(path_info='/kiwi') 'http://example.com/fruitsalad/kiwi' The path and query values are URL escaped before being returned:: >>> request.make_uri(path=u'/caff\u00e8 latte') 'http://example.com/caff%C3%A8%20latte' The ``query`` argument can be a string, a dictionary, a ``MultiDict``, or a list of ``(name, value)`` tuples:: >>> request.make_uri(query=u'a=tokyo&b=milan') 'http://example.com/foo?a=tokyo&b=milan' >>> request.make_uri(query={'a': 'tokyo', 'b': 'milan'}) 'http://example.com/foo?a=tokyo;b=milan' >>> request.make_uri(query=MultiDict([('a', 'tokyo'), ('b', 'milan'), ('b', 'paris')])) 'http://example.com/foo?a=tokyo;b=milan;b=paris' >>> request.make_uri(query=[('a', 'tokyo'), ('b', 'milan'), ('b', 'paris')]) 'http://example.com/foo?a=tokyo;b=milan;b=paris' If a relative path is passed, the returned URI is joined to the old in the same way as a web browser would interpret a relative HREF in a document at the current location:: >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake')) >>> request.make_uri(path='pie') 'http://example.com/banana/pie' >>> request.make_uri(path='../strawberry') 'http://example.com/strawberry' >>> request.make_uri(path='../../../plum') 'http://example.com/plum' Note that a URI with a trailing slash will have different behaviour from one without a trailing slash:: >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake/')) >>> request.make_uri(path='mmmm...') 'http://example.com/banana/milkshake/mmmm...' >>> request = Request(make_environ(HTTP_HOST='example.com', SCRIPT_NAME='', PATH_INFO='/banana/milkshake')) >>> request.make_uri(path='mmmm...') 'http://example.com/banana/mmmm...' """ uri = [] parsed_uri = self.parsed_uri if path is not None: if isinstance(path, unicode): path = path.encode(DEFAULT_CHARSET) if path[0] != '/': path = posixpath.join(posixpath.dirname(parsed_uri[2]), path) path = posixpath.normpath(path) elif script_name is not None or path_info is not None: if script_name is None: script_name = self.environ['SCRIPT_NAME'] if path_info is None: path_info = self.environ['PATH_INFO'] path = script_name + path_info else: path = parsed_uri[2] if isinstance(path, unicode): path = path.encode(DEFAULT_CHARSET) path = quote(path) if query is not None: if not isinstance(query, basestring): query = make_query(query) elif isinstance(query, unicode): query = query.encode(DEFAULT_CHARSET) for specified, parsed in zip((scheme, netloc, path, parameters, query, fragment), parsed_uri): if specified is not None: uri.append(specified) else: uri.append(parsed) return urlunparse(uri) def text(self): """ Return a useful text representation of the request """ import pprint return "<%s\n\trequest_uri=%s\n\trequest_path=%s\n\t%s\n\t%s>" % ( self.__class__.__name__, self.request_uri, self.request_path, pprint.pformat(self.environ), pprint.pformat(self.form.items()), ) # Imports at end to avoid circular dependencies from pesto.httputils import parse_querystring, parse_post pesto-25/pesto/dispatch.py0000644000175100017510000007202711613355542016600 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.dispatch -------------- URL dispatcher WSGI application to map incoming requests to handler functions. Example usage:: >>> from pesto.dispatch import DispatcherApp >>> >>> dispatcher = DispatcherApp() >>> @dispatcher.match('/page/', 'GET') ... def page(request, id): ... return Response(['You requested page %d' % id]) ... >>> from pesto.testing import TestApp >>> TestApp(dispatcher).get('/page/42').body 'You requested page 42' """ __docformat__ = 'restructuredtext en' __all__ = [ 'DispatcherApp', 'dispatcher_app', 'NamedURLNotFound', 'URLGenerationError' ] import re import types from logging import getLogger from urllib import unquote import pesto import pesto.core from pesto.core import PestoWSGIApplication from pesto.response import Response from pesto.request import Request, currentrequest from pesto.wsgiutils import normpath log = getLogger(__name__) class URLGenerationError(Exception): """ Error generating the requested URL """ class Pattern(object): """ Patterns are testable against URL paths using ``Pattern.test``. If they match, they should return a tuple of ``(positional_arguments, keyword_arguments)`` extracted from the URL path. Pattern objects may also be able to take a tuple of ``(positional_arguments, keyword_arguments)`` and return a corresponding URL path. """ def test(self, url): """ Should return a tuple of ``(positional_arguments, keyword_arguments)`` if the pattern matches the given URL path, or None if it does not match. """ raise NotImplementedError def pathfor(self, *args, **kwargs): """ The inverse of ``test``: where possible, should generate a URL path for the given positional and keyword arguments. """ raise NotImplementedError class Converter(object): """ Responsible for converting arguments to and from URI components. A ``Converter`` class has two instance methods: * ``to_string`` - convert from a python object to a string * ``from_string`` - convert from URI-encoded bytestring to the target python type. It must also define the regular expression pattern that is used to extract the string from the URI. """ pattern = '[^/]+' def __init__(self, pattern=None): """ Initialize a ``Converter`` instance. """ if pattern is not None: self.pattern = pattern def to_string(self, ob): """ Convert arbitrary argument ``ob`` to a string representation """ return unicode(ob) def from_string(self, s): """ Convert string argument ``a`` to the target object representation, whatever that may be. """ raise NotImplementedError class IntConverter(Converter): """ Match any integer value and convert to an ``int`` value. """ pattern = r'[+-]?\d+' def from_string(self, s): """ Return ``s`` converted to an ``int`` value. """ return int(s) class UnicodeConverter(Converter): """ Match any string, not including a forward slash, and return a ``unicode`` value """ pattern = r'[^/]+' def to_string(self, s): """ Return ``s`` converted to an ``unicode`` object. """ return s def from_string(self, s): """ Return ``s`` converted to an ``unicode`` object. """ return unicode(s) class AnyConverter(UnicodeConverter): """ Match any one of the given string options. """ pattern = r'[+-]?\d+' def __init__(self, *args): super(AnyConverter, self).__init__(None) if len(args) == 0: raise ValueError("Must supply at least one argument to any()") self.pattern = '|'.join(re.escape(arg) for arg in args) class PathConverter(UnicodeConverter): """ Match any string, possibly including forward slashes, and return a ``unicode`` object. """ pattern = r'.+' class ExtensiblePattern(Pattern): """ An extensible URL pattern matcher. Synopsis:: >>> p = ExtensiblePattern(r"/archive///") >>> p.test('/archive/1999/02/blah') == ((), {'year': 1999, 'month': 2, 'title': 'blah'}) True Patterns are split on slashes into components. A component can either be a literal part of the path, or a pattern component in the form:: : : ``identifer`` can be any python name, which will be used as the name of a keyword argument to the matched function. If omitted, the argument will be passed as a positional arg. ``regex`` can be any regular expression pattern. If omitted, the converter's default pattern will be used. ``converter`` must be the name of a pre-registered converter. Converters must support ``to_string`` and ``from_string`` methods and are used to convert between URL segments and python objects. By default, the following converters are configured: * ``int`` - converts to an integer * ``path`` - any path (ie can include forward slashes) * ``unicode`` - any unicode string (not including forward slashes) * ``any`` - a string matching a list of alternatives Some examples:: >>> p = ExtensiblePattern(r"/images/<:path>") >>> p.test('/images/thumbnails/02.jpg') ((u'thumbnails/02.jpg',), {}) >>> p = ExtensiblePattern("/.html") >>> p.test('/about.html') ((), {'page': u'about'}) >>> p = ExtensiblePattern("/entries/") >>> p.test('/entries/23') ((), {'id': 23}) Others can be added by calling ``ExtensiblePattern.register_converter`` """ preset_patterns = ( ('int', IntConverter), ('unicode', UnicodeConverter), ('path', PathConverter), ('any', AnyConverter), ) pattern_parser = re.compile(""" < (?P\w[\w\d]*)? : (?P\w[\w\d]*) (?: \( (?P.*?) \) )? > """, re.X) class Segment(object): """ Represent a single segment of a URL, storing information about hte ``source``, ``regex`` used to pattern match the segment, ``name`` for named parameters and the ``converter`` used to map the value to a URL parameter if applicable """ def __init__(self, source, regex, name, converter): self.source = source self.regex = regex self.name = name self.converter = converter def __init__(self, pattern, name=''): """ Initialize a new ``ExtensiblePattern`` object with pattern ``pattern`` and an optional name. """ super(ExtensiblePattern, self).__init__() self.name = name self.preset_patterns = dict(self.__class__.preset_patterns) self.pattern = unicode(pattern) self.segments = list(self._make_segments(pattern)) self.args = [ item for item in self.segments if item.converter is not None ] self.regex = re.compile(''.join([ segment.regex for segment in self.segments]) + '$') def _make_segments(self, s): r""" Generate successive Segment objects from the given string. Each segment object represents a part of the pattern to be matched, and comprises ``source``, ``regex``, ``name`` (if a named parameter) and ``converter`` (if a parameter) """ for item in split_iter(self.pattern_parser, self.pattern): if isinstance(item, unicode): yield self.Segment(item, re.escape(item), None, None) continue groups = item.groupdict() name, converter, args = groups['name'], groups['converter'], groups['args'] if isinstance(name, unicode): # Name must be a Python identifiers name = name.encode("ASCII") converter = self.preset_patterns[converter] if args: args, kwargs = self.parseargs(args) converter = converter(*args, **kwargs) else: converter = converter() yield self.Segment(item.group(0), '(%s)' % converter.pattern, name, converter) def parseargs(self, argstr): """ Return a tuple of ``(args, kwargs)`` parsed out of a string in the format ``arg1, arg2, param=arg3``. Synopsis:: >>> ep = ExtensiblePattern('') >>> ep.parseargs("1, 2, 'buckle my shoe'") ((1, 2, 'buckle my shoe'), {}) >>> ep.parseargs("3, four='knock on the door'") ((3,), {'four': 'knock on the door'}) """ return eval('(lambda *args, **kwargs: (args, kwargs))(%s)' % argstr) def test(self, uri): """ Test ``uri`` and return a tuple of parsed ``(args, kwargs)``, or ``None`` if there was no match. """ mo = self.regex.match(uri) if not mo: return None groups = mo.groups() assert len(groups) == len(self.args), ( "Number of regex groups does not match expected count. " "Perhaps you have used capturing parentheses somewhere? " "The pattern tested was %r." % self.regex.pattern ) try: groups = [ (segment.name, segment.converter.from_string(value)) for value, segment in zip(groups, self.args) ] except ValueError: return None args = tuple(value for name, value in groups if not name) kwargs = dict((name, value) for name, value in groups if name) return args, kwargs def pathfor(self, *args, **kwargs): """ Example usage:: >>> p = ExtensiblePattern("/view//") >>> p.pathfor(filename='important_document.pdf', revision=299) u'/view/important_document.pdf/299' >>> p = ExtensiblePattern("/view/<:unicode>/<:int>") >>> p.pathfor('important_document.pdf', 299) u'/view/important_document.pdf/299' """ args = list(args) result = [] for segment in self.segments: if not segment.converter: result.append(segment.source) elif segment.name: try: result.append(segment.converter.to_string(kwargs[segment.name])) except IndexError: raise URLGenerationError( "Argument %r not specified for url %r" % ( segment.name, self.pattern ) ) else: try: result.append(segment.converter.to_string(args.pop(0))) except IndexError, e: raise URLGenerationError( "Not enough positional arguments for url %r" % ( self.pattern, ) ) return ''.join(result) @classmethod def register_converter(cls, name, converter): """ Register a preset pattern for later use in URL patterns. Example usage:: >>> from datetime import date >>> from time import strptime >>> class DateConverter(Converter): ... pattern = r'\d{8}' ... def from_string(self, s): ... return date(*strptime(s, '%d%m%Y')[:3]) ... >>> ExtensiblePattern.register_converter('date', DateConverter) >>> ExtensiblePattern('/<:date>').test('/01011970') ((datetime.date(1970, 1, 1),), {}) """ cls.preset_patterns += ((name, converter),) def __str__(self): """ ``__str__`` method """ return '<%s %r>' % (self.__class__, self.pattern) class DispatcherApp(object): """ Match URLs to pesto handlers. Use the ``match``, ``imatch`` and ``matchre`` methods to associate URL patterns and HTTP methods to callables:: >>> import pesto.dispatch >>> from pesto.response import Response >>> dispatcher = pesto.dispatch.DispatcherApp() >>> def search_form(request): ... return Response(['Search form page']) ... >>> def do_search(request): ... return Response(['Search page']) ... >>> def faq(request): ... return Response(['FAQ page']) ... >>> def faq_category(request): ... return Response(['FAQ category listing']) ... >>> dispatcher.match("/search", GET=search_form, POST=do_search) >>> dispatcher.match("/faq", GET=faq) >>> dispatcher.match("/faq/", GET=faq_category) The last matching pattern wins. Patterns can also be named so that they can be retrieved using the urlfor method:: >>> from pesto.testing import TestApp >>> from pesto.request import Request >>> >>> # URL generation methods require an request object >>> request = Request(TestApp.make_environ()) >>> dispatcher = pesto.dispatch.DispatcherApp() >>> dispatcher.matchpattern( ... ExtensiblePattern("/faq/"), 'faq_category', None, None, GET=faq_category ... ) >>> dispatcher.urlfor('faq_category', request, category='foo') 'http://localhost/faq/foo' Decorated handler functions also grow a ``url`` method that generates valid URLs for the function:: >>> from pesto.testing import TestApp >>> request = Request(TestApp.make_environ()) >>> @dispatcher.match("/faq/", "GET") ... def faq_category(request, category): ... return ['content goes here'] ... >>> faq_category.url(category='alligator') 'http://localhost/faq/alligator' """ default_pattern_type = ExtensiblePattern def __init__(self, prefix='', cache_size=0, debug=False): """ Create a new DispatcherApp. :param prefix: A prefix path that will be prepended to all functions mapped via ``DispatcherApp.match`` :param cache_size: if non-zero, a least recently used (lru) cache of this size will be maintained, mapping URLs to callables. """ self.prefix = prefix self.patterns = [] self.named_patterns = {} self.debug = debug if cache_size > 0: self.gettarget = lru_cached_gettarget(self, self.gettarget, cache_size) def status404_application(self, request): """ Return a ``404 Not Found`` response. Called when the dispatcher cannot find a matching URI. """ return Response.not_found() def status405_application(self, request, valid_methods): """ Return a ``405 Method Not Allowed`` response. Called when the dispatcher can find a matching URI, but the HTTP methods do not match. """ return Response.method_not_allowed(valid_methods) def matchpattern(self, pattern, name, predicate, decorators, *args, **methods): """ Match a URL with the given pattern, specified as an instance of ``Pattern``. pattern A pattern object, eg ``ExtensiblePattern('/pages/')`` name A name that can be later used to retrieve the url with ``urlfor``, or ``None`` predicate A callable that is used to decide whether to match this pattern, or ``None``. The callable must take a ``Request`` object as its only parameter and return ``True`` or ``False``. Synopsis:: >>> from pesto.response import Response >>> dispatcher = DispatcherApp() >>> def view_items(request, tag): ... return Response(["yadda yadda yadda"]) ... >>> dispatcher.matchpattern( ... ExtensiblePattern( ... "/items-by-tag/", ... ), ... 'view_items', ... None, ... None, ... GET=view_items ... ) URLs can later be generated with the urlfor method on the dispatcher object:: >>> Response.redirect(dispatcher.urlfor( ... 'view_items', ... tag='spaghetti', ... )) # doctest: +ELLIPSIS Or, if used in the second style as a function decorator, by calling the function's ``.url`` method:: >>> @dispatcher.match('/items-by-tag/', 'GET') ... def view_items(request, tag): ... return Response(["yadda yadda yadda"]) ... >>> Response.redirect(view_items.url(tag='spaghetti')) # doctest: +ELLIPSIS Note that the ``url`` function can take optional query and fragment paraments to help in URL construction:: >>> from pesto.testing import TestApp >>> from pesto.dispatch import DispatcherApp >>> from pesto.request import Request >>> >>> dispatcher = DispatcherApp() >>> >>> request = Request(TestApp.make_environ()) >>> @dispatcher.match('/pasta', 'GET') ... def pasta(request): ... return Response(["Tasty spaghetti!"]) ... >>> pasta.url(request, query={'sauce' : 'ragu'}, fragment='eat') 'http://localhost/pasta?sauce=ragu#eat' """ if not args and not methods: # Probably called as a decorator, but no HTTP methods specified raise URLGenerationError("HTTP methods not specified") if args: # Return a function decorator def dispatch_decorator(func): methods = dict((method, func) for method in args) self.matchpattern(pattern, name, predicate, decorators, **methods) _add_url_method(func, self.patterns[-1][0]) return func return dispatch_decorator methods = dict( (method, _compose_decorators(func, decorators)) for method, func in methods.items() ) if 'HEAD' not in methods and 'GET' in methods: methods['HEAD'] = _make_head_handler(methods['GET']) if name: self.named_patterns[name] = (pattern, predicate, methods) self.patterns.append((pattern, predicate, methods)) def match(self, pattern, *args, **dispatchers): """ Function decorator to match the given URL to the decorated function, using the default pattern type. name A name that can be later used to retrieve the url with ``urlfor`` (keyword-only argument) predicate A callable that is used to decide whether to match this pattern. The callable must take a ``Request`` object as its only parameter and return ``True`` or ``False``. (keyword-only argument) decorators A list of function decorators that will be applied to the function when called as a WSGI application. (keyword-only argument). The purpose of this is to allow functions to behave differently when called as an API function or as a WSGI application via a dispatcher. """ name = dispatchers.pop('name', None) predicate = dispatchers.pop('predicate', None) decorators = dispatchers.pop('decorators', []) return self.matchpattern( self.default_pattern_type(self.prefix + pattern), name, predicate, decorators, *args, **dispatchers ) def methodsfor(self, path): """ Return a list of acceptable HTTP methods for URI path ``path``. """ methods = {} for p, predicate, dispatchers in self.patterns: match, params = p.test(path) if match: for meth in dispatchers: methods[meth] = None return methods.keys() def urlfor(self, dispatcher_name, request=None, *args, **kwargs): """ Return the URL corresponding to the dispatcher named with ``dispatcher_name``. """ if request is None: request = currentrequest() if dispatcher_name not in self.named_patterns: raise NamedURLNotFound(dispatcher_name) pattern, predicate, handlers = self.named_patterns[dispatcher_name] try: handler = handlers['GET'] except KeyError: handler = handlers.values()[0] return request.make_uri( path=request.script_name + pattern.pathfor(*args, **kwargs), parameters='', query='', fragment='' ) def gettarget(self, path, method, request): """ Generate dispatch targets methods matching the request URI. For each function matched, yield a tuple of:: (function, predicate, positional_args, keyword_args) Positional and keyword arguments are parsed from the URI Synopsis:: >>> from pesto.testing import TestApp >>> d = DispatcherApp() >>> def show_entry(request): ... return [ "Show entry page" ] ... >>> def new_entry_form(request): ... return [ "New entry form" ] ... >>> d.match(r'/entries/new', GET=new_entry_form) >>> d.match(r'/entries/', GET=show_entry) >>> request = Request(TestApp.make_environ(PATH_INFO='/entries/foo')) >>> list(d.gettarget(u'/entries/foo', 'GET', request)) # doctest: +ELLIPSIS [(, None, (), {'id': u'foo'})] >>> request = Request(TestApp.make_environ(PATH_INFO='/entries/new')) >>> list(d.gettarget(u'/entries/new', 'GET', request)) #doctest: +ELLIPSIS [(, None, (), {}), (, None, (), {'id': u'new'})] """ request.environ['pesto.dispatcher_app'] = self path = normpath(path) if self.debug: log.debug("gettarget: path is: %r", path) return self._gettarget(path, method, request) def _gettarget(self, path, method, request): """ Generate ``(func, predicate, positional_args, keyword_args)`` tuples """ if self.debug: log.debug("_gettarget: %s %r", method, path) for p, predicate, dispatchers in self.patterns: result = p.test(path) if self.debug: log.debug( "_gettarget: %r:%r => %s", str(p), dispatchers, bool(result is not None) ) if result is None: continue positional_args, keyword_args = result if self.debug and method in dispatchers: log.debug("_gettarget: matched path to %r", dispatchers[method]) try: target = dispatchers[method] if isinstance(target, types.UnboundMethodType): if getattr(target, 'im_self', None) is None: target = types.MethodType(target, target.im_class(), target.im_class) dispatchers[method] = target yield target, predicate, positional_args, keyword_args except KeyError: methods = set(dispatchers.keys()) request.environ.setdefault( 'pesto.dispatcher_app.valid_methods', set() ).update(methods) if self.debug: log.debug("_gettarget: invalid method for pattern %s: %s", p, method) raise StopIteration def combine(self, *others): """ Add the patterns from dispatcher ``other`` to this dispatcher. Synopsis:: >>> from pesto.testing import TestApp >>> d1 = dispatcher_app() >>> d1.match('/foo', GET=lambda request: Response(['d1:foo'])) >>> d2 = dispatcher_app() >>> d2.match('/bar', GET=lambda request: Response(['d2:bar'])) >>> combined = dispatcher_app().combine(d1, d2) >>> TestApp(combined).get('/foo').body 'd1:foo' >>> TestApp(combined).get('/bar').body 'd2:bar' Note settings other than patterns are not carried over from the other dispatchers - if you intend to use the debug flag or caching options, you must explicitly set them in the combined dispatcher:: >>> combined = dispatcher_app(debug=True, cache_size=50).combine(d1, d2) >>> TestApp(combined).get('/foo').body 'd1:foo' """ for other in others: if not isinstance(other, DispatcherApp): raise TypeError("Can only combine with other DispatcherApp") self.patterns += other.patterns return self def __call__(self, environ, start_response): request = Request(environ) method = request.request_method.upper() path = unquote(request.path_info.decode(request.charset, 'replace')) if path == u'' or path is None: path = u'/' for handler, predicate, args, kwargs in self.gettarget(path, method, request): if predicate and not predicate(request): continue environ['wsgiorg.routing_args'] = (args, kwargs) return PestoWSGIApplication(handler, *args, **kwargs)(environ, start_response) try: del environ['wsgiorg.routing_args'] except KeyError: pass if 'pesto.dispatcher_app.valid_methods' in environ: return self.status405_application( request, environ['pesto.dispatcher_app.valid_methods'] )(environ, start_response) else: return self.status404_application(request)(environ, start_response) # Alias for backwards compatibility dispatcher_app = DispatcherApp def split_iter(pattern, string): """ Generate alternate strings and match objects for all occurances of ``pattern`` in ``string``. """ matcher = pattern.finditer(string) match = None pos = 0 for match in matcher: yield string[pos:match.start()] yield match pos = match.end() yield string[pos:] class NamedURLNotFound(Exception): """ Raised if the named url can't be found (eg in ``urlfor``). """ def lru_cached_gettarget(instance, gettarget, cache_size): """ Wrapper for ``gettarget`` that uses an LRU cache to save path -> dispatcher mappings. """ from repoze.lru import LRUCache cache = LRUCache(cache_size) def lru_cached_gettarget(path, method, request): request.environ['pesto.dispatcher_app'] = instance targets = cache.get((path, method)) if targets is not None: return targets targets = list(gettarget(path, method, request)) cache.put((path, method), targets) return targets return lru_cached_gettarget def _compose_decorators(func, decorators): """ Return a function that is the composition of ``func`` with all decorators in ``decorators``, eg:: decorators[-1](decorators[-2]( ... decorators[0](func) ... )) """ if not decorators: return func for d in reversed(decorators): func = d(func) return func def _add_url_method(func, pattern): """ Add a method at ``func.url`` that returns a URL generated from ``pattern``s pathfor method. """ def url(request=None, scheme=None, netloc=None, script_name=None, query='', fragment='', *args, **kwargs): if request is None: request = currentrequest() return request.make_uri( scheme=scheme, netloc=netloc, script_name=script_name, path_info=pattern.pathfor(*args, **kwargs), parameters='', query=query, fragment=fragment ) try: func.url = url except AttributeError: # Can't set a function attribute on a bound or unbound method # http://www.python.org/dev/peps/pep-0232/ func.im_func.url = url return func def _make_head_handler(handler): """ Take an app that responds to a ``GET`` request and adapt it to one that will handle ``HEAD`` requests. """ def head_handler(*args, **kwargs): response = handler(*args, **kwargs) return response.replace(content = []) return head_handler pesto-25/pesto/caching.py0000644000175100017510000002206111613355542016366 0ustar oliveroliver00000000000000# Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.caching ------------- Utilities to add caching and ETag support. """ import time import re from datetime import datetime from cPickle import dumps try: from hashlib import md5 except ImportError: from md5 import new as md5 from functools import wraps __docformat__ = 'restructuredtext en' def quoted_string(s): r""" Return a quoted string, as per RFC 2616 section 2.2 Synopsis:: >>> from pesto.caching import quoted_string >>> quoted_string(r'"this" is quoted') '"\\"this\\" is quoted"' >>> quoted_string(r'this is \"quoted\"') == r'"this is \\\"quoted\\\""' True """ return '"%s"' % s.replace('\\', '\\\\').replace('"', '\\"') START = object() INSTR = object() WEAK = object() def parse_entity_tags(s): r""" Parse entity tags as found in an If-None-Match header, which may consist of multiple comma separated quoted strings, as per RFC 2616 section 3.11 Example usage:: >>> from pesto.caching import parse_entity_tags >>> parse_entity_tags(r'"tag a", W/"tag b"') [(False, 'tag a'), (True, 'tag b')] >>> parse_entity_tags(r'"\"a\"", "b"') [(False, '"a"'), (False, 'b')] >>> parse_entity_tags(r'"\"a\",\\b", "b"') [(False, '"a",\\b'), (False, 'b')] >>> parse_entity_tags(r'"\"a\",\\b\\", "b"') [(False, '"a",\\b\\'), (False, 'b')] >>> parse_entity_tags(r'"some longer \"text\"", "b"') [(False, 'some longer "text"'), (False, 'b')] """ tokenizer = re.compile(r''' (?P\s+) | (?P\\.) | (?PW/) | (?P") | (?P,) ''', re.X).finditer result = [] current = '' pos = 0 state = START weak = False for match in tokenizer(s): qdtext = s[pos:match.start()] pos = match.end() groups = match.groupdict() matched = match.group(0) if state is START: if matched == '"': state = INSTR weak = False if matched == 'W/': state = WEAK weak = True elif groups['comma']: result.append((weak, current)) weak = False current = '' elif state is WEAK: if matched == '"': state = INSTR elif state is INSTR: if groups['quotedpair']: current += qdtext + matched[1:] elif groups['quote']: current += qdtext state = START else: current += qdtext + matched result.append((weak, current)) return result def make_etag(s, weak=False): """ Return string ``s`` formatted correctly for an ETag header. Example usage:: >>> make_etag('r1089') '"r1089"' >>> make_etag('r1089', True) 'W/"r1089"' """ s = s.replace('"', '\\"') if weak: return 'W/"%s"' % s return '"%s"' % s def with_etag(etag_func, weak=False): """ Decorate the function to add an ETag header to the response object. ``etag_funcs`` is a list of functions which will be called with the request object as an argument, and return an identifier. This could be a timestamp, a revision number, a string, or any other object that identifies the revision of the entity. Synopsis:: >>> from pesto.core import to_wsgi >>> from pesto.testing import TestApp >>> from pesto.response import Response >>> from pesto.caching import with_etag >>> def generate_etag(request): ... return "whoa nelly!" ... >>> @with_etag(generate_etag, False) ... def view(request): ... return Response(["This response should have an etag"]) ... >>> print TestApp(to_wsgi(view)).get() 200 OK\r Content-Type: text/html; charset=UTF-8\r ETag: "whoa nelly!"\r \r This response should have an etag >>> """ def to_etag(ob): """ Make an etag component from a given object. * If the object is a short string, return the string. * If the object is numeric, return the string equivalent. * If the object is a date or datetime, return the string representation of the number of seconds since the Epoch. * Otherwise return the md5 digest of the pickled object """ if isinstance(ob, unicode): ob = ob.encode('utf8') if isinstance(ob, str) and len(ob) <= 16 and '-' not in ob: return ob if isinstance(ob, (int, float)): return str(ob) if isinstance(ob, datetime): return time.mktime(ob.utctimetuple()) return md5(dumps(ob)).hexdigest() def with_etag(func): """ Decorate ``func`` to add an ETag head to the return value (which must be an instance of ``pesto.response.Response``) """ @wraps(func) def with_etag(*args, **kwargs): """ Call ``func`` and add an ETag header to the response """ etag = to_etag(etag_func(*args, **kwargs)) return func(*args, **kwargs).add_header('ETag', make_etag(etag, weak=weak)) return with_etag return with_etag def etag_middleware(app): """ Interpret If-None-Match headers and only sends the response on to the client if the upstream app doesn't produce a matching etag. Note that the upstream application *will* be called on every request. The response's content iterator will not be called on cached responses. """ from pesto.response import Response from pesto.request import Request def call(environ, start_response): """ WSGI middleware callable to handle negotiating caching headers for WSGI application ``app``. """ request = Request(environ) test_etags = request.get_header('If-None-Match') if test_etags is None: return app(environ, start_response) test_etags = parse_entity_tags(test_etags) if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): start_response('412 Precondition Failed', []) return [] allow_weak = not request.get_header('range') response = Response.from_wsgi(app, environ, start_response) etags = parse_entity_tags(response.get_header('ETag')) if etags and etags_match(etags[0], test_etags, allow_weak): # NB. If the original content has a .close() method, this will # be called by the Response.onclose mechanism, thus giving it a # chance to tidy up at the end of the request. response = response.replace( status='304 Not Modified', content=[], content_type=None, ) return response(environ, start_response) return call def etags_match(tag, tags, allow_weak=False): """ Return True if any ``tags`` matches an entry in ``tomatch`` :param tag: a tuple of (``weak``, ``entity-tag``) :param tags: a list of tuples of the same format Synopsis:: # Strong comparison function >>> etags_match((False, 'a'), [(False, 'a'), (False, 'b')], allow_weak=False) True >>> etags_match((False, 'a'), [(True, 'a'), (False, 'b')], allow_weak=False) False # Weak comparison function >>> etags_match((False, 'a'), [(True, 'a'), (False, 'b')], allow_weak=True) True # Weak comparison function >>> etags_match((True, 'b'), [(True, 'a'), (False, 'b')], allow_weak=True) True # The special case '*' tag >>> etags_match((False, 'a'), [(False, '*')]) True """ if (False, '*') in tags: return True if allow_weak: # Discard weak information tags = [(True, t) for w, t in tags] tag = (True, tag[1]) return tag in tags def no_cache(func): """ Add standard no cache headers to a response:: >>> from pesto.testing import TestApp >>> from pesto.core import to_wsgi >>> from pesto.response import Response >>> from pesto.caching import no_cache >>> @no_cache ... def view(request): ... return Response(['cache me if you can!']) ... >>> print TestApp(to_wsgi(view)).get().text() 200 OK Cache-Control: no-cache, no-store, must-revalidate Content-Type: text/html; charset=UTF-8 Expires: Mon, 26 Jul 1997 05:00:00 GMT Pragma: no-store cache me if you can! """ @wraps(func) def decorated(*args, **kwargs): """ Decorated function """ return func(*args, **kwargs).add_headers( pragma='no-store', cache_control="no-cache, no-store, must-revalidate", expires="Mon, 26 Jul 1997 05:00:00 GMT", ) return decorated pesto-25/pesto/cookie.py0000644000175100017510000001332311613355542016244 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. import cgi import copy import urllib from datetime import datetime, timedelta from time import timezone from calendar import timegm try: from email.utils import formatdate except ImportError: from email.Utils import formatdate class Cookie(object): """ Represents an HTTP cookie. See rfc2109, HTTP State Management Mechanism >>> from pesto.cookie import Cookie >>> c = Cookie('session_id', 'abc123') >>> c.path = '/cgi-bin' >>> c.domain = '.ucl.ac.uk' >>> c.path '/cgi-bin' >>> print str(c) session_id=abc123;Domain=.ucl.ac.uk;Path=/cgi-bin;Version=1 """ attributes = [ ("Comment", "comment"), ("Domain", "domain"), ("Expires", "expires"), ("Max-Age", "maxage"), ("Path", "path"), ("Secure", "secure"), ("Version", "version"), ] attribute_dict = dict(attributes) def __init__( self, name, value, maxage=None, expires=None, path=None, secure=None, domain=None, comment=None, http_only=False, version=1 ): """ Initialize a ``Cookie`` instance. """ self.name = name self.value = value self.maxage = maxage self.path = path self.secure = secure self.domain = domain self.comment = comment self.version = version self.expires = expires self.http_only = http_only def __str__(self): """ Returns a string representation of the cookie in the format, eg ``session_id=abc123;Path=/cgi-bin;Domain=.example.com;Version=1`` """ cookie = ['%s=%s' % (self.name, urllib.quote(str(self.value)))] for cookie_name, att_name in self.attributes: value = getattr(self, att_name, None) if value is not None: cookie.append('%s=%s' % (cookie_name, str(value))) if self.http_only: cookie.append('HttpOnly') return ';'.join(cookie) def set_expires(self, dt): """ Set the cookie ``expires`` value to ``datetime`` object ``dt`` """ self._expires = dt def get_expires(self): """ Return the cookie ``expires`` value as an instance of ``datetime``. """ if self._expires is None and self.maxage is not None: if self.maxage == 0: # Make sure immediately expiring cookies get a date firmly in # the past. self._expires = datetime(1980, 1, 1) else: self._expires = datetime.now() + timedelta(seconds = self.maxage) if isinstance(self._expires, datetime): return formatdate(timegm(self._expires.utctimetuple())) else: return self._expires expires = property(get_expires, set_expires) def expire_cookie(cookie_or_name, *args, **kwargs): """ Synopsis:: >>> from pesto.testing import TestApp >>> from pesto.response import Response >>> from pesto import to_wsgi >>> >>> def handler(request): ... return Response(set_cookie = expire_cookie('Customer', path='/')) ... >>> TestApp( ... to_wsgi(handler), ... HTTP_COOKIE='''$Version="1"; ... Customer="WILE_E_COYOTE"; ... Part="Rocket_0001"; ... Part="catapult_0032" ... ''').get().get_header('Set-Cookie') 'Customer=;Expires=Tue, 01 Jan 1980 00:00:00 -0000;Max-Age=0;Path=/;Version=1' """ if isinstance(cookie_or_name, Cookie): expire = cookie_or_name else: expire = Cookie(name=cookie_or_name, value='', *args, **kwargs) return Cookie( name=expire.name, value='', expires=datetime(1980, 1, 1), maxage=0, domain=kwargs.get('domain', expire.domain), path=kwargs.get('path', expire.path) ) def parse_cookie_header(cookie_string, unquote=urllib.unquote): """ Return a list of Cookie objects read from the request headers. :param cookie_string: The cookie, eg ``CUSTOMER=FRED; path=/;`` :param unquote: A function to decode quoted values. If set to ``None``, values will be left as-is. See rfc2109, section 4.4 The Cookie header should be a ';' separated list of name value pairs. If a name is prefixed by a '$', then that name is an attribute of the most recently (left to right) encountered cookie. If no cookie has yet been parsed then the value applies to the cookie mechanism as a whole. """ if unquote is None: unquote = lambda v: v if not cookie_string: return [] cookies = [] # Here we put the $ prefixed attributes that appear *before* a # named cookie, to use as a template for other cookies. cookie_template = Cookie(None, None) for part in cookie_string.split(";"): if not '=' in part: continue k, v = part.strip().split("=", 1) # Unquote quoted values ('"..."' => '...') if v and '"' == v[0] == v[-1] and len(v) > 1: v = v[1:-1] if k[0] == '$': # Value pertains to most recently read cookie, # or cookie_template k = k[1:] if len(cookies) == 0: cookie = copy.copy(cookie_template) else: cookie = cookies[-1] try: setattr(cookie, cookie.attribute_dict[k], v) except KeyError: pass else: cookies.append(copy.copy(cookie_template)) cookies[-1].name = unquote(k) cookies[-1].value = unquote(v) return cookies pesto-25/pesto/testing.py0000644000175100017510000003131211613355542016446 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.testing ------------- Test utilities for WSGI applications. """ from itertools import chain try: from wsgiref.validate import validator as wsgi_validator except ImportError: # Redefine wsgi_validator as a no-op for Python < 2.5 wsgi_validator = lambda app: app from StringIO import StringIO from shutil import copyfileobj from urlparse import urlparse from pesto.response import Response from pesto.request import Request from pesto.wsgiutils import make_query CRLF = '\r\n' class MockResponse(Response): """ Response class with some extra methods to facilitate testing output of applications """ def __init__(self, content=None, status="200 OK", headers=None, onclose=None, add_default_content_type=True, **kwargs): super(MockResponse, self).__init__( content, status, headers, onclose, add_default_content_type, **kwargs ) # Buffer the content iterator to make sure that it is not exhausted # when inspecting it through the various debug methods self._content = list(content) if getattr(content, 'close', None): content.close() def __str__(self): """ Return a string representation of the entire response """ return ''.join( chain( ['%s\r\n' % (self.status,)], ('%s: %s\r\n' % (k, v) for k, v in self.headers), ['\r\n'], self.content ) ) def text(self): """ Return a string representation of the entire response, using newlines to separate headers, rather than the CRLF required by the HTTP spec. """ return ''.join( chain( ['%s\n' % (self.status,)], ('%s: %s\n' % (k, v) for k, v in self.headers), ['\n'], self.content ) ) @property def body(self): """ Content part as a single string """ return ''.join(self.content) class TestApp(object): response_class = MockResponse environ_defaults = { 'SCRIPT_NAME': "", 'PATH_INFO': "", 'QUERY_STRING': "", 'SERVER_NAME': "localhost", 'SERVER_PORT': "80", 'SERVER_PROTOCOL': "HTTP/1.0", 'REMOTE_ADDR': '127.0.0.1', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, } def __init__(self, app, charset='utf-8', **environ_defaults): self.app = app self.charset = charset self.environ_defaults = self.environ_defaults.copy() self.environ_defaults.update(**environ_defaults) @classmethod def make_environ(cls, REQUEST_METHOD='GET', PATH_INFO='', wsgi_input='', **kwargs): """ Generate a WSGI environ suitable for testing applications Example usage:: >>> from pprint import pprint >>> pprint(make_environ(PATH_INFO='/xyz')) # doctest: +ELLIPSIS {'PATH_INFO': '/xyz', 'QUERY_STRING': '', 'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.errors': , 'wsgi.input': , 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0)} """ SCRIPT_NAME = kwargs.pop('SCRIPT_NAME', cls.environ_defaults["SCRIPT_NAME"]) if SCRIPT_NAME and SCRIPT_NAME[-1] == "/": SCRIPT_NAME = SCRIPT_NAME[:-1] PATH_INFO = "/" + PATH_INFO environ = cls.environ_defaults.copy() environ.update(kwargs) for key, value in kwargs.items(): environ[key.replace('wsgi_', 'wsgi.')] = value if isinstance(wsgi_input, basestring): wsgi_input = StringIO(wsgi_input) environ.update({ 'REQUEST_METHOD': REQUEST_METHOD, 'SCRIPT_NAME': SCRIPT_NAME, 'PATH_INFO': PATH_INFO, 'wsgi.input': wsgi_input, 'wsgi.errors': StringIO(), }) if environ['SCRIPT_NAME'] == '/': environ['SCRIPT_NAME'] = '' environ['PATH_INFO'] = '/' + environ['PATH_INFO'] while PATH_INFO.startswith('//'): PATH_INFO = PATH_INFO[1:] return environ def _request(self, REQUEST_METHOD, PATH_INFO="", **kwargs): """ Generate a WSGI request of HTTP method ``REQUEST_METHOD`` and pass it to the application being tested. """ environ = self.make_environ(REQUEST_METHOD, PATH_INFO, **kwargs) if '?' in environ['PATH_INFO']: environ['PATH_INFO'], querystring = environ['PATH_INFO'].split('?', 1) if environ.get('QUERY_STRING'): environ['QUERY_STRING'] += querystring else: environ['QUERY_STRING'] = querystring app = wsgi_validator(self.app) return self.response_class.from_wsgi(app, environ, self.start_response) def get(self, PATH_INFO='/', data=None, charset='UTF-8', **kwargs): """ Make a GET request to the application and return the response. """ if data is not None: kwargs.setdefault('QUERY_STRING', make_query(data, charset=charset)) return self._request( 'GET', PATH_INFO=PATH_INFO, **kwargs ) def head(self, PATH_INFO='/', data=None, charset='UTF-8', **kwargs): """ Make a GET request to the application and return the response. """ if data is not None: kwargs.setdefault('QUERY_STRING', make_query(data, charset=charset)) return self._request( 'HEAD', PATH_INFO=PATH_INFO, **kwargs ) def start_response(self, status, headers, exc_info=None): """ WSGI start_response method """ def post(self, PATH_INFO='/', data=None, charset='UTF-8', **kwargs): """ Make a POST request to the application and return the response. """ if data is None: data = {} data = make_query(data, charset=charset) wsgi_input = StringIO(data) wsgi_input.seek(0) return self._request( 'POST', PATH_INFO=PATH_INFO, CONTENT_TYPE="application/x-www-form-urlencoded", CONTENT_LENGTH=str(len(data)), wsgi_input=wsgi_input, ) def post_multipart(self, PATH_INFO='/', data=None, files=None, charset='UTF-8', **kwargs): """ Create a MockWSGI configured to post multipart/form-data to the given URI. This is usually used for mocking file uploads :param data: dictionary of post data :param files: list of ``(name, filename, content_type, data)`` tuples. ``data`` may be either a byte string, iterator or file-like object. """ boundary = '----------------------------------------BoUnDaRyVaLuE' if data is None: data = {} if files is None: files = [] items = chain( ( ( [ ('Content-Disposition', 'form-data; name="%s"' % (name,)) ], data.encode(charset) ) for name, data in data.items() ), ( ( [ ('Content-Disposition', 'form-data; name="%s"; filename="%s"' % (name, fname)), ('Content-Type', content_type) ], data ) for name, fname, content_type, data in files ) ) post_data = StringIO() post_data.write('--' + boundary) for headers, data in items: post_data.write(CRLF) for name, value in headers: post_data.write('%s: %s%s' % (name, value, CRLF)) post_data.write(CRLF) if hasattr(data, 'read'): copyfileobj(data, post_data) elif isinstance(data, str): post_data.write(data) else: for chunk in data: post_data.write(chunk) post_data.write(CRLF) post_data.write('--' + boundary) post_data.write('--' + CRLF) length = post_data.tell() post_data.seek(0) kwargs.setdefault('CONTENT_LENGTH', str(length)) return self._request( 'POST', PATH_INFO, CONTENT_TYPE='multipart/form-data; boundary=%s' % boundary, wsgi_input=post_data, **kwargs ) make_environ = TestApp.make_environ class MockRequest(object): """ A mock object for testing WSGI applications Synopsis:: >>> from pesto.core import to_wsgi >>> from pesto.response import Response >>> mock = MockWSGI('http://www.example.com/nelly') >>> mock.request.request_uri 'http://www.example.com/nelly' >>> def app(request): ... return Response( ... content_type = 'text/html; charset=UTF-8', ... x_whoa = 'Nelly', ... content = ['Yop!'] ... ) >>> result = mock.run(to_wsgi(app)) #doctest: +ELLIPSIS >>> mock.headers [('Content-Type', 'text/html; charset=UTF-8'), ('X-Whoa', 'Nelly')] >>> mock.output ['Yop!'] >>> print str(mock) 200 OK\r Content-Type: text/html; charset=UTF-8\r X-Whoa: Nelly\r \r Yop! >>> """ def __init__(self, url=None, wsgi_input=None, SCRIPT_NAME='/', charset=None, **environ): from pesto.core import to_wsgi self.status = None self.headers = None self.output = None self.exc_info = None if wsgi_input is not None: self.wsgi_input = wsgi_input else: self.wsgi_input = StringIO() self.wsgi_errors = StringIO() self.environ = { 'REQUEST_METHOD' : "GET", 'SCRIPT_NAME' : "/", 'PATH_INFO' : "", 'QUERY_STRING' : "", 'CONTENT_TYPE' : "", 'CONTENT_LENGTH' : "", 'SERVER_NAME' : "localhost", 'SERVER_PORT' : "80", 'SERVER_PROTOCOL' : "HTTP/1.0", 'REMOTE_ADDR' : "127.0.0.1", 'wsgi.version' : (1, 0), 'wsgi.url_scheme' : "http", 'wsgi.input' : self.wsgi_input, 'wsgi.errors' : self.wsgi_errors, 'wsgi.multithread' : False, 'wsgi.multiprocess' : False, 'wsgi.run_once' : False, } self.mockapp = to_wsgi(lambda request: Response(['ok'])) if url is not None: scheme, netloc, path, params, query, fragment = urlparse(url) if scheme == '': scheme = 'http' if netloc == '': netloc = 'example.org' if ':' in netloc: server, port = netloc.split(':') else: if scheme == 'https': port = '443' else: port = '80' server = netloc assert path.startswith(SCRIPT_NAME) PATH_INFO = path[len(SCRIPT_NAME):] if SCRIPT_NAME and SCRIPT_NAME[-1] == "/": SCRIPT_NAME = SCRIPT_NAME[:-1] PATH_INFO = "/" + PATH_INFO self.environ.update({ 'wsgi.url_scheme' : scheme, 'SERVER_NAME' : server, 'SERVER_PORT' : port, 'SCRIPT_NAME' : SCRIPT_NAME, 'QUERY_STRING' : query, 'PATH_INFO' : PATH_INFO, }) self.environ.update(environ) if self.environ['SCRIPT_NAME'] == '/': self.environ['SCRIPT_NAME'] = '' self.environ['PATH_INFO'] = '/' + self.environ['PATH_INFO'] self.request = Request(self.environ) if charset is not None: self.request.charset = charset self.buf = StringIO() pesto-25/pesto/session/0000755000175100017510000000000011613356014016075 5ustar oliveroliver00000000000000pesto-25/pesto/session/base.py0000644000175100017510000004031111613355542017365 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ Web session management. """ __docformat__ = 'restructuredtext en' __all__ = ['session_middleware'] import logging import os import random import re import threading from time import sleep, time from pesto.request import Request from pesto.response import Response from pesto.cookie import Cookie from pesto.wsgiutils import ClosingIterator def get_session_id_from_querystring(environ): """ Return the session from the query string or None if no session can be read. """ pattern = re.escape(environ['pesto.sessionmanager'].COOKIE_NAME) + '=([0-9a-z]{%d})' % ID_LENGTH try: return re.search(pattern, environ.get("QUERY_STRING", "")).group(1) except AttributeError: return None def get_session_id_from_cookie(environ): """ Return the session from a cookie or None if no session can be read """ cookie = Request(environ).cookies.get(environ['pesto.sessionmanager'].COOKIE_NAME) if cookie and is_valid_id(cookie.value): return cookie.value return None try: import hashlib ID_LENGTH = hashlib.sha256().digest_size * 2 def generate_id(): """Generate a unique session ID""" return hashlib.sha256( str(os.getpid()) + str(time()) + str(random.random()) ).hexdigest() except ImportError: import sha ID_LENGTH = 40 def generate_id(): """Generate a unique session ID""" return sha.new( str(os.getpid()) + str(time()) + str(random.random()) ).hexdigest() def is_valid_id(session_id, pattern=re.compile('^[a-f0-9]{%d}$' % ID_LENGTH)): """ Return True if ``session_id`` is a well formed session id. This must be a hex string as produced by hashlib objects' ``hexdigest`` method. Synopsis:: >>> is_valid_id('a' * ID_LENGTH) True >>> is_valid_id('z' * ID_LENGTH) False >>> is_valid_id('a' * (ID_LENGTH - 1)) False """ try: return pattern.match(session_id) is not None except TypeError: return False class Session(object): """ Session objects store information about the http sessions """ # Indicates whether the session is newly created (ie within the current request) is_new = True def __init__(self, session_manager, session_id, is_new, data=None): """ Create a new session object within the given session manager. """ self.session_manager = session_manager self._changed = False self.session_id = session_id self.is_new = is_new self.data = {} if data is not None: self.data.update(data) def save_if_changed(self): """ Save the session in the underlying storage mechanism if the session is new or if it has been changed since being loaded. Note that this will only detect changes to the session object itself. If you store a mutable object within the session and change that then you must explicity call ``request.session.save`` to ensure your change is saved. Return ``True`` if the session was saved, or ``False`` if it was not necessary to save the session. """ if self._changed or self.is_new: self.save() self._changed = False self.is_new = False return True return False def save(self): """ Saves the session to the underlying storage mechanism. """ self.session_manager.store(self) def setdefault(self, key, value=None): self._changed = True return self.data.setdefault(key, value) def pop(self, key, default): self._changed = True return self.data.pop(key, default) def popitem(self): self._changed = True return self.data.popitem() def clear(self): self._changed = True return self.data.clear() def has_key(self, key): return self.data.has_key(key) def items(self): return self.data.items() def iteritems(self): return self.data.iteritems() def iterkeys(self): return self.data.iterkeys() def itervalues(self): return self.data.itervalues() def update(self, other, **kwargs): self._changed = True return self.data.update(other, **kwargs) def values(self): return self.data.values() def get(self, key, default=None): return self.data.get(key, default) def __getitem__(self, key): return self.data[key] def __iter__(self): return self.data.__iter__() def invalidate(self): """ invalidate and remove this session from the sessionmanager """ self.session_manager.remove(self.session_id) self.session_id = None def __setitem__(self, key, val): self._changed = True return self.data.__setitem__(key, val) def __delitem__(self, key): self._changed = True return self.data.__delitem__(key) def text(self): """ Return a useful text representation of the session """ import pprint return "<%s id=%s, is_new=%s\n%s\n>" % ( self.__class__.__name__, self.session_id, self.is_new, pprint.pformat(self) ) class SessionManagerBase(object): """ Manages Session objects using an ObjectStore to persist the sessions. """ # Which version of the pickling protocol to select. PICKLE_PROTOCOL = -1 # Key to use in HTTP cookies COOKIE_NAME = "pesto_session" def load(self, session_id): """ Load a session object from this sessionmanager. Note that if ``session_id`` cannot be found in the underlying storage, a new session id will be created. """ self.acquire_lock(session_id) try: data = self._get_session_data(session_id) if data is None: # Generate a fresh session with a new id session = Session(self, generate_id(), is_new=True, data=data) else: session = Session(self, session_id, is_new=False, data=data) self.update_access_time(session.session_id) return session finally: self.release_lock(session_id) def update_access_time(self, session_id): raise NotImplementedError def get_access_time(self, session_id): raise NotImplementedError def acquire_lock(self, session_id=None): """ Acquire a lock for the given session_id. If session_id is none, then the whole storage should be locked. """ raise NotImplementedError def release_lock(self, session_id=None): raise NotImplementedError def read_session(self, session_id): """ Return a session object from the given ``session_id``. If ``session_id`` is None a new session will be generated. Synopsis:: >>> from pesto.session.memorysessionmanager import MemorySessionManager >>> sm = MemorySessionManager() >>> sm.read_session(None) #doctest: +ELLIPSIS """ if session_id is not None: session = self.load(session_id) else: session = Session(self, generate_id(), is_new=True) self.update_access_time(session.session_id) return session def __contains__(self, session_id): """ Return true if the given session id exists in this sessionmanager """ raise NotImplementedError def store(self, session): """ Save the given session object in this sessionmanager. """ self.acquire_lock(session.session_id) try: self._store(session) finally: self.release_lock(session.session_id) def _store(self, session): """ Write session data to the underlying storage. Subclasses must implement this method """ def remove(self, session_id): """ Remove the specified session from the session manager. """ self.acquire_lock(session_id) try: self._remove(session_id) finally: self.release_lock(session_id) def _remove(self, session_id): """ Remove the specified session from the underlying storage. Subclasses must implement this method """ raise NotImplementedError def _get_session_data(self, session_id): """ Return a dict of the session data from the underlying storage, or ``None`` if the session does not exist. """ raise NotImplementedError def close(self): """ Close the persistent store cleanly. """ self.acquire_lock() try: self._close() finally: self.release_lock() def _close(self): """ Default implementation: do nothing """ def purge(self, olderthan=1800): for session_id in self._purge_candidates(olderthan): self.remove(session_id) def _purge_candidates(self, olderthan=1800): """ Return a list of session ids ready to be purged from the session manager. """ raise NotImplementedError class ThreadsafeSessionManagerBase(SessionManagerBase): """ Base class for sessioning to run in a threaded environment. DOES NOT GUARANTEE PROCESS-LEVEL SAFETY! """ def __init__(self): super(ThreadsafeSessionManagerBase, self).__init__() self._access_times = {} self._lock = threading.RLock() def _purge_candidates(self, olderthan=1800): """ Purge all sessions older than ``olderthan`` seconds. """ # Re-importing time fixes exception raised on interpreter shutdown from time import time expiry = time() - olderthan self.acquire_lock() try: return [ id for id, access_time in self._access_times.iteritems() if access_time < expiry ] finally: self.release_lock() def acquire_lock(self, session_id=None): self._lock.acquire() def release_lock(self, session_id=None): self._lock.release() def update_access_time(self, session_id): self.acquire_lock() try: self._access_times[session_id] = time() finally: self.release_lock() def get_access_time(self, session_id): """ Return the time the given session_id was last accessed """ return self._access_times[session_id] def _remove(self, session_id): """ Subclasses should call this implementation to ensure the access_times dictionary is kept up to date. """ try: del self._access_times[session_id] except KeyError: logging.warn("tried to remove non-existant session id %r", session_id) def __contains__(self, session_id): return session_id in self._access_times def start_thread_purger(sessionmanager, howoften=60, olderthan=1800, lock=threading.Lock()): """ Start a thread to purge sessions older than ``olderthan`` seconds every ``howoften`` seconds. """ def _purge(): while True: sleep(howoften) sessionmanager.purge(olderthan) lock.acquire() try: if hasattr(sessionmanager, '_purge_thread'): # Don't start the thread twice return sessionmanager._purge_thread = threading.Thread(target=_purge) sessionmanager._purge_thread.setDaemon(True) sessionmanager._purge_thread.start() finally: lock.release() def session_middleware( session_manager, auto_purge_every=60, auto_purge_olderthan=1800, persist='cookie', cookie_path=None, cookie_domain=None ): """ WSGI middleware application for sessioning. Synopsis:: >>> from pesto.session.memorysessionmanager import MemorySessionManager >>> def my_wsgi_app(environ, start_response): ... session = environ['pesto.session'] ... >>> app = session_middleware(MemorySessionManager())(my_wsgi_app) >>> session_manager An implementation of ``pesto.session.base.SessionManagerBase`` auto_purge_every If non-zero, a separate thread will be launched which will purge expired sessions every ``auto_purge_every`` seconds. In a CGI environment (or equivalent, detected via, ``environ['wsgi.run_once']``) the session manager will be purged after every request. auto_purge_olderthan Auto purge sessions older than ``auto_purge_olderthan`` seconds. persist Either ``cookie`` or ``querystring``. If set to ``cookie`` then sessions will be automatically persisted via a session cookie. If ``querystring`` then the session-id will be read from the querystring. However it is up to the underlying application to ensure that the session-id is embedded into all links generated by the application. cookie_path The path to use when setting cookies. If ``None`` this will be taken from the ``SCRIPT_NAME`` variable. cookie_domain The domain to use when setting cookies. If ``None`` this will not be set and the browser will default to the domain used for the request. """ get_session_id = { 'cookie': get_session_id_from_cookie, 'querystring': get_session_id_from_querystring, }[persist] def middleware(app): def sessionmanager_middleware(environ, start_response): if environ['wsgi.run_once'] and auto_purge_every > 0: session_manager.purge(auto_purge_olderthan) environ['pesto.sessionmanager'] = session_manager session = session_manager.read_session(get_session_id(environ)) environ['pesto.session'] = session my_start_response = start_response if persist == 'cookie' and session.is_new: def my_start_response(status, headers, exc_info=None): _cookie_path = cookie_path if _cookie_path is None: _cookie_path = environ.get('SCRIPT_NAME') if not _cookie_path: _cookie_path = '/' cookie = Cookie( session_manager.COOKIE_NAME, session.session_id, path=_cookie_path, domain=cookie_domain, http_only=True ) return start_response( status, list(headers) + [("Set-Cookie", str(cookie))], exc_info ) elif persist == 'querystring': request = Request(environ) if session.is_new and environ['REQUEST_METHOD'] == 'GET': query_session_id = request.query.get(session_manager.COOKIE_NAME) if query_session_id != session.session_id: new_query = [ item for item in request.query.iterallitems() if item[0] != session_manager.COOKIE_NAME ] new_query.append((session_manager.COOKIE_NAME, session.session_id)) return ClosingIterator( Response.redirect( request.make_uri(query=new_query) )(environ, my_start_response), session.save_if_changed ) return ClosingIterator(app(environ, my_start_response), session.save_if_changed) if auto_purge_every > 0: start_thread_purger( session_manager, howoften = auto_purge_every, olderthan = auto_purge_olderthan ) return sessionmanager_middleware return middleware pesto-25/pesto/session/__init__.py0000644000175100017510000000022511613355542020212 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. from pesto.session.base import * pesto-25/pesto/session/filesessionmanager.py0000644000175100017510000001044411613355542022335 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.session.filesessionmanager -------------------------------- Store request sessions in flat files. Usage:: >>> from pesto.session import session_middleware >>> from pesto.session.filesessionmanager import FileSessionManager >>> def my_app(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/html')]) ... yield "Whoa nelly!" ... >>> manager = FileSessionManager('./sessions') >>> app = session_middleware(manager)(my_app) """ __docformat__ = 'restructuredtext en' __all__ = ['FileSessionManager'] import cPickle import logging import os import time import stat from pesto.session.base import SessionManagerBase class FileSessionManager(SessionManagerBase): """ A File-backed session manager. Synopsis:: >>> from pesto.session import session_middleware >>> from pesto.session.filesessionmanager import FileSessionManager >>> manager = FileSessionManager('/tmp/sessions') >>> def app(environ, start_response): ... "WSGI application code here" ... >>> app = session_middleware(manager)(app) >>> """ def __init__(self, directory): """ directory Path to directory in which session files will be stored. """ super(FileSessionManager, self).__init__() self.directory = os.path.join(directory, '_pesto_sessions') def acquire_lock(self, session_id=None): """ Acquire lock for the storage """ def release_lock(self, session_id=None): """ Release lock for the storage """ def get_path(self, session_id): """ Return the path to the file where session data is stored. Synopsis:: >>> from pesto.session.base import Session >>> fsm = FileSessionManager(directory='/tmp') >>> session = Session(fsm, 'abcdefgh', True) >>> fsm.get_path(session.session_id) '/tmp/_pesto_sessions/ab/abcdefgh' """ return os.path.join(self.directory, session_id[:2], session_id) def store(self, session): """ Store ``session`` to a file """ path = self.get_path(session.session_id) try: os.makedirs(os.path.dirname(path)) except OSError: # Path exists or cannot be created. The latter error will be # picked up later :) pass f = open(path, 'w') try: cPickle.dump(session.data, f) finally: f.close() def _get_session_data(self, session_id): path = self.get_path(session_id) try: f = open(path, 'r') except IOError: return None try: try: return cPickle.load(f) except (EOFError, IOError): logging.exception("Could not read session %r", session_id) return None finally: f.close() def update_access_time(self, session_id): """ Update session access time, by calling ``os.utime`` on the session file. """ try: os.utime(self.get_path(session_id), None) except OSError: pass def get_access_time(self, session_id): """ Return the time the given session was last accessed. Note that this uses the underlying filesystem's atime attribute, so will not work on filesystems mounted with noatime """ return os.stat(self.get_path(session_id)).st_atime def _remove(self, session_id): try: os.unlink(self.get_path(session_id)) except OSError: logging.exception("Could not remove session file for %r", self.get_path(session_id)) def _purge_candidates(self, olderthan=1800): remove_from = time.time() - olderthan for directory, dirs, files in os.walk(self.directory): for filename in files: if os.stat(os.path.join(directory, filename))[stat.ST_MTIME] < remove_from: yield filename def __contains__(self, session_id): return os.path.exists(self.get_path(session_id)) pesto-25/pesto/session/memorysessionmanager.py0000644000175100017510000000436711613355542022735 0ustar oliveroliver00000000000000# Copyright (c) 2007-2010 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.session.memorysessionmanager ----------------------------------- Store request sessions in memory Usage:: >>> from pesto.session import session_middleware >>> from pesto.session.memorysessionmanager import MemorySessionManager >>> def my_app(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/html')]) ... yield "Whoa nelly!" ... >>> manager = MemorySessionManager() >>> app = session_middleware(manager)(my_app) """ __docformat__ = 'restructuredtext en' __all__ = ['MemorySessionManager'] from pesto.session.base import ThreadsafeSessionManagerBase from repoze.lru import LRUCache class MemorySessionManager(ThreadsafeSessionManagerBase): """ An in-memory session manager. Synopsis:: >>> from pesto.session import session_middleware >>> from pesto.session.memorysessionmanager import MemorySessionManager >>> manager = MemorySessionManager() >>> def app(environ, start_response): ... "WSGI application code here" ... >>> app = session_middleware(manager)(app) >>> """ def __init__(self, cache_size=200): """ cache_size The maximum number of session objects to store. If zero this will be unlimited, otherwise, a least recently used cache mechanism will be used to store only up to ``cache_size`` objects. """ super(MemorySessionManager, self).__init__() self._cache = LRUCache(cache_size) def _store(self, session): """ Store session ``session``. """ self._cache.put(session.session_id, session.data) def _get_session_data(self, session_id): """ Retrieve session identified by ``session_id``. """ return self._cache.get(session_id, None) def _remove(self, session_id): try: super(MemorySessionManager, self)._remove(session_id) self._cache.put(session_id, None) except KeyError: pass def __contains__(self, session_id): return self._cache.get(session_id) is not None pesto-25/pesto/httputils.py0000644000175100017510000002524611613355542017042 0ustar oliveroliver00000000000000# Copyright (c) 2009-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. """ pesto.httputils --------------- Utility functions to handle HTTP data """ try: from email.message import Message from email.parser import Parser except ImportError: from email.Message import Message from email.Parser import Parser import re from urllib import unquote_plus from shutil import copyfileobj from pesto.utils import ExpandableOutput, SizeLimitedInput, PutbackInput, DelimitedInput KB = 1024 MB = 1024 * KB class RequestParseError(Exception): """ Error encountered while parsing the HTTP request """ def response(self): """ Return a ``pesto.response.Response`` object to represent this error condition """ from pesto.response import Response return Response.bad_request().add_header('X-Pesto-Exception', repr(self)) class TooBig(RequestParseError): """ Request body is too big """ def response(self): """ Return a ``pesto.response.Response`` object to represent this error condition """ from pesto.response import Response return Response.request_entity_too_large() class MissingContentLength(RequestParseError): """ No ``Content-Length`` header given """ def response(self): """ Return a ``pesto.response.Response`` object to represent this error condition """ from pesto.response import Response return Response.length_required() def dequote(s): """ Return ``s`` with surrounding quotes removed. Example usage:: >>> dequote('foo') 'foo' >>> dequote('"foo"') 'foo' """ if len(s) > 1 and s[0] == '"' == s[-1]: return s[1:-1] return s def parse_header(header): """ Given a header, return a tuple of ``(value, [(parameter_name, parameter_value)])``. Example usage:: >>> parse_header("text/html; charset=UTF-8") ('text/html', {'charset': 'UTF-8'}) >>> parse_header("multipart/form-data; boundary=---------------------------7d91772e200be") ('multipart/form-data', {'boundary': '---------------------------7d91772e200be'}) """ items = header.split(';') pairs = [ (name, dequote(value)) for name, value in ( item.lstrip().split('=', 1) for item in items[1:] ) ] return (items[0], dict(pairs)) def parse_querystring( data, charset=None, strict=False, keep_blank_values=True, pairsplitter=re.compile('[;&]').split ): """ Return ``(key, value)`` pairs from the given querystring:: >>> list(parse_querystring('green%20eggs=ham;me=sam+i+am')) [(u'green eggs', u'ham'), (u'me', u'sam i am')] :param charset: Character encoding used to decode values. If not specified, ``pesto.DEFAULT_CHARSET`` will be used. :param keep_blank_values: if True, keys without associated values will be returned as empty strings. if False, no key, value pair will be returned. :param strict: if ``True``, a ``ValueError`` will be raised on parsing errors. """ if charset is None: charset = DEFAULT_CHARSET for item in pairsplitter(data): if not item: continue try: key, value = item.split('=', 1) except ValueError: if strict: raise RequestParseError("bad query field: %r" % (item,)) if not keep_blank_values: continue key, value = item, '' try: yield unquote_plus(key).decode(charset), unquote_plus(value).decode(charset) except UnicodeDecodeError: raise RequestParseError("Invalid character data: can't decode using %r" % (charset,)) def parse_post(environ, fp, default_charset=None, max_size=16*KB, max_multipart_size=2*MB): """ Parse the contents of an HTTP POST request, which may be either application/x-www-form-urlencoded or multipart/form-data encoded. Returned items are either tuples of (name, value) for simple string values or (name, FileUpload) for uploaded files. :param max_multipart_size: Maximum size of total data for a multipart form submission :param max_size: The maximum size of data allowed to be read into memory. For a application/x-www-form-urlencoded submission, this is the maximum size of the entire data. For a multipart/form-data submission, this is the maximum size of any individual field (except file uploads). """ content_type, content_type_params = parse_header( environ.get('CONTENT_TYPE', 'application/x-www-form-urlencoded') ) if default_charset is None: default_charset = DEFAULT_CHARSET charset = content_type_params.get('charset', default_charset) try: content_length = int(environ['CONTENT_LENGTH']) except (TypeError, ValueError, KeyError): raise MissingContentLength() if content_type == 'application/x-www-form-urlencoded': if content_length > max_size: raise TooBig("Content Length exceeds permitted size") return parse_querystring(SizeLimitedInput(fp, content_length).read(), charset) else: if content_length > max_multipart_size: raise TooBig("Content Length exceeds permitted size") try: boundary = content_type_params['boundary'] except KeyError: raise RequestParseError("No boundary given in multipart/form-data content-type") return parse_multipart(SizeLimitedInput(fp, content_length), boundary, charset, max_size) class HTTPMessage(Message): """ Represent HTTP request message headers """ CHUNK_SIZE = 8192 def parse_multipart(fp, boundary, default_charset, max_size): """ Parse data encoded as ``multipart/form-data``. Generate tuples of:: (, ) Where ``data`` will be a string in the case of a regular input field, or a ``FileUpload`` instance if a file was uploaded. :param fp: input stream from which to read data :param boundary: multipart boundary string, as specified by the ``Content-Disposition`` header :param default_charset: character set to use for encoding, if not specified by a content-type header. In practice web browsers don't supply a content-type header so this needs to contain a sensible value. :param max_size: Maximum size in bytes for any non file upload part """ boundary_size = len(boundary) if not boundary.startswith('--'): raise RequestParseError("Malformed boundary string: must start with '--' (rfc 2046)") if boundary_size > 72: raise RequestParseError("Malformed boundary string: must be no more than 70 characters, not counting the two leading hyphens (rfc 2046)") assert boundary_size + 2 < CHUNK_SIZE, "CHUNK_SIZE cannot be smaller than the boundary string" if fp.read(2) != '--': raise RequestParseError("Malformed POST data: expected two hypens") if fp.read(boundary_size) != boundary: raise RequestParseError("Malformed POST data: expected boundary") if fp.read(2) != '\r\n': raise RequestParseError("Malformed POST data: expected CRLF") fp = PutbackInput(fp) while True: headers, data = _read_multipart_field(fp, boundary) try: disposition_type, params = parse_header(headers['Content-Disposition']) except KeyError: raise RequestParseError("Missing Content-Disposition header") try: name = params['name'] except KeyError: raise RequestParseError("Missing name parameter in Content-Disposition header") is_file_upload = 'Content-Type' in headers and 'filename' in params if is_file_upload: io = data._io io.seek(0) yield name, FileUpload(params['filename'], headers, io) else: charset = parse_header(headers.get('Content-Type', ''))[1].get('charset', default_charset) if data.tell() > max_size: raise TooBig("Data block exceeds maximum permitted size") try: data.seek(0) yield name, data.read().decode(charset) except UnicodeDecodeError: raise RequestParseError("Invalid character data: can't decode using %r" % (charset,)) chunk = fp.read(2) if chunk == '--': if fp.peek(3) != '\r\n': raise RequestParseError("Expected terminating CRLF at end of stream") break if chunk != '\r\n': raise RequestParseError("Expected CRLF after boundary") CONTENT_DISPOSITION_FORM_DATA = 'form-data' CONTENT_DISPOSITION_FILE_UPLOAD = 'file-upload' def _read_multipart_field(fp, boundary): """ Read a single part from a multipart/form-data message and return a tuple of ``(headers, data)``. Stream ``fp`` must be positioned at the start of the header block for the field. Return a tuple of ('', '') ``headers`` is an instance of ``email.message.Message``. ``data`` is an instance of ``ExpandableOutput``. Note that this currently cannot handle nested multipart sections. """ data = ExpandableOutput() headers = Parser(_class=HTTPMessage).parse( DelimitedInput(fp, '\r\n\r\n'), headersonly=True ) fp = DelimitedInput(fp, '\r\n--' + boundary) # XXX: handle base64 encoding etc for chunk in iter(lambda: fp.read(CHUNK_SIZE), ''): data.write(chunk) data.flush() # Fallen off the end of the input without having read a complete field? if not fp.delimiter_found: raise RequestParseError("Incomplete data (expected boundary)") return headers, data class FileUpload(object): """ Represent a file uploaded in an HTTP form submission """ def __init__(self, filename, headers, fileob): self.filename = filename self.headers = headers self.file = fileob # UNC/Windows path if self.filename[:2] == '\\\\' or self.filename[1:3] == ':\\': self.filename = self.filename[self.filename.rfind('\\')+1:] def save(self, fileob): """ Save the upload to the file object or path ``fileob`` :param fileob: a file-like object open for writing, or the path to the file to be written """ if isinstance(fileob, basestring): fileob = open(fileob, 'w') try: return self.save(fileob) finally: fileob.close() self.file.seek(0) copyfileobj(self.file, fileob) # Imports at end to avoid circular dependencies from pesto import DEFAULT_CHARSET pesto-25/THANKS.txt0000644000175100017510000000055511613355542015023 0ustar oliveroliver00000000000000THANKS ------ * Mateusz Korniak for his help finding and fixing cookie parsing and mod_wsgi related bugs * Ferry Pierre for session access time and removal fixes * Christian Jauvin for suggesting the options to override the session cookie path and other improvements to session handling. * Brian Peiris for his help in providing Python 2.4 compatibility fixes pesto-25/pesto.egg-info/0000755000175100017510000000000011613356014016104 5ustar oliveroliver00000000000000pesto-25/pesto.egg-info/top_level.txt0000644000175100017510000000000611613356014020632 0ustar oliveroliver00000000000000pesto pesto-25/pesto.egg-info/SOURCES.txt0000644000175100017510000000146711613356014020000 0ustar oliveroliver00000000000000AUTHORS.txt CHANGELOG.txt FAQ.txt INSTALL.txt LICENSE.txt MANIFEST.in NEWS.txt README.txt THANKS.txt TODO.txt VERSION.txt setup.py doc/Makefile doc/caching.rst doc/conf.py doc/cookies.rst doc/dispatch.rst doc/getting_started.rst doc/httputils.rst doc/index.rst doc/request.rst doc/response.rst doc/session.rst doc/utils.rst doc/wsgiutils.rst pesto/__init__.py pesto/caching.py pesto/cookie.py pesto/core.py pesto/dispatch.py pesto/httputils.py pesto/request.py pesto/response.py pesto/testing.py pesto/utils.py pesto/wsgiutils.py pesto.egg-info/PKG-INFO pesto.egg-info/SOURCES.txt pesto.egg-info/dependency_links.txt pesto.egg-info/not-zip-safe pesto.egg-info/requires.txt pesto.egg-info/top_level.txt pesto/session/__init__.py pesto/session/base.py pesto/session/filesessionmanager.py pesto/session/memorysessionmanager.pypesto-25/pesto.egg-info/PKG-INFO0000644000175100017510000003163211613356014017206 0ustar oliveroliver00000000000000Metadata-Version: 1.0 Name: pesto Version: 25 Summary: Library for WSGI applications Home-page: http://www.ollycope.com/software/pesto Author: Oliver Cope Author-email: oliver@redgecko.org License: BSD Description: Pesto is a library for Python web applications. Its aim is to make writing WSGI web applications easy and fun. Pesto doesn't constrain you -- how you integrate with databases, what templating system you use or how you prefer to lay out your source files is up to you. Above all, pesto is small, well documented and well tested. Pesto makes it easy to: - Map any URI to any part of your application. - Produce unicode aware, standards compliant WSGI applications. - Interrogate WSGI request information -- form variables and HTTP request headers. - Create and manipulate HTTP headers, redirects, cookies etc. - Integrate with any other WSGI application or middleware, giving you access to a vast and growing resource. Development status ------------------ Pesto is production ready and used on a wide variety of websites. To browse or check out the latest development version, visit http://patch-tag.com/repo/pesto. For documentation, visit http://pesto.redgecko.org/. Licence -------- Pesto is available under the terms of the `new BSD licence `_. Documentation -------------- Full documentation is included with each release. Documentation for the latest version is available at . Version 25 * Fixed bug in pesto.utils.MultiDict and added a MultiDict.extend method Version 24 * Bugfixes for pesto.utils.MultiDict and pesto.dispatch * pesto.utils.with_request_args decorator handles malformed input more gracefully Version 23 * Fix for exc_info handling Version 22 * Partial Python 2.4 backwards compatibility (thanks to Brian Peiris). * Improvements to the handling of close methods on content iterators * Other minor bugfixes Version 21 * HEAD requests are passed to the GET handler if no explicit HEAD handler is set up. * DispatcherApp can now take a ``prefix`` argument that is prepended to all match patterns * Now uses the repoze.lru LRU cache implementation. Version 20 * Fixed RuntimeError when used under mod_wsgi (thanks to Mateusz Korniak) * Fixed error parsing information out of user-agent cookie headers (thanks to Mateusz Korniak) Version 19 * pesto.wsgiutils.serve_static_file now correctly sets the Last-Modified header (thanks to Samuel Wan) * Better compliance with HTTP RFC and WSGI spec on 204 No Content and 304 Not Modified responses * Restored Python 2.5 compatibility Version 18 * PestoWSGIApplication/to_wsgi may now be used to decorate class methods as well as regular functions * Minor bugfixes Version 17 * pesto.util.MultiDict now retains insertion order of keys. This is useful for form processing where you want to know the order in which fields are submitted. * Fixed bugs relating to path handling functions stripping trailing slashes eg in ``pesto.wsgiutils.make_absolute_url``. Also ``pesto.dispatch.dispatcher_app`` no longer strips trailing slashes either, it left to the application to do this if desired. Version 16 * Fixed error introduced into session handling, where the session cookie was not resent if an old session disappeared Version 15 * Corrected package version number * Included fixes for session handling (thanks to Ferry Pierre). Version 14 * FileSessionManager now saves files under dedicated _pesto_sessions subdirectory, making it safer to use when initialized on a shared temporary directory. * Corrected pesto.wsgiutils.mount_app, which previously passed a reference to an out of date request object to sub apps * Altered the url() function acquired by dispatcher_app decorated functions to allow script_name, netloc and URL scheme to be specified, making it possible for multiple Pesto dispatcher_apps mounted at different paths or network locations to generate valid URLs for each other. * pesto.wsgiutils.with_request_args now raises error objects that may be more easily separated out from other errors in middleware layers if required. * Session.save() now forces a save even if no changes have been detected (which may happen if you store mutable objects in the session) * pesto.wsgiutils.with_request_args now raises exceptions generated when parsing arguments from request data. This behaviour is generally more useful to developers than an error page with no information about what parameter is missing. Version 13 * Added HttpOnly option to cookies * Session middleware can now have the cookie path and domain overridden Version 12 * Added a path_info argument to request.make_uri * Improved support for dispatcher_app to dispatch to bound methods * Added many missing docstrings and other pylint/cheesecake suggested changes * Extended request.make_uri to take any query argument that wsgiutils.make_query will accept * make_query now supports taking a list of (name, value) tuples Version 11 * Removed dbm and rdbms session managers, these were untested, undocumented and were not recommended for use. * Added fix for parse_querystring when qs is empty * Fixed error on non-UTF8 encoded request body * Added X-Pesto-Exception header to help debugging of webapps * Improved dispatch debug logging * Fixed issue when using dispatcher predicates and caching * Reversed dispatcher function matching precedence: the first matching function wins * Remove pesto.wsgiutils.MockWSGI and replace with classes in a new pesto.testing module * Added a decorators argument to dispatcher_app.match, allowing handler functions to have decorators which are only applied in the context of web requests * Request.cookies is now a MultiDict object with the same API as Request.form, Request.files and Request.query * Renamed urldispatcher to dispatcher_app to clarify intent of this class. * Made response.Response normalize header case and sort headers on ingress, fixing a few consistency issues. * Adjusted response header handling to be standards compliant on 304 responses and ETag headers * Use wsgiref.validator in MockWSGI * Refactored some parts of request.py and wsgiutils.py into new httputils.py and utils.py modules. * Removed dependency on stdlib cgi.py (see http://mail.python.org/pipermail/web-sig/2009-May/003822.html for reasons). * Changed variables named 'encoding' to 'charset' to make the naming more consistent with HTTP. * Removed support for Python 2.3. From now on pesto is actively tested on 2.5 and 2.6, and I will try to support 2.4 if bugs are reported. Version 10 * Fixes in documentation and packaging, but no changes to functionality Version 9 * Added a predicate argument to dispatcher.match, allowing for extra arbitrary checks before routing a URL. * Add basic ETag support * Removed http exceptions * Added support for wsigorg.routing_args * WSGI apps created with pesto now have execution deferred until the first iteration. Therefore the iterator's .close method can be used to reliably release resources etc. * Fix for Response objects not catching close methods on upstream content iterators * Added onclose kwarg for Response object. Any functions in onclose will be called on the wsgi close event. * Added request.files object to complement request.form and request.query * Added MultiDict object to replace FieldStorageWrapper * Renamed register_pattern to register_converter for consistency * Allowed multiple close functions to be passed to ClosingIterator * request.make_uri now uri quotes paths * Rewrote urldispatcher to use a much more flexible URL matching syntax * Refactored Request initialisation to ensure that multiple calls don't attempt to reinitialize request parameters * added Response.getheaders, Response.getheader and Response.from_wsgi * add an ExtensiblePattern to dispatcher and enable it by default * Changed dispatcher to be a WSGI callable * Added wsgiutils.ClosingIterator and wsgiutils.overlay * Pesto response objects are now valid WSGI apps * Ensure the same Request instance is returned when called multiple times on the same environ object * Added a function for mapping request form parameters to function arguments * Updated documentation: now requires sphinx for building docs * Renamed WSGIRequest to Request for consistency with Response * Changed 'despatcher' to 'dispatcher' throughout, after it was pointed out that no one spells it this way any more Version 8 * Merged pesto.utils with pesto.wsgiutils * Refactored builtin static server to allow it to be used to serve ad-hoc single files * Added a make_query utility function to wsgiutils * Fixed request.make_uri when presented with a relative URI. expanded the docstring and tests. * Static server fixes for windows environments Version 7 * Reimplemented session saving machinery using the WSGI standard close() mechanism. * Request.FieldStorageWrapper now raises a KeyError on non-existant item access Version 6 * Added uri_join and fixed request.make_uri path concatenation logic * Moved debug param out of despatch class and into despatcher_app, where it can be applied across all despatchers in operation * Removed duplicate default charset/encoding setting * Added Response.bad_request() classmethod * Added Response.buffered method * Added Response.add_cookie Version 5 * Changed wsgiutils.make_absolute_url to bring into line with PEP 333 and improve handling of relative URIs * Added Response.redirect, Response.not_found etc classmethods, replacing previous freestanding functions * make_query now allows a separator character to be specified * pesto.currentrequest() returns None when no request available * Add WSGIRequest.query to model querystring data. Both WSGIRequest.form and .query are now properties and lazily instantiated. Version 4 * First publicly released version! Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Software Development :: Libraries :: Python Modules pesto-25/pesto.egg-info/dependency_links.txt0000644000175100017510000000000111613356014022152 0ustar oliveroliver00000000000000 pesto-25/pesto.egg-info/requires.txt0000644000175100017510000000001711613356014020502 0ustar oliveroliver00000000000000repoze.lru>=0.3pesto-25/pesto.egg-info/not-zip-safe0000644000175100017510000000000111613355551020337 0ustar oliveroliver00000000000000 pesto-25/LICENSE.txt0000644000175100017510000000261111613355542015110 0ustar oliveroliver00000000000000Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of Oliver Cope may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pesto-25/setup.py0000644000175100017510000000263511613355542015005 0ustar oliveroliver00000000000000#!/usr/bin/env python # Copyright (c) 2007-2011 Oliver Cope. All rights reserved. # See LICENSE.txt for terms of redistribution and use. import os from setuptools import setup, find_packages def read(*path): """ Read and return content from ``path`` """ f = open( os.path.join( os.path.dirname(__file__), *path ), 'r' ) try: return f.read().decode('UTF-8') finally: f.close() setup( name='pesto', version=read('VERSION.txt').strip().encode('ASCII'), description='Library for WSGI applications', long_description=read('README.txt') + "\n\n" + read("CHANGELOG.txt"), author='Oliver Cope', license = 'BSD', author_email='oliver@redgecko.org', url='http://www.ollycope.com/software/pesto', zip_safe = False, classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: WSGI', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), install_requires=[ 'repoze.lru>=0.3', ] ) pesto-25/PKG-INFO0000644000175100017510000003163211613356014014362 0ustar oliveroliver00000000000000Metadata-Version: 1.0 Name: pesto Version: 25 Summary: Library for WSGI applications Home-page: http://www.ollycope.com/software/pesto Author: Oliver Cope Author-email: oliver@redgecko.org License: BSD Description: Pesto is a library for Python web applications. Its aim is to make writing WSGI web applications easy and fun. Pesto doesn't constrain you -- how you integrate with databases, what templating system you use or how you prefer to lay out your source files is up to you. Above all, pesto is small, well documented and well tested. Pesto makes it easy to: - Map any URI to any part of your application. - Produce unicode aware, standards compliant WSGI applications. - Interrogate WSGI request information -- form variables and HTTP request headers. - Create and manipulate HTTP headers, redirects, cookies etc. - Integrate with any other WSGI application or middleware, giving you access to a vast and growing resource. Development status ------------------ Pesto is production ready and used on a wide variety of websites. To browse or check out the latest development version, visit http://patch-tag.com/repo/pesto. For documentation, visit http://pesto.redgecko.org/. Licence -------- Pesto is available under the terms of the `new BSD licence `_. Documentation -------------- Full documentation is included with each release. Documentation for the latest version is available at . Version 25 * Fixed bug in pesto.utils.MultiDict and added a MultiDict.extend method Version 24 * Bugfixes for pesto.utils.MultiDict and pesto.dispatch * pesto.utils.with_request_args decorator handles malformed input more gracefully Version 23 * Fix for exc_info handling Version 22 * Partial Python 2.4 backwards compatibility (thanks to Brian Peiris). * Improvements to the handling of close methods on content iterators * Other minor bugfixes Version 21 * HEAD requests are passed to the GET handler if no explicit HEAD handler is set up. * DispatcherApp can now take a ``prefix`` argument that is prepended to all match patterns * Now uses the repoze.lru LRU cache implementation. Version 20 * Fixed RuntimeError when used under mod_wsgi (thanks to Mateusz Korniak) * Fixed error parsing information out of user-agent cookie headers (thanks to Mateusz Korniak) Version 19 * pesto.wsgiutils.serve_static_file now correctly sets the Last-Modified header (thanks to Samuel Wan) * Better compliance with HTTP RFC and WSGI spec on 204 No Content and 304 Not Modified responses * Restored Python 2.5 compatibility Version 18 * PestoWSGIApplication/to_wsgi may now be used to decorate class methods as well as regular functions * Minor bugfixes Version 17 * pesto.util.MultiDict now retains insertion order of keys. This is useful for form processing where you want to know the order in which fields are submitted. * Fixed bugs relating to path handling functions stripping trailing slashes eg in ``pesto.wsgiutils.make_absolute_url``. Also ``pesto.dispatch.dispatcher_app`` no longer strips trailing slashes either, it left to the application to do this if desired. Version 16 * Fixed error introduced into session handling, where the session cookie was not resent if an old session disappeared Version 15 * Corrected package version number * Included fixes for session handling (thanks to Ferry Pierre). Version 14 * FileSessionManager now saves files under dedicated _pesto_sessions subdirectory, making it safer to use when initialized on a shared temporary directory. * Corrected pesto.wsgiutils.mount_app, which previously passed a reference to an out of date request object to sub apps * Altered the url() function acquired by dispatcher_app decorated functions to allow script_name, netloc and URL scheme to be specified, making it possible for multiple Pesto dispatcher_apps mounted at different paths or network locations to generate valid URLs for each other. * pesto.wsgiutils.with_request_args now raises error objects that may be more easily separated out from other errors in middleware layers if required. * Session.save() now forces a save even if no changes have been detected (which may happen if you store mutable objects in the session) * pesto.wsgiutils.with_request_args now raises exceptions generated when parsing arguments from request data. This behaviour is generally more useful to developers than an error page with no information about what parameter is missing. Version 13 * Added HttpOnly option to cookies * Session middleware can now have the cookie path and domain overridden Version 12 * Added a path_info argument to request.make_uri * Improved support for dispatcher_app to dispatch to bound methods * Added many missing docstrings and other pylint/cheesecake suggested changes * Extended request.make_uri to take any query argument that wsgiutils.make_query will accept * make_query now supports taking a list of (name, value) tuples Version 11 * Removed dbm and rdbms session managers, these were untested, undocumented and were not recommended for use. * Added fix for parse_querystring when qs is empty * Fixed error on non-UTF8 encoded request body * Added X-Pesto-Exception header to help debugging of webapps * Improved dispatch debug logging * Fixed issue when using dispatcher predicates and caching * Reversed dispatcher function matching precedence: the first matching function wins * Remove pesto.wsgiutils.MockWSGI and replace with classes in a new pesto.testing module * Added a decorators argument to dispatcher_app.match, allowing handler functions to have decorators which are only applied in the context of web requests * Request.cookies is now a MultiDict object with the same API as Request.form, Request.files and Request.query * Renamed urldispatcher to dispatcher_app to clarify intent of this class. * Made response.Response normalize header case and sort headers on ingress, fixing a few consistency issues. * Adjusted response header handling to be standards compliant on 304 responses and ETag headers * Use wsgiref.validator in MockWSGI * Refactored some parts of request.py and wsgiutils.py into new httputils.py and utils.py modules. * Removed dependency on stdlib cgi.py (see http://mail.python.org/pipermail/web-sig/2009-May/003822.html for reasons). * Changed variables named 'encoding' to 'charset' to make the naming more consistent with HTTP. * Removed support for Python 2.3. From now on pesto is actively tested on 2.5 and 2.6, and I will try to support 2.4 if bugs are reported. Version 10 * Fixes in documentation and packaging, but no changes to functionality Version 9 * Added a predicate argument to dispatcher.match, allowing for extra arbitrary checks before routing a URL. * Add basic ETag support * Removed http exceptions * Added support for wsigorg.routing_args * WSGI apps created with pesto now have execution deferred until the first iteration. Therefore the iterator's .close method can be used to reliably release resources etc. * Fix for Response objects not catching close methods on upstream content iterators * Added onclose kwarg for Response object. Any functions in onclose will be called on the wsgi close event. * Added request.files object to complement request.form and request.query * Added MultiDict object to replace FieldStorageWrapper * Renamed register_pattern to register_converter for consistency * Allowed multiple close functions to be passed to ClosingIterator * request.make_uri now uri quotes paths * Rewrote urldispatcher to use a much more flexible URL matching syntax * Refactored Request initialisation to ensure that multiple calls don't attempt to reinitialize request parameters * added Response.getheaders, Response.getheader and Response.from_wsgi * add an ExtensiblePattern to dispatcher and enable it by default * Changed dispatcher to be a WSGI callable * Added wsgiutils.ClosingIterator and wsgiutils.overlay * Pesto response objects are now valid WSGI apps * Ensure the same Request instance is returned when called multiple times on the same environ object * Added a function for mapping request form parameters to function arguments * Updated documentation: now requires sphinx for building docs * Renamed WSGIRequest to Request for consistency with Response * Changed 'despatcher' to 'dispatcher' throughout, after it was pointed out that no one spells it this way any more Version 8 * Merged pesto.utils with pesto.wsgiutils * Refactored builtin static server to allow it to be used to serve ad-hoc single files * Added a make_query utility function to wsgiutils * Fixed request.make_uri when presented with a relative URI. expanded the docstring and tests. * Static server fixes for windows environments Version 7 * Reimplemented session saving machinery using the WSGI standard close() mechanism. * Request.FieldStorageWrapper now raises a KeyError on non-existant item access Version 6 * Added uri_join and fixed request.make_uri path concatenation logic * Moved debug param out of despatch class and into despatcher_app, where it can be applied across all despatchers in operation * Removed duplicate default charset/encoding setting * Added Response.bad_request() classmethod * Added Response.buffered method * Added Response.add_cookie Version 5 * Changed wsgiutils.make_absolute_url to bring into line with PEP 333 and improve handling of relative URIs * Added Response.redirect, Response.not_found etc classmethods, replacing previous freestanding functions * make_query now allows a separator character to be specified * pesto.currentrequest() returns None when no request available * Add WSGIRequest.query to model querystring data. Both WSGIRequest.form and .query are now properties and lazily instantiated. Version 4 * First publicly released version! Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Software Development :: Libraries :: Python Modules pesto-25/FAQ.txt0000644000175100017510000000001011613355542014424 0ustar oliveroliver00000000000000FAQ --- pesto-25/setup.cfg0000644000175100017510000000007311613356014015101 0ustar oliveroliver00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pesto-25/CHANGELOG.txt0000644000175100017510000002051611613355542015321 0ustar oliveroliver00000000000000Version 25 * Fixed bug in pesto.utils.MultiDict and added a MultiDict.extend method Version 24 * Bugfixes for pesto.utils.MultiDict and pesto.dispatch * pesto.utils.with_request_args decorator handles malformed input more gracefully Version 23 * Fix for exc_info handling Version 22 * Partial Python 2.4 backwards compatibility (thanks to Brian Peiris). * Improvements to the handling of close methods on content iterators * Other minor bugfixes Version 21 * HEAD requests are passed to the GET handler if no explicit HEAD handler is set up. * DispatcherApp can now take a ``prefix`` argument that is prepended to all match patterns * Now uses the repoze.lru LRU cache implementation. Version 20 * Fixed RuntimeError when used under mod_wsgi (thanks to Mateusz Korniak) * Fixed error parsing information out of user-agent cookie headers (thanks to Mateusz Korniak) Version 19 * pesto.wsgiutils.serve_static_file now correctly sets the Last-Modified header (thanks to Samuel Wan) * Better compliance with HTTP RFC and WSGI spec on 204 No Content and 304 Not Modified responses * Restored Python 2.5 compatibility Version 18 * PestoWSGIApplication/to_wsgi may now be used to decorate class methods as well as regular functions * Minor bugfixes Version 17 * pesto.util.MultiDict now retains insertion order of keys. This is useful for form processing where you want to know the order in which fields are submitted. * Fixed bugs relating to path handling functions stripping trailing slashes eg in ``pesto.wsgiutils.make_absolute_url``. Also ``pesto.dispatch.dispatcher_app`` no longer strips trailing slashes either, it left to the application to do this if desired. Version 16 * Fixed error introduced into session handling, where the session cookie was not resent if an old session disappeared Version 15 * Corrected package version number * Included fixes for session handling (thanks to Ferry Pierre). Version 14 * FileSessionManager now saves files under dedicated _pesto_sessions subdirectory, making it safer to use when initialized on a shared temporary directory. * Corrected pesto.wsgiutils.mount_app, which previously passed a reference to an out of date request object to sub apps * Altered the url() function acquired by dispatcher_app decorated functions to allow script_name, netloc and URL scheme to be specified, making it possible for multiple Pesto dispatcher_apps mounted at different paths or network locations to generate valid URLs for each other. * pesto.wsgiutils.with_request_args now raises error objects that may be more easily separated out from other errors in middleware layers if required. * Session.save() now forces a save even if no changes have been detected (which may happen if you store mutable objects in the session) * pesto.wsgiutils.with_request_args now raises exceptions generated when parsing arguments from request data. This behaviour is generally more useful to developers than an error page with no information about what parameter is missing. Version 13 * Added HttpOnly option to cookies * Session middleware can now have the cookie path and domain overridden Version 12 * Added a path_info argument to request.make_uri * Improved support for dispatcher_app to dispatch to bound methods * Added many missing docstrings and other pylint/cheesecake suggested changes * Extended request.make_uri to take any query argument that wsgiutils.make_query will accept * make_query now supports taking a list of (name, value) tuples Version 11 * Removed dbm and rdbms session managers, these were untested, undocumented and were not recommended for use. * Added fix for parse_querystring when qs is empty * Fixed error on non-UTF8 encoded request body * Added X-Pesto-Exception header to help debugging of webapps * Improved dispatch debug logging * Fixed issue when using dispatcher predicates and caching * Reversed dispatcher function matching precedence: the first matching function wins * Remove pesto.wsgiutils.MockWSGI and replace with classes in a new pesto.testing module * Added a decorators argument to dispatcher_app.match, allowing handler functions to have decorators which are only applied in the context of web requests * Request.cookies is now a MultiDict object with the same API as Request.form, Request.files and Request.query * Renamed urldispatcher to dispatcher_app to clarify intent of this class. * Made response.Response normalize header case and sort headers on ingress, fixing a few consistency issues. * Adjusted response header handling to be standards compliant on 304 responses and ETag headers * Use wsgiref.validator in MockWSGI * Refactored some parts of request.py and wsgiutils.py into new httputils.py and utils.py modules. * Removed dependency on stdlib cgi.py (see http://mail.python.org/pipermail/web-sig/2009-May/003822.html for reasons). * Changed variables named 'encoding' to 'charset' to make the naming more consistent with HTTP. * Removed support for Python 2.3. From now on pesto is actively tested on 2.5 and 2.6, and I will try to support 2.4 if bugs are reported. Version 10 * Fixes in documentation and packaging, but no changes to functionality Version 9 * Added a predicate argument to dispatcher.match, allowing for extra arbitrary checks before routing a URL. * Add basic ETag support * Removed http exceptions * Added support for wsigorg.routing_args * WSGI apps created with pesto now have execution deferred until the first iteration. Therefore the iterator's .close method can be used to reliably release resources etc. * Fix for Response objects not catching close methods on upstream content iterators * Added onclose kwarg for Response object. Any functions in onclose will be called on the wsgi close event. * Added request.files object to complement request.form and request.query * Added MultiDict object to replace FieldStorageWrapper * Renamed register_pattern to register_converter for consistency * Allowed multiple close functions to be passed to ClosingIterator * request.make_uri now uri quotes paths * Rewrote urldispatcher to use a much more flexible URL matching syntax * Refactored Request initialisation to ensure that multiple calls don't attempt to reinitialize request parameters * added Response.getheaders, Response.getheader and Response.from_wsgi * add an ExtensiblePattern to dispatcher and enable it by default * Changed dispatcher to be a WSGI callable * Added wsgiutils.ClosingIterator and wsgiutils.overlay * Pesto response objects are now valid WSGI apps * Ensure the same Request instance is returned when called multiple times on the same environ object * Added a function for mapping request form parameters to function arguments * Updated documentation: now requires sphinx for building docs * Renamed WSGIRequest to Request for consistency with Response * Changed 'despatcher' to 'dispatcher' throughout, after it was pointed out that no one spells it this way any more Version 8 * Merged pesto.utils with pesto.wsgiutils * Refactored builtin static server to allow it to be used to serve ad-hoc single files * Added a make_query utility function to wsgiutils * Fixed request.make_uri when presented with a relative URI. expanded the docstring and tests. * Static server fixes for windows environments Version 7 * Reimplemented session saving machinery using the WSGI standard close() mechanism. * Request.FieldStorageWrapper now raises a KeyError on non-existant item access Version 6 * Added uri_join and fixed request.make_uri path concatenation logic * Moved debug param out of despatch class and into despatcher_app, where it can be applied across all despatchers in operation * Removed duplicate default charset/encoding setting * Added Response.bad_request() classmethod * Added Response.buffered method * Added Response.add_cookie Version 5 * Changed wsgiutils.make_absolute_url to bring into line with PEP 333 and improve handling of relative URIs * Added Response.redirect, Response.not_found etc classmethods, replacing previous freestanding functions * make_query now allows a separator character to be specified * pesto.currentrequest() returns None when no request available * Add WSGIRequest.query to model querystring data. Both WSGIRequest.form and .query are now properties and lazily instantiated. Version 4 * First publicly released version! pesto-25/INSTALL.txt0000644000175100017510000000013611613355542015134 0ustar oliveroliver00000000000000Installation ------------ Please see doc/getting_started.rst for installation instructions. pesto-25/AUTHORS.txt0000644000175100017510000000004211613355542015147 0ustar oliveroliver00000000000000Oliver Cope pesto-25/README.txt0000644000175100017510000000243111613355542014763 0ustar oliveroliver00000000000000 Pesto is a library for Python web applications. Its aim is to make writing WSGI web applications easy and fun. Pesto doesn't constrain you -- how you integrate with databases, what templating system you use or how you prefer to lay out your source files is up to you. Above all, pesto is small, well documented and well tested. Pesto makes it easy to: - Map any URI to any part of your application. - Produce unicode aware, standards compliant WSGI applications. - Interrogate WSGI request information -- form variables and HTTP request headers. - Create and manipulate HTTP headers, redirects, cookies etc. - Integrate with any other WSGI application or middleware, giving you access to a vast and growing resource. Development status ------------------ Pesto is production ready and used on a wide variety of websites. To browse or check out the latest development version, visit http://patch-tag.com/repo/pesto. For documentation, visit http://pesto.redgecko.org/. Licence -------- Pesto is available under the terms of the `new BSD licence `_. Documentation -------------- Full documentation is included with each release. Documentation for the latest version is available at . pesto-25/VERSION.txt0000644000175100017510000000000311613355771015150 0ustar oliveroliver0000000000000025 pesto-25/TODO.txt0000644000175100017510000000001211613355542014564 0ustar oliveroliver00000000000000TODO ---- pesto-25/NEWS.txt0000644000175100017510000000001311613355542014574 0ustar oliveroliver00000000000000NEWS ---- pesto-25/doc/0000755000175100017510000000000011613356014014025 5ustar oliveroliver00000000000000pesto-25/doc/Makefile0000644000175100017510000001076111613355542015477 0ustar oliveroliver00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = ../bin/sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pesto.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pesto.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pesto" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pesto" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pesto-25/doc/conf.py0000644000175100017510000001705211613355542015336 0ustar oliveroliver00000000000000# -*- coding: utf-8 -*- # # Pesto documentation build configuration file, created by # sphinx-quickstart on Fri Jan 16 14:13:09 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pesto' copyright = u'2009-2011 Oliver Cope' # Display docstrings from class AND __init__ method, concatenated autoclass_content = 'both' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. with open(os.path.join(os.path.dirname(__file__), '..', 'VERSION.txt'), 'r') as f: version = f.read().strip() # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'default' html_theme = 'customtheme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['.'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None html_title = "Pesto: a library for WSGI applications" # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Pestodoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Pesto.tex', u'Pesto Documentation', ur'Oliver Cope', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pesto', u'Pesto Documentation', [u'Oliver Cope'], 1) ] html_theme_options = dict( bodyfont = 'Lato,sans-serif', headfont = 'Lato,sans-serif', rightsidebar = True, headbgcolor='none', codebgcolor = 'none', ) # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} pesto-25/doc/session.rst0000644000175100017510000001534711613355542016261 0ustar oliveroliver00000000000000Session storage =============== Sessioning provides a persistant storage for arbitrary data between user requests. Typically the user is sent a cookie containing a randomly generated identification string in the response to the first request to the WSGI application. This session cookie is used to identify the browser session and Pesto makes a data store available in the variable ``request.session`` that is persistent between requests in the same session. A middleware layer must be added to the application that deals with setting and checking the session cookie. As an alternative to using cookies the session identfier may also be passed around in the querystring portion of the URL, however this requires special care when writing the application to include the session identifier in every URL generated by your application. .. testsetup:: * from pesto.session.memorysessionmanager import MemorySessionManager from pesto.testing import TestApp import pesto from pesto import Response @pesto.to_wsgi def app(request): return Response(["Whoa Nelly!"]) original_app = app def FakeFileSessionManager(path): return MemorySessionManager() Here's an example application using sessions: .. testcode:: from pesto import DispatcherApp app = DispatcherApp() @app.match('/login', 'POST') def login(request): username = request.get('username') password = request.get('password') if is_valid(username, password): request.session['username'] = username request.session['logged_in'] = True @app.match('/secure-area', 'GET') def secure_area(request): if not request.session.get('logged_in'): return response.redirect(login.url()) return ["Welcome to the secure area, %s" % request.session['username']] from pesto.session.filesessionmanager import FileSessionManager # Create a file based sessioning middleware, that runs a purge every 600s # for sessions older than 1800s.. sessioning = pesto.session_middleware( FileSessionManager('/path/to/session/store'), auto_purge_every=600, auto_purge_olderthan=1800 ) app = sessioning(app) Sessioning needs some kind of data storage backend. The two backends available in Pesto are memory and file backed storage. File backend ------------ Synopsis: .. testcode:: from pesto.session.filesessionmanager import FileSessionManager app = pesto.session_middleware(FileSessionManager('/path/to/session/store'))(app) This backend is the most generally useful storage backend. Session data is stored in one file per session, using Python's ``pickle`` module to store and retrieve data from disk. The application must have access to a writable directory, which for security reasons should not be readable by other users. Memory backend ---------------- Synopsis: .. testcode:: from pesto.session.memorysessionmanager import MemorySessionManager app = pesto.session_middleware(MemorySessionManager())(app) This implementation stores data directly in a python data structure stored in memory. Session data can't be shared between processes and is lost when the process finishes, so it is not useful for CGI or architectures that do not use long-running processes or architectures that split requests between multiple processes. In general it is recommended to use the file backend which does not suffer from these problems. Preserving state between requests ==================================== There are two built in methods for associating a session with a user request, using either HTTP cookies or the URI querystring. Cookie persistence ------------------ This is the default method and is usually preferred as it is transparent both to users and the application developer. However not all browsers accept cookies and cookies cannot easily be passed between applications running on different domains, for these reasons you may still prefer the querystring based approach. Cookie based persistence is the default method so no extra code is required to enable this: .. testcode:: app = pesto.session_middleware(FileSessionManager('/path/to/session/store'))(app) Setting the cookie path and domain ``````````````````````````````````` When using cookie based persistence cookies are by default tied to the path and domain on which the application is running. To override this, you can specify the cookie path and domain when constructing the session middleware: .. testcode:: :hide: app = original_app FileSessionManager = FakeFileSessionManager .. testcode:: app = pesto.session_middleware( FileSessionManager('/path/to/session/store'), cookie_path='/', cookie_domain='.example.org', )(app) .. testcode:: :hide: response = TestApp(app).get('/foo', SCRIPT_NAME='/foo') print response.get_headers('Set-Cookie') .. testoutput:: :hide: ...Domain=.example.org;Path=/... If you are using the ``filesessionmanager`` this could be used to share session state between WSGI applications running on the same server mounted on different subdomains or paths. Querystring persistence ------------------------ To set up a pesto application with querystring based persistence: .. testcode:: :hide: app = original_app FileSessionManager = FakeFileSessionManager .. testcode:: app = pesto.session_middleware( FileSessionManager('/path/to/session/store'), persist='querystring' )(app) .. testcode:: :hide: response = TestApp(app).get('/foo', SCRIPT_NAME='/foo') print response.get_headers('Location') .. testoutput:: :hide: .../foo?pesto_session=... When querystrings are used it is necessary for all links generated by the application to contain a reference to the session id. Thus code to generate a link would typically look like this (this example uses the `Genshi `_ page template syntax):: ... Pesto doesn't contain any code to support rewriting links output by your application, it is up to you to ensure the querystring is always present in links. Any ``GET`` requests that do not contain a session id will be redirected by the session middleware to a URL containing a fresh session identifier if one is not already present. pesto.session API documention ------------------------------- .. automodule:: pesto.session.base :members: .. automodule:: pesto.session.memorysessionmanager :members: .. automodule:: pesto.session.filesessionmanager :members: pesto-25/doc/index.rst0000644000175100017510000001024311613355542015673 0ustar oliveroliver00000000000000Pesto ====== .. testsetup:: * from pesto.testing import TestApp Introduction ------------ Pesto is a library for Python web applications. Its aim is to make writing WSGI web applications easy and fun. Pesto isn't a framework – how you integrate with databases, what templating system you use or how you prefer to organize your source files is up to you. Above all, Pesto aims to be small, well documented and well tested. Pesto makes it easy to: - Map any URI to any part of your application. - Produce unicode aware, standards compliant WSGI applications. - Interrogate WSGI request information – form variables and HTTP request headers. - Create and manipulate HTTP headers, redirects, cookies etc. - Integrate with any other WSGI application or middleware, giving you access to a vast and growing resource. Contents: .. toctree:: :maxdepth: 2 getting_started request response dispatch cookies session httputils wsgiutils utils caching Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` Examples --------- A very basic web application to demonstrate handling a request and creating a response: .. testcode:: from pesto import to_wsgi, Response def handler(request): return Response([ "", "

Whoa Nelly!

", "", ]) if __name__ == "__main__": from wsgiref import simple_server httpd = simple_server.make_server('', 8080, to_wsgi(handler)) Pesto handler functions typically take a ``Request`` object as an argument and should return a ``Response`` object. A longer example using the ``DispatcherApp`` class to map URLs to handlers: .. testcode:: from pesto import Response from pesto.dispatch import DispatcherApp app = DispatcherApp() recipes = { 'pesto': "Blend garlic, oil, parmesan and pine nuts.", 'toast': "Put bread in toaster. Toast it." } @app.match('/', 'GET') def recipe_index(request): """ Display an index of available recipes. """ markup = ['

List of recipes

    '] for recipe in sorted(recipes): markup.append( '
  • %s
  • ' % ( show_recipe.url(recipe=recipe), recipe ) ) markup.append('
') return Response(markup) @app.match('/recipes/', 'GET') def show_recipe(request, recipe): """ Display a single recipe """ if recipe not in recipes: return Response.not_found() return Response([ '

How to make %s

' % recipe, '

%s

Back to index' % (recipes[recipe], recipe_index.url()), '' ]) if __name__ == "__main__": from wsgiref import simple_server httpd = simple_server.make_server('', 8080, app) httpd.serve_forever() .. doctest:: :hide: >>> app = TestApp(app) >>> print app.get('/').body

List of recipes

>>> print app.get('/recipes/toast').body

How to make toast

Put bread in toaster. Toast it.

Back to index >>> print app.get('/recipes/cheese').status 404 Not Found Development status ------------------ Pesto is production ready and used on a wide variety of websites. To browse or check out the latest development version, visit http://patch-tag.com/r/oliver/pesto. For documentation, visit http://pesto.redgecko.org/. Mailing list ------------ A google groups mailing list is online at http://groups.google.com/group/python-pesto. Please use this for any questions or comments relating to Pesto. Licence -------- Pesto is available under the terms of the `new BSD licence `_. pesto-25/doc/getting_started.rst0000644000175100017510000005541511613355542017765 0ustar oliveroliver00000000000000.. testsetup:: * from pesto.response import Response from pesto import to_wsgi from pesto.testing import TestApp Getting started with Pesto ########################## .. contents:: Contents Introduction ============= This guide covers: - Downloading and installing the Pesto library - Running applications with an existing web server or a standalone WSGI server. - Integrating applications with a templating system Installation ============= Pesto requires Python 2.5, 2.6 or 2.7 (recommended). You will need a webserver to run the examples. There are examples in this guide for integrating with a CGI webserver (eg Apache), or using a WSGI server which can either run standalone or be integrated with a front end server such as Apache. Installing Pesto ``````````````````` Download and install the latest version from the Python Package Index:: % pip install pesto Installing the development version ```````````````````````````````````` For the cutting edge, fetch the development version of Pesto from its darcs repository:: % darcs get http://patch-tag.com/r/pesto/pullrepo pesto % cd pesto % sudo python setup.py install Programming with Pesto: basic concepts ====================================== Pesto provides: - A request object that gives you easy access to information about the request, eg the URL and any submitted form data. - A response class that makes it easy to construct and modify HTTP responses. - A URI dispatch mechanism to help you map URIs to handler functions. - Functions that convert your handler functions to WSGI functions, and back again. Sample application ``````````````````` Here's a simple web application giving an overview of the functions provided by Pesto. Save this code in a file called ``guestbook.py``: .. testcode:: #!/usr/bin/python from cgi import escape from datetime import datetime from pesto import Response, DispatcherApp app = DispatcherApp() entries = [] @app.match('/', 'GET') def guestbook(request): """ Display an index of all entries """ content = [ '

Welcome to my Guestbook

' '
' 'Name:
' 'Your message:
' '' '
' % add_entry.url() ] content.extend([ '

From %s @ %s

%s

view details' % ( escape(entry['name']), entry['date'].strftime('%d/%m/%Y %H:%M'), escape(entry['message']), view_entry.url(index=ix), ) for ix, entry in reversed(list(enumerate(entries))) ]) return Response(content) @app.match('/add-entry', 'POST') def add_entry(request): """ Add an entry to the guestbook then redirect back to the main guestbook page. """ entries.append({ 'date': datetime.now(), 'name': request.form.get('name', ''), 'message': request.form.get('message', ''), 'ip': request.remote_addr, 'useragent': request.get_header('User-Agent', ''), }) return Response.redirect(guestbook.url()) @app.match('/view-entry/', 'GET') def view_entry(request, index): """ View all details of an individual entry """ try: entry = entries[index] except IndexError: return Response.not_found() return Response(["""
Name%s
Time%s
IP address%s
Browser%s
Message%s
Back""" % ( escape(entry['name']), entry['date'].strftime('%d %m %Y %H:%M'), escape(entry['ip']), escape(entry['useragent']), escape(entry['message']), guestbook.url() ) ]) if __name__ == "__main__": from wsgiref import simple_server httpd = simple_server.make_server('', 8080, app) httpd.serve_forever() .. doctest:: :hide: >>> app = TestApp(app) >>> '

Welcome to my Guestbook

' in app.get('/').body True >>> print app.post('/add-entry', data={'name': 'Jim', 'message': "hello, I'm Jim & I like guestbooks"}).text() 302 Found ... Location: http://localhost/ ... >>> "hello, I'm Jim & I like guestbooks" in app.get('/').body True >>> print app.get('/view-entry/2').text() 404 Not Found ... >>> print app.get('/view-entry/0').text() 200 OK ... NameJim Time... IP address127.0.0.1 Browser Messagehello, I'm Jim & I like guestbooks ... Run the file by typing ``python guestbook.py`` and a web server should start on port 8080. Here's a line-by-line breakdown of the important functionality illustrated here: -------- :: app = DispatcherApp() ``DispatcherApp`` is a WSGI application that takes incoming requests and routes them to handler functions. In their simplest form, handler functions take a single argument (a ``pesto.request.Request`` object) and must return a ``pesto.response.Response`` object. -------- :: @app.match('/', 'GET') def guestbook(request): ... @app.match('/add-entry', 'POST') def add_entry(request): ... Using ``@app.match`` is the most convenient way to match URIs to handler functions. You need to specify both the path and at least one HTTP method (usually ``GET`` or ``POST``). In this case, the function ``guestbook`` will be called for all GET requests to ``http:///``, while ``add_entry`` will be called for POST requests to ``http://

Welcome to my Guestbook

' ... ] ... return Response(content) The ``Response`` object returned by a handler function specifies the response body and any HTTP headers. ``Response`` requires one argument, which must be an iterator over the content you want to return. Other arguments can be used to specify HTTP headers and other aspects of the response. If you don't tell Pesto otherwise it will assume that the HTTP status should be ``200 OK`` and that the content type should be ``text/html; charset=UTF-8``. -------- :: return Response.redirect(guestbook.url()) ``Response.redirect`` is a method that returns a 302 redirect to the web browser to any given URL. Because the ``guestbook`` has been mapped to a URL via the ``DispatcherApp`` class, we can call ``guestbook.url()`` to retrieve the fully qualified URL pointing to that function. -------- :: @app.match('/view-entry/', 'GET') def view_entry(request, index): """ View all details of an individual entry """ Again we are using a ``DispatcherApp`` to map a URI to a function. Here we want to extract the second part of the URI and pass it as the named argument ``index`` to the function. We also tell Pesto that it should convert the value to an integer. Other pattern types are supported, like ```` or ````. You can also define your own pattern matching rules. -------- :: try: entry = entries[index] except IndexError: return Response.not_found() The ``Response`` class contains predefined functions for most error responses. Returning ``Response.not_found`` will automatically return a 404 response to the web browser. Using with CGI ============================== If you have access to a web server that is already configured to run CGI scripts and then this is a quick way to get started with Pesto. However it is more limited that other methods and can give poor performance. Let's start by creating a CGI script as follows: .. testcode:: #!/usr/bin/env python import pesto from pesto import Response def handler(request): return Response(["Welcome to Pesto!"]) if __name__ == "__main__": app = pesto.to_wsgi(handler) pesto.run_with_cgi(app) Save this file in your web server's cgi-bin directory with the filename ``pesto_test.cgi`` Visit the script with a web browser and if all is well you should see the "Welcome to Pesto!" message. If you don't see this message, check that the file permissions are set correctly (ie ``chmod 755 pesto_test.cgi``). You may also need to change the first line of your script to read either ``#!/usr/bin/python`` or ``#!/usr/local/bin/python``, depending on your hosting provider. CGI with mod_rewrite ``````````````````````````` If you are using Apache and mod_rewrite is enabled, then using a ``RewriteRule`` in your server configuration or from a ``.htaccess`` file is an easy way of running CGI scripts that gives you user friendly URIs and the possibility of having more than one handler function per script. Here is how to set up a script that responds to the URIs ``/pages/one`` and ``/pages/two``. **.htaccess** :: RewriteEngine On RewriteBase / RewriteRule ^(pages/.*) cgi-bin/pesto_test.cgi/$1 **cgi-bin/pesto_test.cgi** .. testcode:: #!/usr/bin/python import pesto.wsgiutils from pesto import DispatcherApp, Response app = pesto.DispatcherApp() @app.match('/page/one', 'GET') def render_page(request, response): return Response(["This is page one"]) @app.match('/page/two', 'GET') def render_page(request): return Response(["This is page two"]) if __name__ == "__main__": app = pesto.wsgiutils.use_redirect_url()(app) pesto.run_with_cgi(app) The first time you try this, you might want to enable debugging in the dispatcher to log details of the URL processing. To do this, change the first 7 lines of your script to the following: :: #!/usr/bin/python import logging logging.getLogger().setLevel(logging.DEBUG) import pesto import pesto.wsgiutils from pesto import Response app = pesto.DispatcherApp(debug=True) Using a standalone WSGI server =============================================================== You can run a Pesto based web application under any WSGI server. If you have Python 2.5 or above, you can use the `wsgiref module `_ from the Python standard library. First, create a file called ``myhandlers.py``: .. testcode:: from pesto import DispatcherApp import pesto.wsgiutils app = DispatcherApp() @app.match('/page/one', 'GET') def page_one(request): return Response([ 'This is page one. Click here for page two...' % (page_two.url(),) ]) @app.match('/page/two', 'GET') def page_two(request): return Response([ '...and this is page two. Click here for page one' % (page_one.url(),) ]) .. doctest:: :hide: >>> TestApp(app).get('/page/one').body 'This is page one. Click here for page two...' >>> TestApp(app).get('/page/two').body '...and this is page two. Click here for page one' And a file called ``myapp.py``: :: import myhandlers if __name__ == "__main__": print "Serving application on port 8000..." httpd = make_server('', 8080, app) httpd.serve_forever() Now you can start the server by running myapp.py directly:: % python myapp.py Serving application on port 8080... Visit http://localhost:8080/page/one in your web browser and see the application in action. Virtualhosting and Apache ========================== Using a standalone webserver has many advantages. But it's better if you can proxy it through another web server such as Apache. This gives added flexibility and security and if necessary, you can set up proxy caching to get a big performance boost. **For the following to work, you need to make sure your apache installation has the proxy and rewrite modules loaded.** Refer to the `Apache HTTP server documentation `_ for details of how to achieve this. Let's assume that you want to run a site at the URL http://example.com/. For this configuration we need Apache to listen on port 80, and the WSGI server on any other port – we'll use port 8080 in this example. In your httpd.conf, set up the following directives:: RewriteEngine On RewriteRule ^/(.*)$ http://localhost:8080/$1 [L,P] ProxyVia On The first ``RewriteRule`` simply proxies everything to the WSGI server. Restart apache and visit http://localhost/page/one - you should see a ``Bad Gateway`` error page. Don't panic – this means that the proxying is working in apache, but your application is not running yet. Modify ``myapp.py`` to read as follows: :: import myhandlers import pesto.wsgiutils def make_app(): app = myhandlers.app app = pesto.wsgiutils.use_x_forwarded()(app) return app if __name__ == "__main__": print "Serving application on port 8000..." httpd = make_server('', 8080, make_app()) httpd.serve_forever() To see it in action, fire up the server:: % python myapp.py Serving application on port 8080... and reload http://localhost/page/one in your browser: you should now see your pesto application being server through Apache. For a more sophisticated setup suitable for production applications, you should investigate the `Paste `_ package. HTTPS ``````` For URI generation to work correctly when proxying from an Apache/mod_ssl server, you will need to add the following to the Apache configuration in the SSL `` section:: RequestHeader set X_FORWARDED_SSL 'ON' Pesto handlers ``````````````````` Pesto handlers are at the heart of the Pesto library. The basic signature of a handler is: .. testcode:: def my_handler(request): return Response(["

Whoa Nelly!

"]) Handlers must accept a request object and must return a ``pesto.response.Response`` object. The ``Response`` constructor takes at least one argument, ``content``, which must be an iterator over the content you want to return. In the example above the payload is HTML, but any data can be returned. For example, the following are also examples of valid Response objects: .. testcode:: :hide: cursor = None .. testcode:: # Simple textual response Response(['ok'], content_type="text/plain") # Iterator over database query def format_results(cursor): yield "" for row in iter(cursor.fetchone, None): yield '' for column in row: yield '' % column yield '' yield "
%d
" Response(format_results(cursor)) Function decorators ``````````````````` Function decorators are simple, expressive and a natural way to add functionality to web applications using Pesto. Here are a few examples. First up, a decorator to set caching headers on the response: .. testcode:: from functools import wraps def nocache(func): """ Pesto middleware to send no-cache headers. """ @wraps(func) def nocache(request, *args, **kwargs): res = func(request, *args, **kwargs) res = res.add_header("Cache-Control", "no-cache, no-store, must-revalidate") res = res.add_header("Expires", "Mon, 26 Jul 1997 05:00:00 GMT") return res return nocache This could be used as follows: .. testcode:: @nocache def handler(request): return Response(['blah']) .. testcode:: :hide: from pesto.testing import TestApp print TestApp(to_wsgi(handler)).get('/').text() Giving the following output: .. testoutput:: 200 OK Cache-Control: no-cache, no-store, must-revalidate Content-Type: text/html; charset=UTF-8 Expires: Mon, 26 Jul 1997 05:00:00 GMT blah Second: a decorator to allow handlers to return datastructures which are automatically converted into JSON notation (this example requires python 2.6, for earlier versions you will need to install the SimpleJSON module installed): .. testcode:: import json def to_json(func): """ Wrap a Pesto handler to return a JSON-encoded string from a python data structure. """ @wraps(func) def to_json(request, *args, **kwargs): result = func(request, *args, **kwargs) if isinstance(result, Response): return result return Response( content=[json.dumps(result)], content_type='application/json' ) return to_json Finally, a decorator to turn 'water' into 'wine': .. testcode:: def water2wine(func): @wraps(func) def water2wine(*args, **kwargs): res = func(*args, **kwargs) return res.replace( content=(chunk.replace('water', 'wine') for chunk in res.content) ) return water2wine Decorators may be chained together, for example: .. testcode:: from pesto import DispatcherApp app = DispatcherApp() @app.match('/drink-preference', 'GET') @water2wine @nocache @to_json def handler(request): return {'preferred-drink': 'water' } .. testcode:: :hide: from pesto.testing import TestApp print TestApp(app).get('/drink-preference').text() This would output the following JSON response: .. testoutput:: 200 OK Cache-Control: no-cache, no-store, must-revalidate Content-Type: application/json Expires: Mon, 26 Jul 1997 05:00:00 GMT {"preferred-drink": "wine"} Running Pesto applications ````````````````````````````````````` Pesto and WSGI ''''''''''''''' The ``to_wsgi`` utility function adapts a Pesto handler function to form a WSGI application. This can then be run by any WSGI compliant server, eg `wsgiref.simple_server `_:: from wsgiref.simpleserver import make_server app = pesto.to_wsgi(my_handler) httpd = make_server('', 8000, app) print "Serving on port 8000..." httpd.serve_forever() Or in a CGI environment (eg for shared hosting) by using ``pesto.run_with_cgi``:: app = pesto.to_wsgi(my_handler) pesto.run_with_cgi(app) Pesto ``DispatcherApp`` instances are WSGI applications and can be passed directly to ``pesto.run_with_cgi``. Using Pesto with a templating system ===================================== Pesto does not tie you to any particular templating system. As an example of how you can use a templating system in your application, here is a minimal example of code that uses the `Genshi `_ templating library: .. testcode:: import os from functools import wraps from genshi.template.loader import TemplateLoader from pesto import Response, DispatcherApp templateloader = TemplateLoader(["."]) def render(filepath): """ Render a template in genshi, passing any keyword arguments to the template namespace. """ def decorator(func): @wraps(func) def decorated(request, *args, **kwargs): template = templateloader.load(filepath) data = func(request, *args, **kwargs) return Response([ template.generate( **data ).render(method='xhtml', encoding='utf8') ]) return decorated return decorator app = DispatcherApp(debug=True) @app.match("/", "GET") @render("welcome.html") def welcome(request, name): return {'name': name.title()} if __name__ == "__main__": from wsgiref.simpleserver import make_server print "Serving application on port 8080..." httpd = make_server('', 8080, app) httpd.serve_forever() To make this work, we'll need a template file: .. doctest:: >>> f = open('welcome.html', 'w') >>> f.write(''' ... ... ...

Greetings, $name!

... ... ... ''') >>> f.close() .. testcode:: :hide: print TestApp(app).get('/fred').body Once running, a call to ``http://localhost:8080/fred`` should give you the following result: .. testoutput::

Greetings, Fred!

pesto-25/doc/utils.rst0000644000175100017510000000024611613355542015726 0ustar oliveroliver00000000000000 pesto.utils API documention ================================== .. testsetup:: * from pesto.utils import * .. automodule:: pesto.utils :members: pesto-25/doc/httputils.rst0000644000175100017510000000016611613355542016627 0ustar oliveroliver00000000000000 pesto.httputils API documention ------------------------------- .. automodule:: pesto.httputils :members: pesto-25/doc/response.rst0000644000175100017510000001621311613355542016425 0ustar oliveroliver00000000000000 The response object ===================== .. testsetup:: * from pesto.response import Response from pesto.testing import TestApp, make_environ from pesto import to_wsgi The response object allows you to set headers and provides shortcuts for common handler responses, such as redirection. Constructing response objects ``````````````````````````````` At the minimum the ``pesto.response.Response`` constructor needs the response content. If this is all you specify, a successful response of type ``text/html`` will be generated with your specified content. .. testcode:: def app(request): return Response(['

Hello World']) .. testcode:: :hide: print TestApp(to_wsgi(app)).get().text() This will output: .. testoutput:: 200 OK Content-Type: text/html; charset=UTF-8

Hello World The content argument must be an iterable object – eg a list, a generator expression or any python object that implements the iteration interface) HTTP headers and a status code can also be supplied. Here's a longer example, showing more options: .. testcode:: from pesto.response import Response def app(request): return Response( status=405, # method not allowed content_type='text/html', allow=['GET', 'POST'], content=['Sorry, that method is not allowed'] ) .. testcode:: :hide: print TestApp(to_wsgi(app)).get().text() This will output: .. testoutput:: 405 Method Not Allowed Allow: GET Allow: POST Content-Type: text/html Sorry, that method is not allowed Headers can be supplied as a list of tuples (the same way the WSGI ``start_response`` function expects them), or as keyword arguments, or any mixture of the two: .. testcode:: Response( ['Sorry, that method is not allowed'], status=405, headers=[('Content-Type', 'text/html'), ('Allow', 'GET'), ('Allow', 'POST')], ) Response( ['Sorry, that method is not allowed'], status=405, content_type='text/html', allow=['GET', 'POST'], ) Changing response objects ``````````````````````````` Response objects have a range of methods allowing you to add, remove and replace the headers and content. This makes it easy to chain handler functions together, each operating on the output of the last: .. testcode:: def handler1(request): return Response(["Ten green bottles, hanging on the wall"], content_type='text/plain') def handler2(request): response = handler1(request) return response.replace(content=[chunk.replace('Ten', 'Nine') for chunk in response.content]) def handler3(request): response = handler2(request) return response.replace(content_type='text/html') .. doctest:: >>> from pesto.testing import TestApp >>> print TestApp(to_wsgi(handler1)).get('/').text() 200 OK Content-Type: text/plain Ten green bottles, hanging on the wall >>> print TestApp(to_wsgi(handler2)).get('/').text() 200 OK Content-Type: text/plain Nine green bottles, hanging on the wall >>> print TestApp(to_wsgi(handler3)).get('/').text() 200 OK Content-Type: text/html Nine green bottles, hanging on the wall Headers may be added, either singly: .. doctest:: >>> r = Response(content = ['Whoa nelly!']) >>> r.headers [('Content-Type', 'text/html; charset=UTF-8')] >>> r = r.add_header('Cache-Control', 'private') >>> r.headers [('Cache-Control', 'private'), ('Content-Type', 'text/html; charset=UTF-8')] or in groups: .. doctest:: >>> r = Response(content = ['Whoa nelly!']) >>> r.headers [('Content-Type', 'text/html; charset=UTF-8')] >>> r = r.add_headers([('Content-Length', '11'), ('Cache-Control', 'Private')]) >>> r.headers [('Cache-Control', 'Private'), ('Content-Length', '11'), ('Content-Type', 'text/html; charset=UTF-8')] >>> r = r.add_headers(x_powered_by='pesto') >>> r.headers [('Cache-Control', 'Private'), ('Content-Length', '11'), ('Content-Type', 'text/html; charset=UTF-8'), ('X-Powered-By', 'pesto')] Removing and replacing headers is the same. See the API documentation for `pesto.response.Response` for details. Integrating with WSGI ------------------------ It's often useful to be able to switch between Pesto handler functions and WSGI application functions – for example, when writing WSGI middleware. To aid this, ``Response`` objects are fully compliant WSGI applications:: >>> def mywsgi_app(environ, start_response): ... r = Response(content = ['Whoa nelly!']) ... return r(environ, start_response) ... >>> print TestApp(mywsgi_app).get('/').text() 200 OK Content-Type: text/html; charset=UTF-8 Whoa nelly! Secondly, it is possible to proxy a WSGI application through a response object, capturing its output to allow further inspection and modification:: >>> def basic_wsgi_app(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/html')]) ... return [ "" ... "" ... "

Hello World!

" ... "" ... "" ... ] ... >>> def altered_wsgi_app(environ, start_response): ... response = Response.from_wsgi(wsgi_app1, environ, start_response) ... return response.add_headers(x_powered_by='pesto')(environ, start_response) ... >>> print TestApp(altered_wsgi_app).get('/').text() 200 OK Content-Type: text/html X-Powered-By: pesto

Hello World!

Common responses ----------------- Many canned error responses are available as ``Response`` classmethods: .. doctest:: >>> from pesto.response import Response >>> def handler(request): ... if not somecondition(): ... return Response.not_found() ... return Response(['ok']) ... >>> def handler2(request): ... if not somecondition(): ... return Response.forbidden() ... return Response(['ok']) ... Redirect responses ```````````````````` A temporary or permanent redirect may be achieved by returning ``pesto.response.Response.redirect()``. For example: .. doctest:: >>> def redirect(request): ... return Response.redirect("http://www.example.com/") ... pesto.response API documention ------------------------------- .. automodule:: pesto.response :members: pesto-25/doc/caching.rst0000644000175100017510000002271211613355542016164 0ustar oliveroliver00000000000000 Caching ======= .. testsetup:: * from pesto.testing import TestApp Pesto contains support for setting and parsing cache control headers, including entity tags (ETags). Cache control headers --------------------- The ``pesto.caching.no_cache`` decorator modifies response headers to instruct browsers and caching proxies not to cache a page. See the module API docs (below) for sample usage. ETags ----- The minimum you need to know about ETags `````````````````````````````````````````` ETags are HTTP headers sent by a HTTP server indicating a revision of a document. Typically a web server will supply an ETag along with the content on the first request. Sample HTTP response headers including an etag might look like this:: 200 OK Content-Type: text/html; charset=UTF-8 ETag: "xyzzy" When the browser makes subsequent requests for the same URL, it will send an ``If-None-Match`` header:: GET /index.html HTTP/1.1 If-None-Match: "xyzzy" The server can then compare the ``If-None-Match`` ETag value with the current value. If they match then rather than serving the content again, the server can reply with a ``304 Not Modified`` status, and the browser will display a cached version. Any string can be used as an ETag. When serving static content you could use a concatenate the file's inode, filesize and modification time to generate an ETag – this is what Apache does. Alternatives include a using an incrementing sequence number or a string that represents an object's state in memory. ETags come in two flavours: strong and weak. The above examples show strong ETags. A weak ETag looks much the same, but has a ``W/`` prefix:: ETag: W/"notsostrong" A weak ETag signifies that the content is semantically equivalent, even if the byte-for-byte representation may have changed. The HTTP RFC gives an example of a hit counter image, which does not absolutely need to be refreshed by the client on every request. How to add ETag support ```````````````````````` Let's build a hit counter application. Here's a version without ETag support. You'll need the Python Image library installed to try this example. Also note that this is a very simplified example that would not be suitable for use for a real application: .. testcode:: import Image import ImageDraw from StringIO import StringIO from pesto import to_wsgi from pesto.response import Response class HitCounter(object): current_count = 0 def counter(self, request): self.current_count += 1 img = Image.new('RGB', (50, 30)) draw = ImageDraw.Draw(img) draw.text((10, 10), str(self.current_count)) buf = StringIO() img.save(buf, 'PNG') return Response( [buf.getvalue()], content_type='image/png' ) if __name__ == '__main__': from wsgiref.simple_server import make_server counter = HitCounter() app = to_wsgi(counter.counter) httpd = make_server('', 8000, app) print "Now load http://localhost:8000/ in a web browser" httpd.serve_forever() .. doctest:: :hide: >>> counter = HitCounter() >>> app = to_wsgi(counter.counter) >>> print TestApp(app).get().text().decode('ascii', 'ignore') 200 OK Content-Type: image/png ... Save and run this script, then browse to http://localhost:8000/ and you should see a hit counter. To make this cacheable, we need to add an ETag header to the response. Let's suppose we only want the image to be cached for up to seven hits. We would start off by defining a method that generates an ETag to reflect this: .. testcode:: def hitcounter_etag(self, request): return self.current_count / 7 Then we can use the ``pesto.caching.with_etag`` decorator to apply this to the counter function, and the ``pesto.caching.etag_middleware`` to make the application return a ``304 Not Modified`` response when the ETag matches. I have also put the image generation into a separate function so that it is lazily generated – the image will not be regenerated when the ETag is matched: .. testcode:: import Image import ImageDraw from StringIO import StringIO from pesto import to_wsgi from pesto.response import Response from pesto.caching import with_etag, etag_middleware class HitCounter(object): current_count = 0 def hitcounter_etag(self, request): return self.current_count / 7 @with_etag(hitcounter_etag, weak=True) def counter(self, request): self.current_count += 1 def image(): yield '' img = Image.new('RGB', (50, 30)) draw = ImageDraw.Draw(img) draw.text((10, 10), str(self.current_count)) buf = StringIO() img.save(buf, 'PNG') yield buf.getvalue() return Response(image(), content_type='image/png') if __name__ == '__main__': from wsgiref.simple_server import make_server counter = HitCounter() app = to_wsgi(counter.counter) app = etag_middleware(app) httpd = make_server('', 8000, app) print "Now load http://localhost:8000/ in a web browser" httpd.serve_forever() .. testcode:: :hide: counter = HitCounter() app = to_wsgi(counter.counter) app = etag_middleware(app) print TestApp(app).get().text().decode('ascii', 'ignore') Load this in your browser, and examine the headers using the LiveHTTPHeaders FireFox extension or something similar. You will see an ETag header has been added: .. testoutput:: 200 OK Content-Type: image/png ETag: W/"0" ... Refresh a few times and you should see the server sending a ``304 Not Modified`` response to repeated requests. More on ETag generation ```````````````````````` The function ``pesto.caching.with_etag`` expects to be passed a function which must return an object to be used as an ETag. It then uses the following rules: * If passed an numeric value or short string, it is used as-is. * If passed a long string, an MD5 signature is computed and used as the ETag. * If passed any other object, the object is pickled and the MD5 signature of the pickle used as the ETag. The ``pesto.caching.etag_middleware`` will call the WSGI handler in order to allow it to set the ``ETag`` header function, and then either abort the response and return a ``304 Not Modified`` status or proceed with the response. If the underlying application is a pesto handler, this means the handler will be invoked and the first iteration of the content iterator called (ie as far as the first ``yield`` statement in the ``image`` function above), before the content iterator is closed. For best performance a good pattern for handlers is: * return an generator function to generate the response lazily * start that function with ``yield ''``. For example this is bad: ``very_expensive_calculation`` will be called every time, even on ETag matches: .. testcode:: from pesto.caching import with_etag, etag_middleware from pesto.response import Response from pesto import to_wsgi def very_expensive_calculation(): print "Calculating!" return "The answer is 42" @to_wsgi @with_etag(lambda request: 'foo') def my_handler(request): return Response([very_expensive_calculation()]) app = etag_middleware(my_handler) We can see that ``very_expensive_calculation`` is called every time by calling this in a test rig: .. doctest:: >>> from pesto.testing import TestApp >>> print TestApp(app).get('/').text() Calculating! 200 OK Content-Type: text/html; charset=UTF-8 ETag: "foo" The answer is 42 >>> print TestApp(app).get('/', HTTP_IF_NONE_MATCH='"foo"').text() Calculating! 304 Not Modified ETag: "foo" Now we rewrite this so that ``very_expensive_calculation`` will be only be called when the ETag does not match: .. testcode:: @to_wsgi @with_etag(lambda request: 'foo') def my_handler(request): def generate_content(): yield '' yield very_expensive_calculation() return Response(generate_content()) app = etag_middleware(my_handler) And we can see that ``very_expensive_calculation`` is not called when we supply the ``If-None-Match`` header: .. doctest:: >>> print TestApp(app).get('/', HTTP_IF_NONE_MATCH='"foo"').text() 304 Not Modified ETag: "foo" pesto.caching API documentation ---------------------------------- .. automodule:: pesto.caching :members: pesto-25/doc/wsgiutils.rst0000644000175100017510000000016611613355542016621 0ustar oliveroliver00000000000000 pesto.wsgiutils API documention -------------------------------- .. automodule:: pesto.wsgiutils :members: pesto-25/doc/request.rst0000644000175100017510000001065711613355542016265 0ustar oliveroliver00000000000000 The request object ==================== .. testsetup:: * from pesto.request import Request from pesto.testing import make_environ, TestApp The ``request`` object gives access to all WSGI environment properties, including request headers and server variables, as well as submitted form data. Core WSGI environment variables and other useful information is exposed as attributes:: >>> from pesto.request import Request >>> environ = { ... 'PATH_INFO': '/pages/index.html', ... 'SCRIPT_NAME': '/myapp', ... 'REQUEST_METHOD': 'GET', ... 'SERVER_PROTOCOL': 'http', ... 'SERVER_NAME': 'example.org', ... 'SERVER_PORT': '80' ... } >>> request = Request(environ) >>> request.script_name '/myapp' >>> request.path_info '/pages/index.html' >>> request.application_uri 'http://example.org/myapp' The API documentation at the end of this page contains the complete list of request attributes. MultiDicts ---------- Many items of data accessible through the request object are exposed as instances of ``pesto.utils.MultiDict``. This has a dictionary-like interface, with additional methods for retrieving data where a key has multiple values associated with it. For example: .. doctest:: >>> from pesto.testing import TestApp, make_environ >>> from pesto.request import Request >>> request = Request(make_environ(QUERY_STRING='a=Trinidad;b=Mexico;b=Honolulu')) >>> request.form.get('a') u'Trinidad' >>> request.form.get('b') u'Mexico' >>> request.form.getlist('b') [u'Mexico', u'Honolulu'] Form data ----------- Form values are accessible from ``request.form``, a ``pesto.utils.MultiDict`` object, giving dictionary like access to submitted form data:: request.form["email"] request.form.get("email") As this is a very common usage, shortcuts exist:: request["email"] request.get("email") For GET requests, ``request.form`` contains data parsed from the URL query string. For POST requests, ``request.form`` contains only POSTED data. If a query string was present in a POST request, it is necessary to use ``request.query`` to access this, which has the same interface. Cookie data ------------ Cookies are accessible from the ``request.cookies`` MultiDict. See :doc:`cookies` for more information. File uploads ------------- The ``request.files`` dictionary has the same interface as ``request.form``, but values are ``FileUpload`` objects, which allow you to access information about uploaded files as well as the raw data: .. testcode:: :hide: >>> from pesto.testing import TestApp >>> request = MockWSGI.make_post_multipart( ... files=[('fileupload', 'uploaded.txt', 'text/plain', 'Here is a file upload for you')] ... ).request .. doctest:: >>> upload = request.files['fileupload'] >>> upload.filename 'uploaded.txt' >>> upload.file # doctest: +ELLIPSIS >>> upload.headers # doctest: +ELLIPSIS >>> upload.headers['Content-Type'] 'text/plain' >>> upload.file.read() 'Here is a nice file upload for you' Maximum size limit ------------------ Posted data, including file uploads, is limited in size. This limit can be altered by adjusting ``pesto.request.Request.MAX_SIZE`` and ``pesto.request.Request.MAX_MULTIPART_SIZE``. Example 1 – set the global maximum size for POSTed data to 100kb: .. doctest:: >>> from pesto.request import Request >>> kb = 1024 >>> Request.MAX_SIZE = 100 * kb Example 2 – set the global maximum size for multipart/form-data POSTs to 4Mb. The total size data uploaded, including all form fields and file uploads will be limited to 4Mb. Individual fields (except file uploads) will be limited to 100Kb by the ``MAX_SIZE`` set in the previous example: .. doctest:: >>> Mb = 1024 * kb >>> Request.MAX_MULTIPART_SIZE = 4 * Mb Pesto also supports overriding these limits on a per-request basis: .. doctest:: >>> def big_file_upload(request): ... request.MAX_MULTIPART_SIZE = 100 * Mb ... request.files['bigfile'].save('/tmp/bigfile.bin') ... return Response(["Thanks for uploading a big file"]) ... pesto.request API documention ------------------------------- .. automodule:: pesto.request :members: pesto-25/doc/cookies.rst0000644000175100017510000000515011613355542016221 0ustar oliveroliver00000000000000 Cookies ========= .. testsetup:: * from pesto.testing import TestApp from pesto import to_wsgi from pesto.response import Response import pesto.cookie You can read cookies through the request object, and construct cookies using the functions in ``pesto.cookies``. Reading a cookie ----------------- The easiest way to read a single cookie is to query the ``request.cookies`` attribute. This is a ``pesto.utils.MultiDict`` mapping cookie names to single instances of ``pesto.cookie.Cookie``: .. testcode:: def handler(request): secret = request.cookies.get('secret') if secret and secret.value == 'marmot': return Response(['pass, friend']) else: return Response.forbidden() .. doctest:: :hide: >>> app = TestApp(to_wsgi(handler)) >>> app.get(HTTP_COOKIE='secret=marmot').body 'pass, friend' >>> app.get(HTTP_COOKIE='secret=doormat').status '403 Forbidden' Setting cookies ----------------- Simply assign an instance of ``pesto.cookie.Cookie`` to a set-cookie header: .. testcode:: def handler(request): return Response( ['blah'], set_cookie=pesto.cookie.Cookie( name='partnumber', value='Rocket_Launcher_0001', path='/acme', maxage=3600, domain='example.com' ) ) .. doctest:: :hide: >>> app = TestApp(to_wsgi(handler)) >>> print app.get().text() 200 OK Content-Type: text/html; charset=UTF-8 Set-Cookie: partnumber=Rocket_Launcher_0001;Domain=example.com;Expires=...Max-Age=3600;Path=/acme;Version=1 ... Clearing cookies ----------------- To expire a cookie is to clear it. Set a new cookie with the same details as the one you are clearing, but with no value and maxage=0: .. testcode:: def handler(request): return Response( [], set_cookie=pesto.cookie.Cookie( name='partnumber', value='', path='/acme', maxage=0, domain='example.com' ) ) .. doctest:: :hide: >>> app = TestApp(to_wsgi(handler)) >>> print app.get().text() 200 OK Content-Type: text/html; charset=UTF-8 Set-Cookie: partnumber=;Domain=example.com;Expires=...;Max-Age=0;Path=/acme;Version=1 ... pesto.cookie API documention ------------------------------ .. automodule:: pesto.cookie :members: pesto-25/doc/dispatch.rst0000644000175100017510000002271411613355542016371 0ustar oliveroliver00000000000000 URL dispatch ============== .. testsetup:: * from pesto import to_wsgi from pesto.response import Response from pesto.testing import TestApp Pesto's ``DispatcherApp`` is a useful WSGI application that can map URIs to handler functions. For example: .. testcode:: from pesto import DispatcherApp, Response app = DispatcherApp() @app.match('/recipes', 'GET') def recipe_index(request): return Response(['This is the recipe index page']) @app.match('/recipes/', 'GET') def recipe_index(request, category): return Response(['This is the page for ', category, ' recipes']) .. doctest:: :hide: >>> TestApp(app).get("/recipes").body 'This is the recipe index page' >>> TestApp(app).get("/recipes/goop").body 'This is the page for goop recipes' Dispatchers can use prefined patterns expressions to extract data from URIs and pass it on to a handler. The following expression types are supported: * ``unicode`` - any unicode string (not including forward slashes) * ``path`` - any path (includes forward slashes) * ``int`` - any integer * ``any`` - a string matching a list of alternatives It is also possible to add your own types so you to match custom patterns (see the API documentation for ``ExtensiblePattern.register_pattern``). Match patterns are delimited by angle brackets, and generally have the form ````. Some examples: * ``'/recipes//'``. This would match a URI such as ``/recipes/fish/7``, and call the handler function with the arguments ``category=u'fish', id=7``. * ``'/entries//``. This would match a URI such as ``/entries/2008/05``, and call the handler function with the arguments ``year=2008, month=5``. * ``'/documents//.pdf``. This would match a URI such as ``/documents/all/2008/topsecret.pdf``, and call the handler function with the arguments ``directory=u'all/2008/', name=u'topsecret'``. You can also map separate handlers to different HTTP methods for the same URL, eg the ``GET`` method could display a form, and the ``POST`` method of the same URL could handle the submission: .. testcode:: @app.match('/contact-form', 'GET') def contact_form(request): """ Display a contact form """ @app.match('/contact-form', 'POST') def contact_form_submit(request): """ Process the form, eg by sending an email """ Dispatchers do not have to be function decorators. The following code is equivalent to the previous example: .. testcode:: app.match('/contact-form', GET=contact_form, POST=contact_form_submit) Matching is always based on the path part of the URL (taken from the WSGI ``environ['PATH_INFO']`` value). URI redirecting --------------- A combination of the Response object and dispatchers can be used for URI rewriting and redirection: .. testcode:: from functools import partial from pesto import DispatcherApp, Response from pesto import response app = DispatcherApp() app.match('/old-link', GET=partial(Response.redirect, '/new-link', status=response.STATUS_MOVED_PERMANENTLY)) Any calls to ``/old-link`` will now be met with: .. testcode:: :hide: print TestApp(app).get('/old-link').text() .. testoutput:: 301 Moved Permanently Content-Type: text/html; charset=UTF-8 Location: http://localhost/new-link ... URI generation --------------- Functions mapped by the dispatcher object are assigned a ``url`` method, allowing URIs to be generated: .. testcode:: from pesto import DispatcherApp, Response app = DispatcherApp() @app.match('/recipes', 'GET') def recipe_index(request): return Response(['this is the recipe index page']) @app.match('/recipes/', 'GET') def show_recipe(request, recipe_id): return Response(['this is the recipe detail page for recipe #%d' % recipe_id]) Calling the ``url`` method will generate fully qualified URLs for any function mapped by a dispatcher: .. doctest:: >>> from pesto.testing import make_environ >>> from pesto.request import Request >>> request = Request(make_environ(SERVER_NAME='example.com')) >>> >>> recipe_index.url() 'http://example.com/recipes' >>> show_recipe.url(recipe_id=42) 'http://example.com/recipes/42' Note: the ``url`` method needs a live request object, usually acquired through ``pesto.currentrequest``, although it can also be passed as a parameter. If you need to call this method outside of a WSGI request context then you will need to simulate a WSGI environ to generate a Request object. Repurposing handler functions ----------------------------- Suppose you have a function that returns a list of orders, with the price and date, and you want to this list both as regular HTML page and in JSON notation for AJAX enhancement. Instead of writing two handlers – one for the HTML response and one for the JSON – it's possible to use the same handler function to serve both types of request. We'll start by creating some sample data: .. testcode:: from datetime import date class Order(object): def __init__(self, price, date): self.price = price self.date = date orders = [ Order(12.99, date(2009, 7, 1)), Order(7.75, date(2009, 8, 1)), Order(8.25, date(2009, 8, 1)), ] The handler function is going to return a Python data structure, and we'll add decorator functions that can convert this data structure to JSON and HTML: .. testcode:: import json from cgi import escape from functools import wraps def to_json(func): """ Wrap a Pesto handler to return a JSON-encoded string from a python data structure. """ @wraps(func) def to_json(request, *args, **kwargs): result = func(request, *args, **kwargs) if isinstance(result, Response): return result return Response( content=[json.dumps(result)], content_type='application/json' ) return to_json def to_html(func): def to_html(request, *args, **kwargs): data = func(request, *args, **kwargs) if not data: return Response([], content_type='text/html') keys = sorted(data[0].keys()) result = ['\n'] result.append('\n') result.extend(' \n' % escape(key) for key in keys) result.append('\n') for item in data: result.append('\n') result.extend(' \n' % escape(str(item[key])) for key in keys) result.append('\n') result.append('
%s
%s
') return Response(result) return to_html (Note that for a real world application you should use a templating system rather than putting HTML directly in your code. But for this small example it's fine). Now we can write a handler function to serve the data. ``DispatcherApp.match`` has a ``decorators`` argument that allows us to use the same function to serve both the HTML and JSON versions by wrapping it in different decorators for each: .. testcode:: from pesto import DispatcherApp app = DispatcherApp() @app.match('/orders.json', 'GET', decorators=[to_json]) @app.match('/orders.html', 'GET', decorators=[to_html]) def list_orders(request): return [ { 'date': order.date.strftime('%Y-%m-%d'), 'price': order.price, } for order in orders ] We can now call this function in three ways. First, the HTML version: .. doctest:: >>> from pesto.testing import TestApp >>> print TestApp(app).get('/orders.html').body
date price
2009-07-01 12.99
2009-08-01 7.75
2009-08-01 8.25
And the JSON version: .. doctest:: >>> print TestApp(app).get('/orders.json').body [{"date": "2009-07-01", "price": 12.99}, {"date": "2009-08-01", "price": 7.75}, {"date": "2009-08-01", "price": 8.25}] Finally, we can call the function just as a regular python function. We need to pass the function a (dummy) request object in this case:: >>> from pprint import pprint >>> from pesto.testing import make_environ >>> dummy_request = make_environ() >>> pprint(list_orders(dummy_request)) [{'date': '2009-07-01', 'price': 12.99}, {'date': '2009-08-01', 'price': 7.75}, {'date': '2009-08-01', 'price': 8.25}] pesto.dispatch API documentation ---------------------------------- .. automodule:: pesto.dispatch :members: