summaryrefslogtreecommitdiff
path: root/flup/publisher.py
diff options
context:
space:
mode:
Diffstat (limited to 'flup/publisher.py')
-rw-r--r--flup/publisher.py767
1 files changed, 0 insertions, 767 deletions
diff --git a/flup/publisher.py b/flup/publisher.py
deleted file mode 100644
index a0c6676..0000000
--- a/flup/publisher.py
+++ /dev/null
@@ -1,767 +0,0 @@
-# Copyright (c) 2002, 2005, 2006 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. 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.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import os
-import inspect
-import cgi
-import types
-from Cookie import SimpleCookie
-
-__all__ = ['Request',
- 'Response',
- 'Transaction',
- 'Publisher',
- 'File',
- 'Action',
- 'Redirect',
- 'InternalRedirect',
- 'getexpected',
- 'trimkw']
-
-class NoDefault(object):
- """Sentinel object so we can distinguish None in keyword arguments."""
- pass
-
-class Request(object):
- """
- Encapsulates data about the HTTP request.
-
- Supported attributes that originate from here: (The attributes are
- read-only, however you may modify the contents of environ.)
- transaction - Enclosing Transaction object.
- environ - Environment variables, as passed from WSGI adapter.
- method - Request method.
- publisherScriptName - SCRIPT_NAME of Publisher.
- scriptName - SCRIPT_NAME for request.
- pathInfo - PATH_INFO for request.
- form - Dictionary containing data from query string and/or POST request.
- cookie - Cookie from request.
- """
- def __init__(self, transaction, environ):
- self._transaction = transaction
-
- self._environ = environ
- self._publisherScriptName = environ.get('SCRIPT_NAME', '')
-
- self._form = {}
- self._parseFormData()
-
- # Is this safe? Will it ever raise exceptions?
- self._cookie = SimpleCookie(environ.get('HTTP_COOKIE', None))
-
- def _parseFormData(self):
- """
- Fills self._form with data from a FieldStorage. May be overidden to
- provide custom form processing. (Like no processing at all!)
- """
- # Parse query string and/or POST data.
- form = FieldStorage(fp=self._environ['wsgi.input'],
- environ=self._environ, keep_blank_values=1)
-
- # Collapse FieldStorage into a simple dict.
- if form.list is not None:
- for field in form.list:
- # Wrap uploaded files
- if field.filename:
- val = File(field)
- else:
- val = field.value
-
- # Add File/value to args, constructing a list if there are
- # multiple values.
- if self._form.has_key(field.name):
- self._form[field.name].append(val)
- else:
- self._form[field.name] = [val]
-
- # Unwrap lists with a single item.
- for name,val in self._form.items():
- if len(val) == 1:
- self._form[name] = val[0]
-
- def _get_transaction(self):
- return self._transaction
- transaction = property(_get_transaction, None, None,
- "Transaction associated with this Request")
-
- def _get_environ(self):
- return self._environ
- environ = property(_get_environ, None, None,
- "Environment variables passed from adapter")
-
- def _get_method(self):
- return self._environ['REQUEST_METHOD']
- method = property(_get_method, None, None,
- "Request method")
-
- def _get_publisherScriptName(self):
- return self._publisherScriptName
- publisherScriptName = property(_get_publisherScriptName, None, None,
- 'SCRIPT_NAME of Publisher')
-
- def _get_scriptName(self):
- return self._environ.get('SCRIPT_NAME', '')
- scriptName = property(_get_scriptName, None, None,
- "SCRIPT_NAME of request")
-
- def _get_pathInfo(self):
- return self._environ.get('PATH_INFO', '')
- pathInfo = property(_get_pathInfo, None, None,
- "PATH_INFO of request")
-
- def _get_form(self):
- return self._form
- form = property(_get_form, None, None,
- "Parsed GET/POST data")
-
- def _get_cookie(self):
- return self._cookie
- cookie = property(_get_cookie, None, None,
- "Cookie received from client")
-
-class Response(object):
- """
- Encapsulates response-related data.
-
- Supported attributes:
- transaction - Enclosing Transaction object.
- status - Response status code (and message).
- headers - Response headers.
- cookie - Response cookie.
- contentType - Content type of body.
- body - Response body. Must be an iterable that yields strings.
-
- Since there are multiple ways of passing response info back to
- Publisher, here is their defined precedence:
- status - headers['Status'] first, then status
- cookie - Any values set in this cookie are added to the headers in
- addition to existing Set-Cookie headers.
- contentType - headers['Content-Type'] first, then contentType. If
- neither are specified, defaults to 'text/html'.
- body - Return of function takes precedence. If function returns None,
- body is used instead.
- """
- _initialHeaders = {
- 'Pragma': 'no-cache',
- 'Cache-Control': 'no-cache'
- }
-
- def __init__(self, transaction):
- self._transaction = transaction
-
- self._status = '200 OK'
-
- # Initial response headers.
- self._headers = header_dict(self._initialHeaders)
-
- self._cookie = SimpleCookie()
-
- self.body = []
-
- def _get_transaction(self):
- return self._transaction
- transaction = property(_get_transaction, None, None,
- "Transaction associated with this Response")
-
- def _get_status(self):
- status = self._headers.get('Status')
- if status is not None:
- return status
- return self._status
- def _set_status(self, value):
- if self._headers.has_key('Status'):
- self._headers['Status'] = value
- else:
- self._status = value
- status = property(_get_status, _set_status, None,
- 'Response status')
-
- def _get_headers(self):
- return self._headers
- headers = property(_get_headers, None, None,
- "Headers to send in response")
-
- def _get_cookie(self):
- return self._cookie
- cookie = property(_get_cookie, None, None,
- "Cookie to send in response")
-
- def _get_contentType(self):
- return self._headers.get('Content-Type', 'text/html')
- def _set_contentType(self, value):
- self._headers['Content-Type'] = value
- contentType = property(_get_contentType, _set_contentType, None,
- 'Content-Type of the response body')
-
-class Transaction(object):
- """
- Encapsulates objects associated with a single transaction (Request,
- Response, and possibly a Session).
-
- Public attributes: (all read-only)
- request - Request object.
- response - Response object.
-
- If Publisher sits on top of SessionMiddleware, the public API of
- SessionService is also available through the Transaction object.
- """
- _requestClass = Request
- _responseClass = Response
-
- def __init__(self, publisher, environ):
- self._publisher = publisher
- self._request = self._requestClass(self, environ)
- self._response = self._responseClass(self)
-
- # Session support.
- self._sessionService = environ.get('com.saddi.service.session')
- if self._sessionService is not None:
- self.encodeURL = self._sessionService.encodeURL
-
- def _get_request(self):
- return self._request
- request = property(_get_request, None, None,
- "Request associated with this Transaction")
-
- def _get_response(self):
- return self._response
- response = property(_get_response, None, None,
- "Response associated with this Transaction")
-
- # Export SessionService API
-
- def _get_session(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.session
- session = property(_get_session, None, None,
- 'Returns the Session object associated with this '
- 'client')
-
- def _get_hasSession(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.hasSession
- hasSession = property(_get_hasSession, None, None,
- 'True if a Session currently exists for this client')
-
- def _get_isSessionNew(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.isSessionNew
- isSessionNew = property(_get_isSessionNew, None, None,
- 'True if the Session was created in this '
- 'transaction')
-
- def _get_hasSessionExpired(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.hasSessionExpired
- hasSessionExpired = property(_get_hasSessionExpired, None, None,
- 'True if the client was associated with a '
- 'non-existent Session')
-
- def _get_encodesSessionInURL(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.encodesSessionInURL
- def _set_encodesSessionInURL(self, value):
- assert self._sessionService is not None, 'No session service found'
- self._sessionService.encodesSessionInURL = value
- encodesSessionInURL = property(_get_encodesSessionInURL,
- _set_encodesSessionInURL, None,
- 'True if the Session ID should be encoded '
- 'in the URL')
-
- def prepare(self):
- """
- Called before resolved function is invoked. If overridden,
- super's prepare() MUST be called and it must be called first.
- """
- # Pass form values as keyword arguments.
- args = dict(self._request.form)
-
- # Pass Transaction to function if it wants it.
- args['transaction'] = args['trans'] = self
-
- self._args = args
-
- def call(self, func, args):
- """
- May be overridden to provide custom exception handling and/or
- per-request additions, e.g. opening a database connection,
- starting a transaction, etc.
- """
- # Trim down keywords to only what the callable expects.
- expected, varkw = getexpected(func)
- trimkw(args, expected, varkw)
-
- return func(**args)
-
- def run(self, func):
- """
- Execute the function, doing any Action processing and also
- post-processing the function/Action's return value.
- """
- try:
- # Call the function.
- result = self.call(func, self._args)
- except Action, a:
- # Caught an Action, have it do whatever it does
- result = a.run(self)
-
- response = self._response
- headers = response.headers
-
- if result is not None:
- if type(result) in types.StringTypes:
- assert type(result) is str, 'result cannot be unicode!'
-
- # Set Content-Length, if needed
- if not headers.has_key('Content-Length'):
- headers['Content-Length'] = str(len(result))
-
- # Wrap in list for WSGI
- response.body = [result]
- else:
- if __debug__:
- try:
- iter(result)
- except TypeError:
- raise AssertionError, 'result not iterable!'
- response.body = result
-
- # If result was None, assume response.body was appropriately set.
-
- def finish(self):
- """
- Called after resolved function returns, but before response is
- sent. If overridden, super's finish() should be called last.
- """
- response = self._response
- headers = response.headers
-
- # Copy cookie to headers.
- items = response.cookie.items()
- items.sort()
- for name,morsel in items:
- headers.add('Set-Cookie', morsel.OutputString())
-
- # If there is a Status header, transfer its value to response.status.
- # It must not remain in the headers!
- status = headers.get('Status', NoDefault)
- if status is not NoDefault:
- del headers['Status']
- response.status = status
-
- code = int(response.status[:3])
- # Check if it's a response that must not include a body.
- # (See 4.3 in RFC2068.)
- if code / 100 == 1 or code in (204, 304):
- # Remove any trace of the response body, if that's the case.
- for header,value in headers.items():
- if header.lower().startswith('content-'):
- del headers[header]
- response.body = []
- else:
- if self._request.method == 'HEAD':
- # HEAD reponse must not return a body (but the headers must be
- # kept intact).
- response.body = []
-
- # Add Content-Type, if missing.
- if not headers.has_key('Content-Type'):
- headers['Content-Type'] = response.contentType
-
- # If we have a close() method, ensure that it is called.
- if hasattr(self, 'close'):
- response.body = _addClose(response.body, self.close)
-
-class Publisher(object):
- """
- WSGI application that publishes Python functions as web pages. Constructor
- takes an instance of a concrete subclass of Resolver, which is responsible
- for mapping requests to functions.
-
- Query string/POST data values are passed to the function as keyword
- arguments. If the function does not support variable keywords (e.g.
- does not have a ** parameter), the function will only be passed
- keywords which it expects. It is recommended that all keyword parameters
- have defaults so that missing form data does not raise an exception.
-
- A Transaction instance is always passed to the function via the
- "transaction" or "trans" keywords. See the Transaction, Request, and
- Response classes.
-
- Valid return types for the function are: a string, None, or an iterable
- that yields strings. If returning None, it is expected that Response.body
- has been appropriately set. (It must be an iterable that yields strings.)
-
- An instance of Publisher itself is the WSGI application.
- """
- _transactionClass = Transaction
-
- def __init__(self, resolver, transactionClass=None, error404=None):
- self._resolver = resolver
-
- if transactionClass is not None:
- self._transactionClass = transactionClass
-
- if error404 is not None:
- self._error404 = error404
-
- def _get_resolver(self):
- return self._resolver
- resolver = property(_get_resolver, None, None,
- 'Associated Resolver for this Publisher')
-
- def __call__(self, environ, start_response):
- """
- WSGI application interface. Creates a Transaction (which does most
- of the work) and sends the response.
- """
- # Set up a Transaction.
- transaction = self._transactionClass(self, environ)
-
- # Make any needed preparations.
- transaction.prepare()
-
- redirect = False
-
- while True:
- # Attempt to resolve the function.
- func = self._resolver.resolve(transaction.request,
- redirect=redirect)
- if func is None:
- func = self._error404
-
- try:
- # Call the function.
- transaction.run(func)
- except InternalRedirect, r:
- # Internal redirect. Set up environment and resolve again.
- environ['SCRIPT_NAME'] = transaction.request.publisherScriptName
- environ['PATH_INFO'] = r.pathInfo
- redirect = True
- else:
- break
-
- # Give Transaction a chance to do modify/add to the response.
- transaction.finish()
-
- # Transform headers into a list. (Need to pay attention to
- # multiple values.)
- responseHeaders = []
- for key,value in transaction.response.headers.items():
- if type(value) is list:
- for v in value:
- responseHeaders.append((key, v))
- else:
- responseHeaders.append((key, value))
-
- start_response(transaction.response.status, responseHeaders)
- return transaction.response.body
-
- def _error404(self, transaction):
- """Error page to display when resolver fails."""
- transaction.response.status = '404 Not Found'
- request_uri = transaction.request.environ.get('REQUEST_URI')
- if request_uri is None:
- request_uri = transaction.request.environ.get('SCRIPT_NAME', '') + \
- transaction.request.environ.get('PATH_INFO', '')
- return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
-<html><head>
-<title>404 Not Found</title>
-</head><body>
-<h1>Not Found</h1>
-The requested URL %s was not found on this server.<p>
-<hr>
-%s</body></html>
-""" % (request_uri, transaction.request.environ.get('SERVER_SIGNATURE', ''))]
-
-class File(object):
- """
- Wrapper so we can identify uploaded files.
- """
- def __init__(self, field):
- self.filename = field.filename
- self.file = field.file
- self.type = field.type
- self.type_options = field.type_options
- self.headers = field.headers
-
-class Action(Exception):
- """
- Abstract base class for 'Actions', which are just exceptions.
- Within Publisher, Actions have no inherent meaning and are used
- as a shortcut to perform specific actions where it's ok for a
- function to abruptly halt. (Like redirects or displaying an
- error page.)
-
- I don't know if using exceptions this way is good form (something
- tells me no ;) Their usage isn't really required, but they're
- convenient in some situations.
- """
- def run(self, transaction):
- """Override to perform your action."""
- raise NotImplementedError, self.__class__.__name__ + '.run'
-
-class Redirect(Action):
- """
- Redirect to the given URL.
- """
- def __init__(self, url, permanent=False):
- self._url = url
- self._permanent = permanent
-
- def run(self, transaction):
- response = transaction.response
- response.status = self._permanent and '301 Moved Permanently' or \
- '302 Moved Temporarily'
- response.headers.reset()
- response.headers['Location'] = self._url
- response.contentType = 'text/html'
- return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
-<html><head>
-<title>%s</title>
-</head><body>
-<h1>Found</h1>
-<p>The document has moved <a href="%s">here</a>.</p>
-<hr>
-%s</body></html>
-""" % (response.status, self._url,
- transaction.request.environ.get('SERVER_SIGNATURE', ''))]
-
-class InternalRedirect(Exception):
- """
- An internal redirect using a new PATH_INFO (relative to Publisher's
- SCRIPT_NAME).
-
- When handling an InternalRedirect, the behavior of all included
- Resolvers is to expose a larger set of callables (that would normally
- be hidden). Therefore, it is important that you set pathInfo securely -
- preferably, it should not depend on any data from the request. Ideally,
- pathInfo should be a constant string.
- """
- def __init__(self, pathInfo):
- self.pathInfo = pathInfo
-
-class FieldStorage(cgi.FieldStorage):
- def __init__(self, *args, **kw):
- """
- cgi.FieldStorage only parses the query string during a GET or HEAD
- request. Fix this.
- """
- cgi.FieldStorage.__init__(self, *args, **kw)
-
- environ = kw.get('environ') or os.environ
- method = environ.get('REQUEST_METHOD', 'GET').upper()
- if method not in ('GET', 'HEAD'): # cgi.FieldStorage already parsed?
- qs = environ.get('QUERY_STRING')
- if qs:
- if self.list is None:
- self.list = []
- for key,value in cgi.parse_qsl(qs, self.keep_blank_values,
- self.strict_parsing):
- self.list.append(cgi.MiniFieldStorage(key, value))
-
-class header_dict(dict):
- """
- This is essentially a case-insensitive dictionary, with some additions
- geared towards supporting HTTP headers (like __str__(), add(), and
- reset()).
- """
- def __init__(self, val=None):
- """
- If initialized with an existing dictionary, calling reset() will
- reset our contents back to that initial dictionary.
- """
- dict.__init__(self)
- self._keymap = {}
- if val is None:
- val = {}
- self.update(val)
- self._reset_state = dict(val)
-
- def __contains__(self, key):
- return key.lower() in self._keymap
-
- def __delitem__(self, key):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- raise KeyError, key
- del self._keymap[lower_key]
- dict.__delitem__(self, real_key)
-
- def __getitem__(self, key):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- raise KeyError, key
- return dict.__getitem__(self, real_key)
-
- def __str__(self):
- """Output as HTTP headers."""
- s = ''
- for k,v in self.items():
- if type(v) is list:
- for i in v:
- s += '%s: %s\n' % (k, i)
- else:
- s += '%s: %s\n' % (k, v)
- return s
-
- def __setitem__(self, key, value):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- self._keymap[lower_key] = key
- dict.__setitem__(self, key, value)
- else:
- dict.__setitem__(self, real_key, value)
-
- def clear(self):
- self._keymap.clear()
- dict.clear(self)
-
- def copy(self):
- c = self.__class__(self)
- c._reset_state = self._reset_state
- return c
-
- def get(self, key, failobj=None):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- return failobj
- return dict.__getitem__(self, real_key)
-
- def has_key(self, key):
- return key.lower() in self._keymap
-
- def setdefault(self, key, failobj=None):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- self._keymap[lower_key] = key
- dict.__setitem__(self, key, failobj)
- return failobj
- else:
- return dict.__getitem__(self, real_key)
-
- def update(self, d):
- for k,v in d.items():
- self[k] = v
-
- def add(self, key, value):
- """
- Add a new header value. Does not overwrite previous value of header
- (in contrast to __setitem__()).
- """
- if self.has_key(key):
- if type(self[key]) is list:
- self[key].append(value)
- else:
- self[key] = [self[key], value]
- else:
- self[key] = value
-
- def reset(self):
- """Reset contents to that at the time this instance was created."""
- self.clear()
- self.update(self._reset_state)
-
-def _addClose(appIter, closeFunc):
- """
- Wraps an iterator so that its close() method calls closeFunc. Respects
- the existence of __len__ and the iterator's own close() method.
-
- Need to use metaclass magic because __len__ and next are not
- recognized unless they're part of the class. (Can't assign at
- __init__ time.)
- """
- class metaIterWrapper(type):
- def __init__(cls, name, bases, clsdict):
- super(metaIterWrapper, cls).__init__(name, bases, clsdict)
- if hasattr(appIter, '__len__'):
- cls.__len__ = appIter.__len__
- cls.next = iter(appIter).next
- if hasattr(appIter, 'close'):
- def _close(self):
- appIter.close()
- closeFunc()
- cls.close = _close
- else:
- cls.close = closeFunc
-
- class iterWrapper(object):
- __metaclass__ = metaIterWrapper
- def __iter__(self):
- return self
-
- return iterWrapper()
-
-# Utilities which may be useful outside of Publisher? Perhaps for decorators...
-
-def getexpected(func):
- """
- Returns as a 2-tuple the passed in object's expected arguments and
- whether or not it accepts variable keywords.
- """
- assert callable(func), 'object not callable'
-
- if not inspect.isclass(func):
- # At this point, we assume func is either a function, method, or
- # callable instance.
- if not inspect.isfunction(func) and not inspect.ismethod(func):
- func = getattr(func, '__call__') # When would this fail?
-
- argspec = inspect.getargspec(func)
- expected, varkw = argspec[0], argspec[2] is not None
- if inspect.ismethod(func):
- expected = expected[1:]
- else:
- # A class. Try to figure out the calling conventions of the
- # constructor.
- init = getattr(func, '__init__', None)
- # Sigh, this is getting into the realm of black magic...
- if init is not None and inspect.ismethod(init):
- argspec = inspect.getargspec(init)
- expected, varkw = argspec[0], argspec[2] is not None
- expected = expected[1:]
- else:
- expected, varkw = [], False
-
- return expected, varkw
-
-def trimkw(kw, expected, varkw):
- """
- If necessary, trims down a dictionary of keyword arguments to only
- what's expected.
- """
- if not varkw: # Trimming only necessary if it doesn't accept variable kw
- for name in kw.keys():
- if name not in expected:
- del kw[name]