3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 """An extremely simple WSGI web application framework.
20 This module exports three primary classes: Request, Response, and
21 RequestHandler. You implement a web application by subclassing RequestHandler.
22 As WSGI requests come in, they are passed to instances of your RequestHandlers.
23 The RequestHandler class provides access to the easy-to-use Request and
24 Response objects so you can interpret the request and write the response with
25 no knowledge of the esoteric WSGI semantics. Here is a simple example:
27 from google.appengine.ext import webapp
28 import wsgiref.simple_server
30 class MainPage(webapp.RequestHandler):
32 self.response.out.write(
33 '<html><body><form action="/hello" method="post">'
34 'Name: <input name="name" type="text" size="20"> '
35 '<input type="submit" value="Say Hello"></form></body></html>')
37 class HelloPage(webapp.RequestHandler):
39 self.response.headers['Content-Type'] = 'text/plain'
40 self.response.out.write('Hello, %s' % self.request.get('name'))
42 application = webapp.WSGIApplication([
47 server = wsgiref.simple_server.make_server('', 8080, application)
48 print 'Serving on port 8080...'
49 server.serve_forever()
51 The WSGIApplication class maps URI regular expressions to your RequestHandler
52 classes. It is a WSGI-compatible application object, so you can use it in
53 conjunction with wsgiref to make your web application into, e.g., a CGI
54 script or a simple HTTP server, as in the example above.
56 The framework does not support streaming output. All output from a response
57 is stored in memory before it is written.
69 import wsgiref
.handlers
70 import wsgiref
.headers
73 wsgiref
.handlers
.BaseHandler
.os_environ
= {}
75 RE_FIND_GROUPS
= re
.compile('\(.*?\)')
76 _CHARSET_RE
= re
.compile(r
';\s*charset=([^;\s]*)', re
.I
)
78 class Error(Exception):
79 """Base of all exceptions in the webapp module."""
83 class NoUrlFoundError(Error
):
84 """Thrown when RequestHandler.get_url() fails."""
88 class Request(webob
.Request
):
89 """Abstraction for an HTTP request.
92 uri: the complete URI requested by the user
93 scheme: 'http' or 'https'
94 host: the host, including the port
95 path: the path up to the ';' or '?' in the URL
96 parameters: the part of the URL between the ';' and the '?', if any
97 query: the part of the URL after the '?'
99 You can access parsed query and POST values with the get() method; do not
100 parse the query string yourself.
103 request_body_tempfile_limit
= 0
105 uri
= property(lambda self
: self
.url
)
106 query
= property(lambda self
: self
.query_string
)
108 def __init__(self
, environ
):
109 """Constructs a Request object from a WSGI environment.
111 If the charset isn't specified in the Content-Type header, defaults
115 environ: A WSGI-compliant environment dictionary.
117 match
= _CHARSET_RE
.search(environ
.get('CONTENT_TYPE', ''))
119 charset
= match
.group(1).lower()
123 webob
.Request
.__init
__(self
, environ
, charset
=charset
,
124 unicode_errors
= 'ignore', decode_param_names
=True)
126 def get(self
, argument_name
, default_value
='', allow_multiple
=False):
127 """Returns the query or POST argument with the given name.
129 We parse the query string and POST payload lazily, so this will be a
130 slower operation on the first call.
133 argument_name: the name of the query or POST argument
134 default_value: the value to return if the given argument is not present
135 allow_multiple: return a list of values with the given name (deprecated)
138 If allow_multiple is False (which it is by default), we return the first
139 value with the given name given in the request. If it is True, we always
142 param_value
= self
.get_all(argument_name
)
146 if len(param_value
) > 0:
147 return param_value
[0]
151 def get_all(self
, argument_name
):
152 """Returns a list of query or POST arguments with the given name.
154 We parse the query string and POST payload lazily, so this will be a
155 slower operation on the first call.
158 argument_name: the name of the query or POST argument
161 A (possibly empty) list of values.
164 argument_name
= argument_name
.encode(self
.charset
)
166 param_value
= self
.params
.getall(argument_name
)
168 for i
in xrange(len(param_value
)):
169 if isinstance(param_value
[i
], cgi
.FieldStorage
):
170 param_value
[i
] = param_value
[i
].value
175 """Returns a list of the arguments provided in the query and/or POST.
177 The return value is a list of strings.
179 return list(set(self
.params
.keys()))
181 def get_range(self
, name
, min_value
=None, max_value
=None, default
=0):
182 """Parses the given int argument, limiting it to the given range.
185 name: the name of the argument
186 min_value: the minimum int value of the argument (if any)
187 max_value: the maximum int value of the argument (if any)
188 default: the default value of the argument if it is not given
191 An int within the given range for the argument
194 value
= int(self
.get(name
, default
))
197 if max_value
!= None:
198 value
= min(value
, max_value
)
199 if min_value
!= None:
200 value
= max(value
, min_value
)
204 class Response(object):
205 """Abstraction for an HTTP response.
208 out: file pointer for the output stream
209 headers: wsgiref.headers.Headers instance representing the output headers
212 """Constructs a response with the default settings."""
213 self
.out
= StringIO
.StringIO()
214 self
.__wsgi
_headers
= []
215 self
.headers
= wsgiref
.headers
.Headers(self
.__wsgi
_headers
)
216 self
.headers
['Content-Type'] = 'text/html; charset=utf-8'
217 self
.headers
['Cache-Control'] = 'no-cache'
220 def set_status(self
, code
, message
=None):
221 """Sets the HTTP status code of this response.
224 message: the HTTP status string to use
226 If no status string is given, we use the default from the HTTP/1.1
230 message
= Response
.http_status_message(code
)
231 self
.__status
= (code
, message
)
234 """Clears all data written to the output stream so that it is empty."""
238 def wsgi_write(self
, start_response
):
239 """Writes this response using WSGI semantics with the given WSGI function.
242 start_response: the WSGI-compatible start_response function
244 body
= self
.out
.getvalue()
245 if isinstance(body
, unicode):
246 body
= body
.encode('utf-8')
247 elif self
.headers
.get('Content-Type', '').endswith('; charset=utf-8'):
250 except UnicodeError, e
:
251 logging
.warning('Response written is not UTF-8: %s', e
)
253 if (self
.headers
.get('Cache-Control') == 'no-cache' and
254 not self
.headers
.get('Expires')):
255 self
.headers
['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
256 self
.headers
['Content-Length'] = str(len(body
))
257 write
= start_response('%d %s' % self
.__status
, self
.__wsgi
_headers
)
261 def http_status_message(code
):
262 """Returns the default HTTP status message for the given code.
265 code: the HTTP code for which we want a message
267 if not Response
.__HTTP
_STATUS
_MESSAGES
.has_key(code
):
268 raise Error('Invalid HTTP status code: %d' % code
)
269 return Response
.__HTTP
_STATUS
_MESSAGES
[code
]
270 http_status_message
= staticmethod(http_status_message
)
272 __HTTP_STATUS_MESSAGES
= {
274 101: 'Switching Protocols',
278 203: 'Non-Authoritative Information',
280 205: 'Reset Content',
281 206: 'Partial Content',
282 300: 'Multiple Choices',
283 301: 'Moved Permanently',
284 302: 'Moved Temporarily',
289 307: 'Temporary Redirect',
292 402: 'Payment Required',
295 405: 'Method Not Allowed',
296 406: 'Not Acceptable',
297 407: 'Proxy Authentication Required',
298 408: 'Request Time-out',
301 411: 'Length Required',
302 412: 'Precondition Failed',
303 413: 'Request Entity Too Large',
304 414: 'Request-URI Too Large',
305 415: 'Unsupported Media Type',
306 416: 'Requested Range Not Satisfiable',
307 417: 'Expectation Failed',
308 500: 'Internal Server Error',
309 501: 'Not Implemented',
311 503: 'Service Unavailable',
312 504: 'Gateway Time-out',
313 505: 'HTTP Version not supported'
317 class RequestHandler(object):
318 """Our base HTTP request handler. Clients should subclass this class.
320 Subclasses should override get(), post(), head(), options(), etc to handle
321 different HTTP methods.
323 def initialize(self
, request
, response
):
324 """Initializes this request handler with the given Request and Response."""
325 self
.request
= request
326 self
.response
= response
328 def get(self
, *args
):
329 """Handler method for GET requests."""
332 def post(self
, *args
):
333 """Handler method for POST requests."""
336 def head(self
, *args
):
337 """Handler method for HEAD requests."""
340 def options(self
, *args
):
341 """Handler method for OPTIONS requests."""
344 def put(self
, *args
):
345 """Handler method for PUT requests."""
348 def delete(self
, *args
):
349 """Handler method for DELETE requests."""
352 def trace(self
, *args
):
353 """Handler method for TRACE requests."""
356 def error(self
, code
):
357 """Clears the response output stream and sets the given HTTP error code.
360 code: the HTTP status error code (e.g., 501)
362 self
.response
.set_status(code
)
363 self
.response
.clear()
365 def redirect(self
, uri
, permanent
=False):
366 """Issues an HTTP redirect to the given relative URL.
369 uri: a relative or absolute URI (e.g., '../flowers.html')
370 permanent: if true, we use a 301 redirect instead of a 302 redirect
373 self
.response
.set_status(301)
375 self
.response
.set_status(302)
376 absolute_url
= urlparse
.urljoin(self
.request
.uri
, uri
)
377 self
.response
.headers
['Location'] = str(absolute_url
)
378 self
.response
.clear()
380 def handle_exception(self
, exception
, debug_mode
):
381 """Called if this handler throws an exception during execution.
383 The default behavior is to call self.error(500) and print a stack trace
384 if debug_mode is True.
387 exception: the exception that was thrown
388 debug_mode: True if the web application is running in debug mode
391 logging
.exception(exception
)
393 lines
= ''.join(traceback
.format_exception(*sys
.exc_info()))
394 self
.response
.clear()
395 self
.response
.out
.write('<pre>%s</pre>' % (cgi
.escape(lines
, quote
=True)))
398 def get_url(cls
, *args
, **kargs
):
399 """Returns the url for the given handler.
401 The default implementation uses the patterns passed to the active
402 WSGIApplication and the django urlresolvers module to create a url.
403 However, it is different from urlresolvers.reverse() in the following ways:
404 - It does not try to resolve handlers via module loading
405 - It does not support named arguments
406 - It performs some post-prosessing on the url to remove some regex
407 operators that urlresolvers.reverse_helper() seems to miss.
408 - It will try to fill in the left-most missing arguments with the args
409 used in the active request.
412 args: Parameters for the url pattern's groups.
413 kwargs: Optionally contains 'implicit_args' that can either be a boolean
414 or a tuple. When it is True, it will use the arguments to the
415 active request as implicit arguments. When it is False (default),
416 it will not use any implicit arguments. When it is a tuple, it
417 will use the tuple as the implicit arguments.
418 the left-most args if some are missing from args.
421 The url for this handler/args combination.
424 NoUrlFoundError: No url pattern for this handler has the same
425 number of args that were passed in.
428 app
= WSGIApplication
.active_instance
429 pattern_map
= app
._pattern
_map
431 implicit_args
= kargs
.get('implicit_args', ())
432 if implicit_args
== True:
433 implicit_args
= app
.current_request_args
435 min_params
= len(args
)
439 for pattern_tuple
in pattern_map
.get(cls
, ()):
440 num_params_in_pattern
= pattern_tuple
[1]
441 if num_params_in_pattern
< min_params
:
444 if urlresolvers
is None:
445 from django
.core
import urlresolvers
448 num_implicit_args
= max(0, num_params_in_pattern
- len(args
))
449 merged_args
= implicit_args
[:num_implicit_args
] + args
450 url
= urlresolvers
.reverse_helper(pattern_tuple
[0], *merged_args
)
451 url
= url
.replace('\\', '')
452 url
= url
.replace('?', '')
454 except urlresolvers
.NoReverseMatch
:
457 logging
.warning('get_url failed for Handler name: %r, Args: %r',
459 raise NoUrlFoundError
462 class WSGIApplication(object):
463 """Wraps a set of webapp RequestHandlers in a WSGI-compatible application.
465 To use this class, pass a list of (URI regular expression, RequestHandler)
466 pairs to the constructor, and pass the class instance to a WSGI handler.
467 See the example in the module comments for details.
469 The URL mapping is first-match based on the list ordering.
472 REQUEST_CLASS
= Request
473 RESPONSE_CLASS
= Response
475 def __init__(self
, url_mapping
, debug
=False):
476 """Initializes this application with the given URL mapping.
479 url_mapping: list of (URI regular expression, RequestHandler) pairs
480 (e.g., [('/', ReqHan)])
481 debug: if true, we send Python stack traces to the browser on errors
483 self
._init
_url
_mappings
(url_mapping
)
485 WSGIApplication
.active_instance
= self
486 self
.current_request_args
= ()
488 def __call__(self
, environ
, start_response
):
489 """Called by WSGI when a request comes in."""
490 request
= self
.REQUEST_CLASS(environ
)
491 response
= self
.RESPONSE_CLASS()
493 WSGIApplication
.active_instance
= self
497 for regexp
, handler_class
in self
._url
_mapping
:
498 match
= regexp
.match(request
.path
)
500 handler
= handler_class()
501 handler
.initialize(request
, response
)
502 groups
= match
.groups()
505 self
.current_request_args
= groups
509 method
= environ
['REQUEST_METHOD']
512 elif method
== 'POST':
513 handler
.post(*groups
)
514 elif method
== 'HEAD':
515 handler
.head(*groups
)
516 elif method
== 'OPTIONS':
517 handler
.options(*groups
)
518 elif method
== 'PUT':
520 elif method
== 'DELETE':
521 handler
.delete(*groups
)
522 elif method
== 'TRACE':
523 handler
.trace(*groups
)
527 handler
.handle_exception(e
, self
.__debug
)
529 response
.set_status(404)
531 response
.wsgi_write(start_response
)
534 def _init_url_mappings(self
, handler_tuples
):
535 """Initializes the maps needed for mapping urls to handlers and handlers
539 handler_tuples: list of (URI, RequestHandler) pairs.
546 for regexp
, handler
in handler_tuples
:
549 handler_name
= handler
.__name
__
550 except AttributeError:
553 handler_map
[handler_name
] = handler
555 if not regexp
.startswith('^'):
556 regexp
= '^' + regexp
557 if not regexp
.endswith('$'):
560 if regexp
== '^/form$':
561 logging
.warning('The URL "/form" is reserved and will not be matched.')
563 compiled
= re
.compile(regexp
)
564 url_mapping
.append((compiled
, handler
))
566 num_groups
= len(RE_FIND_GROUPS
.findall(regexp
))
567 handler_patterns
= pattern_map
.setdefault(handler
, [])
568 handler_patterns
.append((compiled
, num_groups
))
570 self
._handler
_map
= handler_map
571 self
._pattern
_map
= pattern_map
572 self
._url
_mapping
= url_mapping
574 def get_registered_handler_by_name(self
, handler_name
):
575 """Returns the handler given the handler's name.
577 This uses the application's url mapping.
580 handler_name: The __name__ of a handler to return.
583 The handler with the given name.
586 KeyError: If the handler name is not found in the parent application.
589 return self
._handler
_map
[handler_name
]
591 logging
.error('Handler does not map to any urls: %s', handler_name
)