1
# Copyright (c) 2002, 2005, 2006 Allan Saddi <allan@saddi.com>
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions
7
# 1. Redistributions of source code must retain the above copyright
8
# notice, this list of conditions and the following disclaimer.
9
# 2. Redistributions in binary form must reproduce the above copyright
10
# notice, this list of conditions and the following disclaimer in the
11
# documentation and/or other materials provided with the distribution.
13
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25
# $Id: publisher.py 1853 2006-04-06 23:05:58Z asaddi $
27
__author__ = 'Allan Saddi <allan@saddi.com>'
28
__version__ = '$Revision: 1853 $'
34
from Cookie import SimpleCookie
47
class NoDefault(object):
48
"""Sentinel object so we can distinguish None in keyword arguments."""
51
class Request(object):
53
Encapsulates data about the HTTP request.
55
Supported attributes that originate from here: (The attributes are
56
read-only, however you may modify the contents of environ.)
57
transaction - Enclosing Transaction object.
58
environ - Environment variables, as passed from WSGI adapter.
59
method - Request method.
60
publisherScriptName - SCRIPT_NAME of Publisher.
61
scriptName - SCRIPT_NAME for request.
62
pathInfo - PATH_INFO for request.
63
form - Dictionary containing data from query string and/or POST request.
64
cookie - Cookie from request.
66
def __init__(self, transaction, environ):
67
self._transaction = transaction
69
self._environ = environ
70
self._publisherScriptName = environ.get('SCRIPT_NAME', '')
75
# Is this safe? Will it ever raise exceptions?
76
self._cookie = SimpleCookie(environ.get('HTTP_COOKIE', None))
78
def _parseFormData(self):
80
Fills self._form with data from a FieldStorage. May be overidden to
81
provide custom form processing. (Like no processing at all!)
83
# Parse query string and/or POST data.
84
form = FieldStorage(fp=self._environ['wsgi.input'],
85
environ=self._environ, keep_blank_values=1)
87
# Collapse FieldStorage into a simple dict.
88
if form.list is not None:
89
for field in form.list:
96
# Add File/value to args, constructing a list if there are
98
if self._form.has_key(field.name):
99
self._form[field.name].append(val)
101
self._form[field.name] = [val]
103
# Unwrap lists with a single item.
104
for name,val in self._form.items():
106
self._form[name] = val[0]
108
def _get_transaction(self):
109
return self._transaction
110
transaction = property(_get_transaction, None, None,
111
"Transaction associated with this Request")
113
def _get_environ(self):
115
environ = property(_get_environ, None, None,
116
"Environment variables passed from adapter")
118
def _get_method(self):
119
return self._environ['REQUEST_METHOD']
120
method = property(_get_method, None, None,
123
def _get_publisherScriptName(self):
124
return self._publisherScriptName
125
publisherScriptName = property(_get_publisherScriptName, None, None,
126
'SCRIPT_NAME of Publisher')
128
def _get_scriptName(self):
129
return self._environ.get('SCRIPT_NAME', '')
130
scriptName = property(_get_scriptName, None, None,
131
"SCRIPT_NAME of request")
133
def _get_pathInfo(self):
134
return self._environ.get('PATH_INFO', '')
135
pathInfo = property(_get_pathInfo, None, None,
136
"PATH_INFO of request")
140
form = property(_get_form, None, None,
141
"Parsed GET/POST data")
143
def _get_cookie(self):
145
cookie = property(_get_cookie, None, None,
146
"Cookie received from client")
148
class Response(object):
150
Encapsulates response-related data.
152
Supported attributes:
153
transaction - Enclosing Transaction object.
154
status - Response status code (and message).
155
headers - Response headers.
156
cookie - Response cookie.
157
contentType - Content type of body.
158
body - Response body. Must be an iterable that yields strings.
160
Since there are multiple ways of passing response info back to
161
Publisher, here is their defined precedence:
162
status - headers['Status'] first, then status
163
cookie - Any values set in this cookie are added to the headers in
164
addition to existing Set-Cookie headers.
165
contentType - headers['Content-Type'] first, then contentType. If
166
neither are specified, defaults to 'text/html'.
167
body - Return of function takes precedence. If function returns None,
168
body is used instead.
171
'Pragma': 'no-cache',
172
'Cache-Control': 'no-cache'
175
def __init__(self, transaction):
176
self._transaction = transaction
178
self._status = '200 OK'
180
# Initial response headers.
181
self._headers = header_dict(self._initialHeaders)
183
self._cookie = SimpleCookie()
187
def _get_transaction(self):
188
return self._transaction
189
transaction = property(_get_transaction, None, None,
190
"Transaction associated with this Response")
192
def _get_status(self):
193
status = self._headers.get('Status')
194
if status is not None:
197
def _set_status(self, value):
198
if self._headers.has_key('Status'):
199
self._headers['Status'] = value
202
status = property(_get_status, _set_status, None,
205
def _get_headers(self):
207
headers = property(_get_headers, None, None,
208
"Headers to send in response")
210
def _get_cookie(self):
212
cookie = property(_get_cookie, None, None,
213
"Cookie to send in response")
215
def _get_contentType(self):
216
return self._headers.get('Content-Type', 'text/html')
217
def _set_contentType(self, value):
218
self._headers['Content-Type'] = value
219
contentType = property(_get_contentType, _set_contentType, None,
220
'Content-Type of the response body')
222
class Transaction(object):
224
Encapsulates objects associated with a single transaction (Request,
225
Response, and possibly a Session).
227
Public attributes: (all read-only)
228
request - Request object.
229
response - Response object.
231
If Publisher sits on top of SessionMiddleware, the public API of
232
SessionService is also available through the Transaction object.
234
_requestClass = Request
235
_responseClass = Response
237
def __init__(self, publisher, environ):
238
self._publisher = publisher
239
self._request = self._requestClass(self, environ)
240
self._response = self._responseClass(self)
243
self._sessionService = environ.get('com.saddi.service.session')
244
if self._sessionService is not None:
245
self.encodeURL = self._sessionService.encodeURL
247
def _get_request(self):
249
request = property(_get_request, None, None,
250
"Request associated with this Transaction")
252
def _get_response(self):
253
return self._response
254
response = property(_get_response, None, None,
255
"Response associated with this Transaction")
257
# Export SessionService API
259
def _get_session(self):
260
assert self._sessionService is not None, 'No session service found'
261
return self._sessionService.session
262
session = property(_get_session, None, None,
263
'Returns the Session object associated with this '
266
def _get_hasSession(self):
267
assert self._sessionService is not None, 'No session service found'
268
return self._sessionService.hasSession
269
hasSession = property(_get_hasSession, None, None,
270
'True if a Session currently exists for this client')
272
def _get_isSessionNew(self):
273
assert self._sessionService is not None, 'No session service found'
274
return self._sessionService.isSessionNew
275
isSessionNew = property(_get_isSessionNew, None, None,
276
'True if the Session was created in this '
279
def _get_hasSessionExpired(self):
280
assert self._sessionService is not None, 'No session service found'
281
return self._sessionService.hasSessionExpired
282
hasSessionExpired = property(_get_hasSessionExpired, None, None,
283
'True if the client was associated with a '
284
'non-existent Session')
286
def _get_encodesSessionInURL(self):
287
assert self._sessionService is not None, 'No session service found'
288
return self._sessionService.encodesSessionInURL
289
def _set_encodesSessionInURL(self, value):
290
assert self._sessionService is not None, 'No session service found'
291
self._sessionService.encodesSessionInURL = value
292
encodesSessionInURL = property(_get_encodesSessionInURL,
293
_set_encodesSessionInURL, None,
294
'True if the Session ID should be encoded '
299
Called before resolved function is invoked. If overridden,
300
super's prepare() MUST be called and it must be called first.
302
# Pass form values as keyword arguments.
303
args = dict(self._request.form)
305
# Pass Transaction to function if it wants it.
306
args['transaction'] = args['trans'] = self
310
def call(self, func, args):
312
May be overridden to provide custom exception handling and/or
313
per-request additions, e.g. opening a database connection,
314
starting a transaction, etc.
316
# Trim down keywords to only what the callable expects.
317
expected, varkw = getexpected(func)
318
trimkw(args, expected, varkw)
324
Execute the function, doing any Action processing and also
325
post-processing the function/Action's return value.
329
result = self.call(func, self._args)
331
# Caught an Action, have it do whatever it does
334
response = self._response
335
headers = response.headers
337
if result is not None:
338
if type(result) in types.StringTypes:
339
assert type(result) is str, 'result cannot be unicode!'
341
# Set Content-Length, if needed
342
if not headers.has_key('Content-Length'):
343
headers['Content-Length'] = str(len(result))
345
# Wrap in list for WSGI
346
response.body = [result]
352
raise AssertionError, 'result not iterable!'
353
response.body = result
355
# If result was None, assume response.body was appropriately set.
359
Called after resolved function returns, but before response is
360
sent. If overridden, super's finish() should be called last.
362
response = self._response
363
headers = response.headers
365
# Copy cookie to headers.
366
items = response.cookie.items()
368
for name,morsel in items:
369
headers.add('Set-Cookie', morsel.OutputString())
371
# If there is a Status header, transfer its value to response.status.
372
# It must not remain in the headers!
373
status = headers.get('Status', NoDefault)
374
if status is not NoDefault:
375
del headers['Status']
376
response.status = status
378
code = int(response.status[:3])
379
# Check if it's a response that must not include a body.
380
# (See 4.3 in RFC2068.)
381
if code / 100 == 1 or code in (204, 304):
382
# Remove any trace of the response body, if that's the case.
383
for header,value in headers.items():
384
if header.lower().startswith('content-'):
388
if self._request.method == 'HEAD':
389
# HEAD reponse must not return a body (but the headers must be
393
# Add Content-Type, if missing.
394
if not headers.has_key('Content-Type'):
395
headers['Content-Type'] = response.contentType
397
# If we have a close() method, ensure that it is called.
398
if hasattr(self, 'close'):
399
response.body = _addClose(response.body, self.close)
401
class Publisher(object):
403
WSGI application that publishes Python functions as web pages. Constructor
404
takes an instance of a concrete subclass of Resolver, which is responsible
405
for mapping requests to functions.
407
Query string/POST data values are passed to the function as keyword
408
arguments. If the function does not support variable keywords (e.g.
409
does not have a ** parameter), the function will only be passed
410
keywords which it expects. It is recommended that all keyword parameters
411
have defaults so that missing form data does not raise an exception.
413
A Transaction instance is always passed to the function via the
414
"transaction" or "trans" keywords. See the Transaction, Request, and
417
Valid return types for the function are: a string, None, or an iterable
418
that yields strings. If returning None, it is expected that Response.body
419
has been appropriately set. (It must be an iterable that yields strings.)
421
An instance of Publisher itself is the WSGI application.
423
_transactionClass = Transaction
425
def __init__(self, resolver, transactionClass=None, error404=None):
426
self._resolver = resolver
428
if transactionClass is not None:
429
self._transactionClass = transactionClass
431
if error404 is not None:
432
self._error404 = error404
434
def _get_resolver(self):
435
return self._resolver
436
resolver = property(_get_resolver, None, None,
437
'Associated Resolver for this Publisher')
439
def __call__(self, environ, start_response):
441
WSGI application interface. Creates a Transaction (which does most
442
of the work) and sends the response.
444
# Set up a Transaction.
445
transaction = self._transactionClass(self, environ)
447
# Make any needed preparations.
448
transaction.prepare()
453
# Attempt to resolve the function.
454
func = self._resolver.resolve(transaction.request,
457
func = self._error404
461
transaction.run(func)
462
except InternalRedirect, r:
463
# Internal redirect. Set up environment and resolve again.
464
environ['SCRIPT_NAME'] = transaction.request.publisherScriptName
465
environ['PATH_INFO'] = r.pathInfo
470
# Give Transaction a chance to do modify/add to the response.
473
# Transform headers into a list. (Need to pay attention to
476
for key,value in transaction.response.headers.items():
477
if type(value) is list:
479
responseHeaders.append((key, v))
481
responseHeaders.append((key, value))
483
start_response(transaction.response.status, responseHeaders)
484
return transaction.response.body
486
def _error404(self, transaction):
487
"""Error page to display when resolver fails."""
488
transaction.response.status = '404 Not Found'
489
request_uri = transaction.request.environ.get('REQUEST_URI')
490
if request_uri is None:
491
request_uri = transaction.request.environ.get('SCRIPT_NAME', '') + \
492
transaction.request.environ.get('PATH_INFO', '')
493
return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
495
<title>404 Not Found</title>
498
The requested URL %s was not found on this server.<p>
501
""" % (request_uri, transaction.request.environ.get('SERVER_SIGNATURE', ''))]
505
Wrapper so we can identify uploaded files.
507
def __init__(self, field):
508
self.filename = field.filename
509
self.file = field.file
510
self.type = field.type
511
self.type_options = field.type_options
512
self.headers = field.headers
514
class Action(Exception):
516
Abstract base class for 'Actions', which are just exceptions.
517
Within Publisher, Actions have no inherent meaning and are used
518
as a shortcut to perform specific actions where it's ok for a
519
function to abruptly halt. (Like redirects or displaying an
522
I don't know if using exceptions this way is good form (something
523
tells me no ;) Their usage isn't really required, but they're
524
convenient in some situations.
526
def run(self, transaction):
527
"""Override to perform your action."""
528
raise NotImplementedError, self.__class__.__name__ + '.run'
530
class Redirect(Action):
532
Redirect to the given URL.
534
def __init__(self, url, permanent=False):
536
self._permanent = permanent
538
def run(self, transaction):
539
response = transaction.response
540
response.status = self._permanent and '301 Moved Permanently' or \
541
'302 Moved Temporarily'
542
response.headers.reset()
543
response.headers['Location'] = self._url
544
response.contentType = 'text/html'
545
return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
550
<p>The document has moved <a href="%s">here</a>.</p>
553
""" % (response.status, self._url,
554
transaction.request.environ.get('SERVER_SIGNATURE', ''))]
556
class InternalRedirect(Exception):
558
An internal redirect using a new PATH_INFO (relative to Publisher's
561
When handling an InternalRedirect, the behavior of all included
562
Resolvers is to expose a larger set of callables (that would normally
563
be hidden). Therefore, it is important that you set pathInfo securely -
564
preferably, it should not depend on any data from the request. Ideally,
565
pathInfo should be a constant string.
567
def __init__(self, pathInfo):
568
self.pathInfo = pathInfo
570
class FieldStorage(cgi.FieldStorage):
571
def __init__(self, *args, **kw):
573
cgi.FieldStorage only parses the query string during a GET or HEAD
576
cgi.FieldStorage.__init__(self, *args, **kw)
578
environ = kw.get('environ') or os.environ
579
method = environ.get('REQUEST_METHOD', 'GET').upper()
580
if method not in ('GET', 'HEAD'): # cgi.FieldStorage already parsed?
581
qs = environ.get('QUERY_STRING')
583
if self.list is None:
585
for key,value in cgi.parse_qsl(qs, self.keep_blank_values,
586
self.strict_parsing):
587
self.list.append(cgi.MiniFieldStorage(key, value))
589
class header_dict(dict):
591
This is essentially a case-insensitive dictionary, with some additions
592
geared towards supporting HTTP headers (like __str__(), add(), and
595
def __init__(self, val=None):
597
If initialized with an existing dictionary, calling reset() will
598
reset our contents back to that initial dictionary.
605
self._reset_state = dict(val)
607
def __contains__(self, key):
608
return key.lower() in self._keymap
610
def __delitem__(self, key):
611
lower_key = key.lower()
612
real_key = self._keymap.get(lower_key)
615
del self._keymap[lower_key]
616
dict.__delitem__(self, real_key)
618
def __getitem__(self, key):
619
lower_key = key.lower()
620
real_key = self._keymap.get(lower_key)
623
return dict.__getitem__(self, real_key)
626
"""Output as HTTP headers."""
628
for k,v in self.items():
631
s += '%s: %s\n' % (k, i)
633
s += '%s: %s\n' % (k, v)
636
def __setitem__(self, key, value):
637
lower_key = key.lower()
638
real_key = self._keymap.get(lower_key)
640
self._keymap[lower_key] = key
641
dict.__setitem__(self, key, value)
643
dict.__setitem__(self, real_key, value)
650
c = self.__class__(self)
651
c._reset_state = self._reset_state
654
def get(self, key, failobj=None):
655
lower_key = key.lower()
656
real_key = self._keymap.get(lower_key)
659
return dict.__getitem__(self, real_key)
661
def has_key(self, key):
662
return key.lower() in self._keymap
664
def setdefault(self, key, failobj=None):
665
lower_key = key.lower()
666
real_key = self._keymap.get(lower_key)
668
self._keymap[lower_key] = key
669
dict.__setitem__(self, key, failobj)
672
return dict.__getitem__(self, real_key)
675
for k,v in d.items():
678
def add(self, key, value):
680
Add a new header value. Does not overwrite previous value of header
681
(in contrast to __setitem__()).
683
if self.has_key(key):
684
if type(self[key]) is list:
685
self[key].append(value)
687
self[key] = [self[key], value]
692
"""Reset contents to that at the time this instance was created."""
694
self.update(self._reset_state)
696
def _addClose(appIter, closeFunc):
698
Wraps an iterator so that its close() method calls closeFunc. Respects
699
the existence of __len__ and the iterator's own close() method.
701
Need to use metaclass magic because __len__ and next are not
702
recognized unless they're part of the class. (Can't assign at
705
class metaIterWrapper(type):
706
def __init__(cls, name, bases, clsdict):
707
super(metaIterWrapper, cls).__init__(name, bases, clsdict)
708
if hasattr(appIter, '__len__'):
709
cls.__len__ = appIter.__len__
710
cls.next = iter(appIter).next
711
if hasattr(appIter, 'close'):
717
cls.close = closeFunc
719
class iterWrapper(object):
720
__metaclass__ = metaIterWrapper
726
# Utilities which may be useful outside of Publisher? Perhaps for decorators...
728
def getexpected(func):
730
Returns as a 2-tuple the passed in object's expected arguments and
731
whether or not it accepts variable keywords.
733
assert callable(func), 'object not callable'
735
if not inspect.isclass(func):
736
# At this point, we assume func is either a function, method, or
738
if not inspect.isfunction(func) and not inspect.ismethod(func):
739
func = getattr(func, '__call__') # When would this fail?
741
argspec = inspect.getargspec(func)
742
expected, varkw = argspec[0], argspec[2] is not None
743
if inspect.ismethod(func):
744
expected = expected[1:]
746
# A class. Try to figure out the calling conventions of the
748
init = getattr(func, '__init__', None)
749
# Sigh, this is getting into the realm of black magic...
750
if init is not None and inspect.ismethod(init):
751
argspec = inspect.getargspec(init)
752
expected, varkw = argspec[0], argspec[2] is not None
753
expected = expected[1:]
755
expected, varkw = [], False
757
return expected, varkw
759
def trimkw(kw, expected, varkw):
761
If necessary, trims down a dictionary of keyword arguments to only
764
if not varkw: # Trimming only necessary if it doesn't accept variable kw
765
for name in kw.keys():
766
if name not in expected: