326 lines
9.2 KiB
Python
326 lines
9.2 KiB
Python
|
"""
|
||
|
Web API (wrapper around WSGI)
|
||
|
(from web.py)
|
||
|
"""
|
||
|
|
||
|
__all__ = [
|
||
|
"config",
|
||
|
"header", "debug",
|
||
|
"input", "data",
|
||
|
"setcookie", "cookies",
|
||
|
"ctx",
|
||
|
"HTTPError",
|
||
|
"BadRequest", "NotFound", "Gone", "InternalError",
|
||
|
"badrequest", "notfound", "gone", "internalerror",
|
||
|
"Redirect", "Found", "SeeOther", "TempRedirect",
|
||
|
"redirect", "found", "seeother", "tempredirect",
|
||
|
"NoMethod", "nomethod",
|
||
|
]
|
||
|
|
||
|
import sys, cgi, Cookie, pprint, urlparse, urllib
|
||
|
from utils import storage, storify, threadeddict, dictadd, intget, utf8
|
||
|
|
||
|
config = storage()
|
||
|
config.__doc__ = """
|
||
|
A configuration object for various aspects of web.py.
|
||
|
|
||
|
`debug`
|
||
|
: when True, enables reloading, disabled template caching and sets internalerror to debugerror.
|
||
|
"""
|
||
|
|
||
|
class HTTPError(Exception):
|
||
|
def __init__(self, status, headers, data=""):
|
||
|
ctx.status = status
|
||
|
for k, v in headers.items():
|
||
|
header(k, v)
|
||
|
self.data = data
|
||
|
Exception.__init__(self, status)
|
||
|
|
||
|
class BadRequest(HTTPError):
|
||
|
"""`400 Bad Request` error."""
|
||
|
message = "bad request"
|
||
|
def __init__(self):
|
||
|
status = "400 Bad Request"
|
||
|
headers = {'Content-Type': 'text/html'}
|
||
|
HTTPError.__init__(self, status, headers, self.message)
|
||
|
|
||
|
badrequest = BadRequest
|
||
|
|
||
|
class _NotFound(HTTPError):
|
||
|
"""`404 Not Found` error."""
|
||
|
message = "not found"
|
||
|
def __init__(self, message=None):
|
||
|
status = '404 Not Found'
|
||
|
headers = {'Content-Type': 'text/html'}
|
||
|
HTTPError.__init__(self, status, headers, message or self.message)
|
||
|
|
||
|
def NotFound(message=None):
|
||
|
"""Returns HTTPError with '404 Not Found' error from the active application.
|
||
|
"""
|
||
|
if message:
|
||
|
return _NotFound(message)
|
||
|
elif ctx.get('app_stack'):
|
||
|
return ctx.app_stack[-1].notfound()
|
||
|
else:
|
||
|
return _NotFound()
|
||
|
|
||
|
notfound = NotFound
|
||
|
|
||
|
class Gone(HTTPError):
|
||
|
"""`410 Gone` error."""
|
||
|
message = "gone"
|
||
|
def __init__(self):
|
||
|
status = '410 Gone'
|
||
|
headers = {'Content-Type': 'text/html'}
|
||
|
HTTPError.__init__(self, status, headers, self.message)
|
||
|
|
||
|
gone = Gone
|
||
|
|
||
|
class Redirect(HTTPError):
|
||
|
"""A `301 Moved Permanently` redirect."""
|
||
|
def __init__(self, url, status='301 Moved Permanently', absolute=False):
|
||
|
"""
|
||
|
Returns a `status` redirect to the new URL.
|
||
|
`url` is joined with the base URL so that things like
|
||
|
`redirect("about") will work properly.
|
||
|
"""
|
||
|
newloc = urlparse.urljoin(ctx.path, url)
|
||
|
|
||
|
if newloc.startswith('/'):
|
||
|
if absolute:
|
||
|
home = ctx.realhome
|
||
|
else:
|
||
|
home = ctx.home
|
||
|
newloc = home + newloc
|
||
|
|
||
|
headers = {
|
||
|
'Content-Type': 'text/html',
|
||
|
'Location': newloc
|
||
|
}
|
||
|
HTTPError.__init__(self, status, headers, "")
|
||
|
|
||
|
redirect = Redirect
|
||
|
|
||
|
class Found(Redirect):
|
||
|
"""A `302 Found` redirect."""
|
||
|
def __init__(self, url, absolute=False):
|
||
|
Redirect.__init__(self, url, '302 Found', absolute=absolute)
|
||
|
|
||
|
found = Found
|
||
|
|
||
|
class SeeOther(Redirect):
|
||
|
"""A `303 See Other` redirect."""
|
||
|
def __init__(self, url, absolute=False):
|
||
|
Redirect.__init__(self, url, '303 See Other', absolute=absolute)
|
||
|
|
||
|
seeother = SeeOther
|
||
|
|
||
|
class TempRedirect(Redirect):
|
||
|
"""A `307 Temporary Redirect` redirect."""
|
||
|
def __init__(self, url, absolute=False):
|
||
|
Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute)
|
||
|
|
||
|
tempredirect = TempRedirect
|
||
|
|
||
|
class NoMethod(HTTPError):
|
||
|
"""A `405 Method Not Allowed` error."""
|
||
|
def __init__(self, cls=None):
|
||
|
status = '405 Method Not Allowed'
|
||
|
headers = {}
|
||
|
headers['Content-Type'] = 'text/html'
|
||
|
|
||
|
methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE']
|
||
|
if cls:
|
||
|
methods = [method for method in methods if hasattr(cls, method)]
|
||
|
|
||
|
headers['Allow'] = ', '.join(methods)
|
||
|
data = None
|
||
|
HTTPError.__init__(self, status, headers, data)
|
||
|
|
||
|
nomethod = NoMethod
|
||
|
|
||
|
class _InternalError(HTTPError):
|
||
|
"""500 Internal Server Error`."""
|
||
|
message = "internal server error"
|
||
|
|
||
|
def __init__(self, message=None):
|
||
|
status = '500 Internal Server Error'
|
||
|
headers = {'Content-Type': 'text/html'}
|
||
|
HTTPError.__init__(self, status, headers, message or self.message)
|
||
|
|
||
|
def InternalError(message=None):
|
||
|
"""Returns HTTPError with '500 internal error' error from the active application.
|
||
|
"""
|
||
|
if message:
|
||
|
return _InternalError(message)
|
||
|
elif ctx.get('app_stack'):
|
||
|
return ctx.app_stack[-1].internalerror()
|
||
|
else:
|
||
|
return _InternalError()
|
||
|
|
||
|
internalerror = InternalError
|
||
|
|
||
|
def header(hdr, value, unique=False):
|
||
|
"""
|
||
|
Adds the header `hdr: value` with the response.
|
||
|
|
||
|
If `unique` is True and a header with that name already exists,
|
||
|
it doesn't add a new one.
|
||
|
"""
|
||
|
hdr, value = utf8(hdr), utf8(value)
|
||
|
# protection against HTTP response splitting attack
|
||
|
if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
|
||
|
raise ValueError, 'invalid characters in header'
|
||
|
|
||
|
if unique is True:
|
||
|
for h, v in ctx.headers:
|
||
|
if h.lower() == hdr.lower(): return
|
||
|
|
||
|
ctx.headers.append((hdr, value))
|
||
|
|
||
|
def input(*requireds, **defaults):
|
||
|
"""
|
||
|
Returns a `storage` object with the GET and POST arguments.
|
||
|
See `storify` for how `requireds` and `defaults` work.
|
||
|
"""
|
||
|
from cStringIO import StringIO
|
||
|
def dictify(fs):
|
||
|
# hack to make web.input work with enctype='text/plain.
|
||
|
if fs.list is None:
|
||
|
fs.list = []
|
||
|
|
||
|
return dict([(k, fs[k]) for k in fs.keys()])
|
||
|
|
||
|
_method = defaults.pop('_method', 'both')
|
||
|
|
||
|
e = ctx.env.copy()
|
||
|
a = b = {}
|
||
|
|
||
|
if _method.lower() in ['both', 'post', 'put']:
|
||
|
if e['REQUEST_METHOD'] in ['POST', 'PUT']:
|
||
|
if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'):
|
||
|
# since wsgi.input is directly passed to cgi.FieldStorage,
|
||
|
# it can not be called multiple times. Saving the FieldStorage
|
||
|
# object in ctx to allow calling web.input multiple times.
|
||
|
a = ctx.get('_fieldstorage')
|
||
|
if not a:
|
||
|
fp = e['wsgi.input']
|
||
|
a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
|
||
|
ctx._fieldstorage = a
|
||
|
else:
|
||
|
fp = StringIO(data())
|
||
|
a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1)
|
||
|
a = dictify(a)
|
||
|
|
||
|
if _method.lower() in ['both', 'get']:
|
||
|
e['REQUEST_METHOD'] = 'GET'
|
||
|
b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
|
||
|
|
||
|
out = dictadd(b, a)
|
||
|
try:
|
||
|
defaults.setdefault('_unicode', True) # force unicode conversion by default.
|
||
|
return storify(out, *requireds, **defaults)
|
||
|
except KeyError:
|
||
|
raise badrequest()
|
||
|
|
||
|
def data():
|
||
|
"""Returns the data sent with the request."""
|
||
|
if 'data' not in ctx:
|
||
|
cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
|
||
|
ctx.data = ctx.env['wsgi.input'].read(cl)
|
||
|
return ctx.data
|
||
|
|
||
|
def setcookie(name, value, expires="", domain=None, secure=False):
|
||
|
"""Sets a cookie."""
|
||
|
if expires < 0:
|
||
|
expires = -1000000000
|
||
|
kargs = {'expires': expires, 'path':'/'}
|
||
|
if domain:
|
||
|
kargs['domain'] = domain
|
||
|
if secure:
|
||
|
kargs['secure'] = secure
|
||
|
# @@ should we limit cookies to a different path?
|
||
|
cookie = Cookie.SimpleCookie()
|
||
|
cookie[name] = urllib.quote(utf8(value))
|
||
|
for key, val in kargs.iteritems():
|
||
|
cookie[name][key] = val
|
||
|
header('Set-Cookie', cookie.items()[0][1].OutputString())
|
||
|
|
||
|
def cookies(*requireds, **defaults):
|
||
|
"""
|
||
|
Returns a `storage` object with all the cookies in it.
|
||
|
See `storify` for how `requireds` and `defaults` work.
|
||
|
"""
|
||
|
cookie = Cookie.SimpleCookie()
|
||
|
cookie.load(ctx.env.get('HTTP_COOKIE', ''))
|
||
|
try:
|
||
|
d = storify(cookie, *requireds, **defaults)
|
||
|
for k, v in d.items():
|
||
|
d[k] = v and urllib.unquote(v)
|
||
|
return d
|
||
|
except KeyError:
|
||
|
badrequest()
|
||
|
raise StopIteration
|
||
|
|
||
|
def debug(*args):
|
||
|
"""
|
||
|
Prints a prettyprinted version of `args` to stderr.
|
||
|
"""
|
||
|
try:
|
||
|
out = ctx.environ['wsgi.errors']
|
||
|
except:
|
||
|
out = sys.stderr
|
||
|
for arg in args:
|
||
|
print >> out, pprint.pformat(arg)
|
||
|
return ''
|
||
|
|
||
|
def _debugwrite(x):
|
||
|
try:
|
||
|
out = ctx.environ['wsgi.errors']
|
||
|
except:
|
||
|
out = sys.stderr
|
||
|
out.write(x)
|
||
|
debug.write = _debugwrite
|
||
|
|
||
|
ctx = context = threadeddict()
|
||
|
|
||
|
ctx.__doc__ = """
|
||
|
A `storage` object containing various information about the request:
|
||
|
|
||
|
`environ` (aka `env`)
|
||
|
: A dictionary containing the standard WSGI environment variables.
|
||
|
|
||
|
`host`
|
||
|
: The domain (`Host` header) requested by the user.
|
||
|
|
||
|
`home`
|
||
|
: The base path for the application.
|
||
|
|
||
|
`ip`
|
||
|
: The IP address of the requester.
|
||
|
|
||
|
`method`
|
||
|
: The HTTP method used.
|
||
|
|
||
|
`path`
|
||
|
: The path request.
|
||
|
|
||
|
`query`
|
||
|
: If there are no query arguments, the empty string. Otherwise, a `?` followed
|
||
|
by the query string.
|
||
|
|
||
|
`fullpath`
|
||
|
: The full path requested, including query arguments (`== path + query`).
|
||
|
|
||
|
### Response Data
|
||
|
|
||
|
`status` (default: "200 OK")
|
||
|
: The status code to be used in the response.
|
||
|
|
||
|
`headers`
|
||
|
: A list of 2-tuples to be used in the response.
|
||
|
|
||
|
`output`
|
||
|
: A string to be used as the response.
|
||
|
"""
|