2 Web API (wrapper around WSGI)
10 "setcookie", "cookies",
15 "OK", "Created", "Accepted",
16 "ok", "created", "accepted",
18 # 301, 302, 303, 304, 307
19 "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect",
20 "redirect", "found", "seeother", "notmodified", "tempredirect",
22 # 400, 401, 403, 404, 405, 406, 409, 410, 412, 415
23 "BadRequest", "Unauthorized", "Forbidden", "NotFound", "NoMethod", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", "UnsupportedMediaType",
24 "badrequest", "unauthorized", "forbidden", "notfound", "nomethod", "notacceptable", "conflict", "gone", "preconditionfailed", "unsupportedmediatype",
31 import sys, cgi, Cookie, pprint, urlparse, urllib
32 from utils import storage, storify, threadeddict, dictadd, intget, safestr
36 A configuration object for various aspects of web.py.
39 : when True, enables reloading, disabled template caching and sets internalerror to debugerror.
42 class HTTPError(Exception):
43 def __init__(self, status, headers={}, data=""):
45 for k, v in headers.items():
48 Exception.__init__(self, status)
50 def _status_code(status, data=None, classname=None, docstring=None):
52 data = status.split(" ", 1)[1]
53 classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified
54 docstring = docstring or '`%s` status' % status
56 def __init__(self, data=data, headers={}):
57 HTTPError.__init__(self, status, headers, data)
59 # trick to create class dynamically with dynamic docstring.
60 return type(classname, (HTTPError, object), {
65 ok = OK = _status_code("200 OK", data="")
66 created = Created = _status_code("201 Created")
67 accepted = Accepted = _status_code("202 Accepted")
69 class Redirect(HTTPError):
70 """A `301 Moved Permanently` redirect."""
71 def __init__(self, url, status='301 Moved Permanently', absolute=False):
73 Returns a `status` redirect to the new URL.
74 `url` is joined with the base URL so that things like
75 `redirect("about") will work properly.
77 newloc = urlparse.urljoin(ctx.path, url)
79 if newloc.startswith('/'):
84 newloc = home + newloc
87 'Content-Type': 'text/html',
90 HTTPError.__init__(self, status, headers, "")
94 class Found(Redirect):
95 """A `302 Found` redirect."""
96 def __init__(self, url, absolute=False):
97 Redirect.__init__(self, url, '302 Found', absolute=absolute)
101 class SeeOther(Redirect):
102 """A `303 See Other` redirect."""
103 def __init__(self, url, absolute=False):
104 Redirect.__init__(self, url, '303 See Other', absolute=absolute)
108 class NotModified(HTTPError):
109 """A `304 Not Modified` status."""
111 HTTPError.__init__(self, "304 Not Modified")
113 notmodified = NotModified
115 class TempRedirect(Redirect):
116 """A `307 Temporary Redirect` redirect."""
117 def __init__(self, url, absolute=False):
118 Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
120 tempredirect = TempRedirect
122 class BadRequest(HTTPError):
123 """`400 Bad Request` error."""
124 message = "bad request"
125 def __init__(self, message=None):
126 status = "400 Bad Request"
127 headers = {'Content-Type': 'text/html'}
128 HTTPError.__init__(self, status, headers, message or self.message)
130 badrequest = BadRequest
132 class Unauthorized(HTTPError):
133 """`401 Unauthorized` error."""
134 message = "unauthorized"
136 status = "401 Unauthorized"
137 headers = {'Content-Type': 'text/html'}
138 HTTPError.__init__(self, status, headers, self.message)
140 unauthorized = Unauthorized
142 class Forbidden(HTTPError):
143 """`403 Forbidden` error."""
144 message = "forbidden"
146 status = "403 Forbidden"
147 headers = {'Content-Type': 'text/html'}
148 HTTPError.__init__(self, status, headers, self.message)
150 forbidden = Forbidden
152 class _NotFound(HTTPError):
153 """`404 Not Found` error."""
154 message = "not found"
155 def __init__(self, message=None):
156 status = '404 Not Found'
157 headers = {'Content-Type': 'text/html'}
158 HTTPError.__init__(self, status, headers, message or self.message)
160 def NotFound(message=None):
161 """Returns HTTPError with '404 Not Found' error from the active application.
164 return _NotFound(message)
165 elif ctx.get('app_stack'):
166 return ctx.app_stack[-1].notfound()
172 class NoMethod(HTTPError):
173 """A `405 Method Not Allowed` error."""
174 def __init__(self, cls=None):
175 status = '405 Method Not Allowed'
177 headers['Content-Type'] = 'text/html'
179 methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
181 methods = [method for method in methods if hasattr(cls, method)]
183 headers['Allow'] = ', '.join(methods)
185 HTTPError.__init__(self, status, headers, data)
189 class NotAcceptable(HTTPError):
190 """`406 Not Acceptable` error."""
191 message = "not acceptable"
193 status = "406 Not Acceptable"
194 headers = {'Content-Type': 'text/html'}
195 HTTPError.__init__(self, status, headers, self.message)
197 notacceptable = NotAcceptable
199 class Conflict(HTTPError):
200 """`409 Conflict` error."""
203 status = "409 Conflict"
204 headers = {'Content-Type': 'text/html'}
205 HTTPError.__init__(self, status, headers, self.message)
209 class Gone(HTTPError):
210 """`410 Gone` error."""
214 headers = {'Content-Type': 'text/html'}
215 HTTPError.__init__(self, status, headers, self.message)
219 class PreconditionFailed(HTTPError):
220 """`412 Precondition Failed` error."""
221 message = "precondition failed"
223 status = "412 Precondition Failed"
224 headers = {'Content-Type': 'text/html'}
225 HTTPError.__init__(self, status, headers, self.message)
227 preconditionfailed = PreconditionFailed
229 class UnsupportedMediaType(HTTPError):
230 """`415 Unsupported Media Type` error."""
231 message = "unsupported media type"
233 status = "415 Unsupported Media Type"
234 headers = {'Content-Type': 'text/html'}
235 HTTPError.__init__(self, status, headers, self.message)
237 unsupportedmediatype = UnsupportedMediaType
239 class _InternalError(HTTPError):
240 """500 Internal Server Error`."""
241 message = "internal server error"
243 def __init__(self, message=None):
244 status = '500 Internal Server Error'
245 headers = {'Content-Type': 'text/html'}
246 HTTPError.__init__(self, status, headers, message or self.message)
248 def InternalError(message=None):
249 """Returns HTTPError with '500 internal error' error from the active application.
252 return _InternalError(message)
253 elif ctx.get('app_stack'):
254 return ctx.app_stack[-1].internalerror()
256 return _InternalError()
258 internalerror = InternalError
260 def header(hdr, value, unique=False):
262 Adds the header `hdr: value` with the response.
264 If `unique` is True and a header with that name already exists,
265 it doesn't add a new one.
267 hdr, value = safestr(hdr), safestr(value)
268 # protection against HTTP response splitting attack
269 if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
270 raise ValueError, 'invalid characters in header'
273 for h, v in ctx.headers:
274 if h.lower() == hdr.lower(): return
276 ctx.headers.append((hdr, value))
278 def rawinput(method=None):
279 """Returns storage object with GET or POST arguments.
281 method = method or "both"
282 from cStringIO import StringIO
285 # hack to make web.input work with enctype='text/plain.
289 return dict([(k, fs[k]) for k in fs.keys()])
294 if method.lower() in ['both', 'post', 'put']:
295 if e['REQUEST_METHOD'] in ['POST', 'PUT']:
296 if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
297 # since wsgi.input is directly passed to cgi.FieldStorage,
298 # it can not be called multiple times. Saving the FieldStorage
299 # object in ctx to allow calling web.input multiple times.
300 a = ctx.get('_fieldstorage')
303 a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
304 ctx._fieldstorage = a
306 fp = StringIO(data())
307 a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
310 if method.lower() in ['both', 'get']:
311 e['REQUEST_METHOD'] = 'GET'
312 b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
314 def process_fieldstorage(fs):
315 if isinstance(fs, list):
316 return [process_fieldstorage(x) for x in fs]
317 elif fs.filename is None:
322 return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()])
324 def input(*requireds, **defaults):
326 Returns a `storage` object with the GET and POST arguments.
327 See `storify` for how `requireds` and `defaults` work.
329 _method = defaults.pop('_method', 'both')
330 out = rawinput(_method)
332 defaults.setdefault('_unicode', True) # force unicode conversion by default.
333 return storify(out, *requireds, **defaults)
338 """Returns the data sent with the request."""
339 if 'data' not in ctx:
340 cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
341 ctx.data = ctx.env['wsgi.input'].read(cl)
344 def setcookie(name, value, expires='', domain=None,
345 secure=False, httponly=False, path=None):
347 morsel = Cookie.Morsel()
348 name, value = safestr(name), safestr(value)
349 morsel.set(name, value, urllib.quote(value))
351 expires = -1000000000
352 morsel['expires'] = expires
353 morsel['path'] = path or ctx.homepath+'/'
355 morsel['domain'] = domain
357 morsel['secure'] = secure
358 value = morsel.OutputString()
360 value += '; httponly'
361 header('Set-Cookie', value)
363 def decode_cookie(value):
364 r"""Safely decodes a cookie value to unicode.
366 Tries us-ascii, utf-8 and io8859 encodings, in that order.
368 >>> decode_cookie('')
370 >>> decode_cookie('asdf')
372 >>> decode_cookie('foo \xC3\xA9 bar')
374 >>> decode_cookie('foo \xE9 bar')
378 # First try plain ASCII encoding
379 return unicode(value, 'us-ascii')
381 # Then try UTF-8, and if that fails, ISO8859
383 return unicode(value, 'utf-8')
385 return unicode(value, 'iso8859', 'ignore')
387 def parse_cookies(http_cookie):
388 r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
390 >>> sorted(parse_cookies('').items())
392 >>> sorted(parse_cookies('a=1').items())
394 >>> sorted(parse_cookies('a=1%202').items())
396 >>> sorted(parse_cookies('a=Z%C3%A9Z').items())
397 [('a', 'Z\xc3\xa9Z')]
398 >>> sorted(parse_cookies('a=1; b=2; c=3').items())
399 [('a', '1'), ('b', '2'), ('c', '3')]
400 >>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
401 [('a', '1'), ('b', 'w('), ('c', '3')]
402 >>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
403 [('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
405 >>> sorted(parse_cookies('keebler=E=mc2').items())
406 [('keebler', 'E=mc2')]
407 >>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
408 [('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
410 #print "parse_cookies"
411 if '"' in http_cookie:
412 # HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
413 cookie = Cookie.SimpleCookie()
415 cookie.load(http_cookie)
416 except Cookie.CookieError:
417 # If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
418 # first splitting on ';' and loading each attr=value pair separately
419 cookie = Cookie.SimpleCookie()
420 for attr_value in http_cookie.split(';'):
422 cookie.load(attr_value)
423 except Cookie.CookieError:
425 cookies = dict((k, urllib.unquote(v.value)) for k, v in cookie.iteritems())
427 # HTTP_COOKIE doesn't have quotes, use fast cookie parsing
429 for key_value in http_cookie.split(';'):
430 key_value = key_value.split('=', 1)
431 if len(key_value) == 2:
432 key, value = key_value
433 cookies[key.strip()] = urllib.unquote(value.strip())
436 def cookies(*requireds, **defaults):
437 r"""Returns a `storage` object with all the request cookies in it.
439 See `storify` for how `requireds` and `defaults` work.
441 This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
444 The values are converted to unicode if _unicode=True is passed.
446 # If _unicode=True is specified, use decode_cookie to convert cookie value to unicode
447 if defaults.get("_unicode") is True:
448 defaults['_unicode'] = decode_cookie
450 # parse cookie string and cache the result for next time.
451 if '_parsed_cookies' not in ctx:
452 http_cookie = ctx.env.get("HTTP_COOKIE", "")
453 ctx._parsed_cookies = parse_cookies(http_cookie)
456 return storify(ctx._parsed_cookies, *requireds, **defaults)
463 Prints a prettyprinted version of `args` to stderr.
466 out = ctx.environ['wsgi.errors']
470 print >> out, pprint.pformat(arg)
475 out = ctx.environ['wsgi.errors']
479 debug.write = _debugwrite
481 ctx = context = threadeddict()
484 A `storage` object containing various information about the request:
486 `environ` (aka `env`)
487 : A dictionary containing the standard WSGI environment variables.
490 : The domain (`Host` header) requested by the user.
493 : The base path for the application.
496 : The IP address of the requester.
499 : The HTTP method used.
505 : If there are no query arguments, the empty string. Otherwise, a `?` followed
509 : The full path requested, including query arguments (`== path + query`).
513 `status` (default: "200 OK")
514 : The status code to be used in the response.
517 : A list of 2-tuples to be used in the response.
520 : A string to be used as the response.
523 if __name__ == "__main__":