370 lines
10 KiB
Python
370 lines
10 KiB
Python
"""
|
|
Web API (wrapper around WSGI)
|
|
(from web.py)
|
|
"""
|
|
|
|
__all__ = [
|
|
"config",
|
|
"badrequest", "notfound", "gone", "internalerror",
|
|
"header", "output", "flush", "debug",
|
|
"input", "data",
|
|
"setcookie", "cookies",
|
|
"ctx",
|
|
"loadhooks", "load", "unloadhooks", "unload", "_loadhooks",
|
|
"wsgifunc"
|
|
]
|
|
|
|
import sys, os, cgi, threading, Cookie, pprint, traceback
|
|
try: import itertools
|
|
except ImportError: pass
|
|
from utils import storage, storify, threadeddict, dictadd, intget, lstrips, utf8
|
|
|
|
config = storage()
|
|
config.__doc__ = """
|
|
A configuration object for various aspects of web.py.
|
|
|
|
`db_parameters`
|
|
: A dictionary containing the parameters to be passed to `connect`
|
|
when `load()` is called.
|
|
`db_printing`
|
|
: Set to `True` if you would like SQL queries and timings to be
|
|
printed to the debug output.
|
|
|
|
"""
|
|
|
|
def badrequest():
|
|
"""Return a `400 Bad Request` error."""
|
|
ctx.status = '400 Bad Request'
|
|
header('Content-Type', 'text/html')
|
|
return output('bad request')
|
|
|
|
def notfound():
|
|
"""Returns a `404 Not Found` error."""
|
|
ctx.status = '404 Not Found'
|
|
header('Content-Type', 'text/html')
|
|
return output('not found')
|
|
|
|
def gone():
|
|
"""Returns a `410 Gone` error."""
|
|
ctx.status = '410 Gone'
|
|
header('Content-Type', 'text/html')
|
|
return output("gone")
|
|
|
|
def internalerror():
|
|
"""Returns a `500 Internal Server` error."""
|
|
ctx.status = "500 Internal Server Error"
|
|
ctx.headers = [('Content-Type', 'text/html')]
|
|
ctx.output = "internal server error"
|
|
|
|
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 output(string_):
|
|
"""Appends `string_` to the response."""
|
|
if isinstance(string_, unicode): string_ = string_.encode('utf8')
|
|
if ctx.get('flush'):
|
|
ctx._write(string_)
|
|
else:
|
|
ctx.output += str(string_)
|
|
|
|
def flush():
|
|
ctx.flush = True
|
|
return flush
|
|
|
|
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): 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']:
|
|
if e['REQUEST_METHOD'] == 'POST':
|
|
a = cgi.FieldStorage(fp = StringIO(data()), 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:
|
|
return storify(out, *requireds, **defaults)
|
|
except KeyError:
|
|
badrequest()
|
|
raise StopIteration
|
|
|
|
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):
|
|
"""Sets a cookie."""
|
|
if expires < 0:
|
|
expires = -1000000000
|
|
kargs = {'expires': expires, 'path':'/'}
|
|
if domain:
|
|
kargs['domain'] = domain
|
|
# @@ should we limit cookies to a different path?
|
|
cookie = Cookie.SimpleCookie()
|
|
cookie[name] = 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:
|
|
return storify(cookie, *requireds, **defaults)
|
|
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
|
|
|
|
class _outputter:
|
|
"""Wraps `sys.stdout` so that print statements go into the response."""
|
|
def __init__(self, file): self.file = file
|
|
def write(self, string_):
|
|
if hasattr(ctx, 'output'):
|
|
return output(string_)
|
|
else:
|
|
self.file.write(string_)
|
|
def __getattr__(self, attr): return getattr(self.file, attr)
|
|
def __getitem__(self, item): return self.file[item]
|
|
|
|
def _capturedstdout():
|
|
sysstd = sys.stdout
|
|
while hasattr(sysstd, 'file'):
|
|
if isinstance(sys.stdout, _outputter): return True
|
|
sysstd = sysstd.file
|
|
if isinstance(sys.stdout, _outputter): return True
|
|
return False
|
|
|
|
if not _capturedstdout():
|
|
sys.stdout = _outputter(sys.stdout)
|
|
|
|
_context = {threading.currentThread(): storage()}
|
|
ctx = context = threadeddict(_context)
|
|
|
|
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.
|
|
"""
|
|
|
|
loadhooks = {}
|
|
_loadhooks = {}
|
|
|
|
def load():
|
|
"""
|
|
Loads a new context for the thread.
|
|
|
|
You can ask for a function to be run at loadtime by
|
|
adding it to the dictionary `loadhooks`.
|
|
"""
|
|
_context[threading.currentThread()] = storage()
|
|
ctx.status = '200 OK'
|
|
ctx.headers = []
|
|
if config.get('db_parameters'):
|
|
import db
|
|
db.connect(**config.db_parameters)
|
|
|
|
for x in loadhooks.values(): x()
|
|
|
|
def _load(env):
|
|
load()
|
|
ctx.output = ''
|
|
ctx.environ = ctx.env = env
|
|
ctx.host = env.get('HTTP_HOST')
|
|
ctx.homedomain = 'http://' + env.get('HTTP_HOST', '[unknown]')
|
|
ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
|
|
ctx.home = ctx.homedomain + ctx.homepath
|
|
ctx.ip = env.get('REMOTE_ADDR')
|
|
ctx.method = env.get('REQUEST_METHOD')
|
|
ctx.path = env.get('PATH_INFO')
|
|
# http://trac.lighttpd.net/trac/ticket/406 requires:
|
|
if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
|
|
ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0],
|
|
os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
|
|
|
|
if env.get('QUERY_STRING'):
|
|
ctx.query = '?' + env.get('QUERY_STRING', '')
|
|
else:
|
|
ctx.query = ''
|
|
|
|
ctx.fullpath = ctx.path + ctx.query
|
|
for x in _loadhooks.values(): x()
|
|
|
|
unloadhooks = {}
|
|
|
|
def unload():
|
|
"""
|
|
Unloads the context for the thread.
|
|
|
|
You can ask for a function to be run at loadtime by
|
|
adding it ot the dictionary `unloadhooks`.
|
|
"""
|
|
for x in unloadhooks.values(): x()
|
|
# ensures db cursors and such are GCed promptly
|
|
del _context[threading.currentThread()]
|
|
|
|
def _unload():
|
|
unload()
|
|
|
|
def wsgifunc(func, *middleware):
|
|
"""Returns a WSGI-compatible function from a webpy-function."""
|
|
middleware = list(middleware)
|
|
|
|
def wsgifunc(env, start_resp):
|
|
_load(env)
|
|
try:
|
|
result = func()
|
|
except StopIteration:
|
|
result = None
|
|
except:
|
|
print >> debug, traceback.format_exc()
|
|
result = internalerror()
|
|
|
|
is_generator = result and hasattr(result, 'next')
|
|
if is_generator:
|
|
# wsgi requires the headers first
|
|
# so we need to do an iteration
|
|
# and save the result for later
|
|
try:
|
|
firstchunk = result.next()
|
|
except StopIteration:
|
|
firstchunk = ''
|
|
|
|
status, headers, output = ctx.status, ctx.headers, ctx.output
|
|
ctx._write = start_resp(status, headers)
|
|
|
|
# and now, the fun:
|
|
|
|
def cleanup():
|
|
# we insert this little generator
|
|
# at the end of our itertools.chain
|
|
# so that it unloads the request
|
|
# when everything else is done
|
|
|
|
yield '' # force it to be a generator
|
|
_unload()
|
|
|
|
# result is the output of calling the webpy function
|
|
# it could be a generator...
|
|
|
|
if is_generator:
|
|
if firstchunk is flush:
|
|
# oh, it's just our special flush mode
|
|
# ctx._write is set up, so just continue execution
|
|
try:
|
|
result.next()
|
|
except StopIteration:
|
|
pass
|
|
|
|
_unload()
|
|
return []
|
|
else:
|
|
return itertools.chain([firstchunk], result, cleanup())
|
|
|
|
# ... but it's usually just None
|
|
#
|
|
# output is the stuff in ctx.output
|
|
# it's usually a string...
|
|
if isinstance(output, str): #@@ other stringlikes?
|
|
_unload()
|
|
return [output]
|
|
# it could be a generator...
|
|
elif hasattr(output, 'next'):
|
|
return itertools.chain(output, cleanup())
|
|
else:
|
|
_unload()
|
|
raise Exception, "Invalid ctx.output"
|
|
|
|
for mw_func in middleware:
|
|
wsgifunc = mw_func(wsgifunc)
|
|
|
|
return wsgifunc
|