0001import sys
0002import inspect
0003import xmlrpclib
0004import traceback
0005try:
0006 from cStringIO import StringIO
0007except ImportError:
0008 from StringIO import StringIO
0009from paste.httpexceptions import HTTPBadRequest
0010json = None
0011
0012__all__ = ['unpack', 'unpack_xmlrpc', 'unpack_json']
0013
0014def unpack(func):
0015 """
0016 Unpacks ``self.path_parts`` and ``self.fields`` into a function
0017 call; this decorator can be used to turn such a method into a
0018 zero-argument method that unpacks those arguments.
0019
0020 Arguments can have ``_path`` on the end of their name, in which
0021 case they are unpacked from the path. Also variable arguments
0022 like ``*args`` will be unpacked from the path.
0023
0024 Other arguments can have ``_int``, ``_list``, ``_float``, and
0025 ``_req`` appended. These each do a convertion -- ``_int`` to
0026 integer, ``_float`` to floating point numbers, ``_list`` will make
0027 sure it is a list of items (and without this it cannot be a
0028 list!), ``_req`` that it is not empty. A bad conversion will
0029 cause an HTTPBadRequest exception to be raised (this is not meant
0030 to be used for validation of user input, only for passing
0031 information back that came from the server).
0032
0033 Note that the argument extensions shouldn't go in the HTTP
0034 variables, they are only present in the function signature.
0035 """
0036 argspec = FunctionArgSpec(func)
0037 def replacement_func(self):
0038 args, kw = argspec.unpack_args(self.path_parts, self.fields)
0039 return func(self, *args, **kw)
0040 replacement_func.__doc__ = func.__doc__
0041 replacement_func.__name__ = func.__name__
0042 return replacement_func
0043
0044def unpack_xmlrpc(func):
0045 """
0046 Unpacks an XMLRPC request into a function call, and packs the
0047 response into a XMLRPC response.
0048 """
0049 def replacement_func(self):
0050 assert self.environ['CONTENT_TYPE'].startswith('text/xml')
0051 data = self.environ['wsgi.input'].read()
0052 xmlargs, method_name = xmlrpclib.loads(data)
0053 if method_name:
0054 kw = {'method_name': method_name}
0055 else:
0056 kw = {}
0057 self.set_header('content-type', 'text/xml; charset=UTF-8')
0058 try:
0059 result = func(self, *xmlargs, **kw)
0060 except:
0061 fault = make_rpc_exception(self.environ, sys.exc_info())
0062 body = xmlrpclib.dumps(
0063 xmlrpclib.Fault(1, fault), encoding='utf-8')
0064 else:
0065 if not isinstance(result, tuple):
0066 result = (result,)
0067 body = xmlrpclib.dumps(
0068 result, methodresponse=True, encoding='utf-8')
0069 self.write(body)
0070 replacement_func.__doc__ = func.__doc__
0071 replacement_func.__name__ = func.__name__
0072 return replacement_func
0073
0074def make_rpc_exception(environ, exc_info):
0075 config = environ['paste.config']
0076 rpc_exception = config.get('rpc_exception', None)
0077 if rpc_exception not in (None, 'occurred', 'exception', 'traceback'):
0078 environ['wsgi.errors'].write(
0079 "Bad 'rpc_exception' setting: %r\n" % rpc_exception)
0080 rpc_exception = None
0081 if rpc_exception is None:
0082 if config.get('debug'):
0083 rpc_exception = 'traceback'
0084 else:
0085 rpc_exception = 'exception'
0086 if rpc_exception == 'occurred':
0087 fault = 'unhandled exception'
0088 elif rpc_exception == 'exception':
0089 fault = str(exc_info[0])
0090 elif rpc_exception == 'traceback':
0091 out = StringIO()
0092 traceback.print_exception(*exc_info, **{'file': out})
0093 fault = out.getvalue()
0094 return fault
0095
0096def unpack_json(func):
0097 """
0098 Unpack a JSON request into a function call, and pack the return
0099 value into a JSON response.
0100
0101 @@: Should this always create a JSON response? Should there be
0102 a decorator to give a JSON response without a JSON request?
0103 """
0104 global json
0105 if json is None:
0106 import json
0107 def replacement_func(self):
0108 data = self.environ['wsgi.input'].read()
0109 jsonrpc = json.jsonToObj(data)
0110 method = jsonrpc['method']
0111 params = jsonrpc['params']
0112 id = jsonrpc['id']
0113 if method:
0114 kw = {'method_name': method}
0115 else:
0116 kw = {}
0117 self.set_header('content-type', 'text/plain; charset: UTF-8')
0118 try:
0119 result = func(self, *params, **kw)
0120 except:
0121 body = make_rpc_exception(self.environ, sys.exc_info())
0122 response = {
0123 'result': None,
0124 'error': body,
0125 'id': id}
0126 else:
0127 response = {
0128 'result': result,
0129 'error': None,
0130 'id': id}
0131 self.write(json.objToJson(response))
0132 replacement_func.__doc__ = func.__doc__
0133 replacement_func.__name__ = func.__name__
0134 return replacement_func
0135
0136
0137class FunctionArg(object):
0138 """
0139 Object that represents an argument to a function which may have
0140 magic extensions which fetch it from the path parts or coerce it
0141 to a specific type.
0142
0143 Example:
0144
0145 user_id_int_path: fetch user_id from path and coerce it into an
0146 int
0147 """
0148
0149 def __init__(self, name):
0150 self.from_path = False
0151 self.argname = name
0152 self.name_parts = []
0153 self.coercer = normal
0154 while 1:
0155 if name.endswith('_int'):
0156 self.add_coercer(make_int)
0157 self.name_parts.append(name[-4:])
0158 name = name[:-4]
0159 elif name.endswith('_list'):
0160 coercer = self.add_coercer(make_list)
0161 self.name_parts.append(name[-5:])
0162 name = name[:-5]
0163 elif name.endswith('_float'):
0164 self.add_coercer(make_float)
0165 self.name_parts.append(name[-6:])
0166 name = name[:-6]
0167 elif name.endswith('_req'):
0168 self.add_coercer(make_required)
0169 self.name_parts.append(name[-4:])
0170 name = name[:-4]
0171 elif name.endswith('_path'):
0172 if self.name_parts:
0173 raise TypeError("The _path extension must come "
0174 "last (use foo_int_path, not foo_path_int)")
0175 self.name_parts.append(name[-5:])
0176 name = name[:-5]
0177 self.from_path = True
0178 else:
0179 break
0180 self.name = name
0181
0182 def add_coercer(self, new_coercer):
0183 coercer = self.coercer
0184 if not coercer or coercer is normal:
0185 self.coercer = new_coercer
0186 else:
0187 def coerce(val):
0188 return new_coercer(coercer(val))
0189 self.coercer = coerce
0190
0191 def coerce(self, value):
0192 try:
0193 value = self.coercer(value)
0194 except (ValueError, TypeError), e:
0195 raise HTTPBadRequest(
0196 "Bad variable %r: %s" % (self.name, e))
0197 return value
0198
0199
0200class FunctionArgSpec(object):
0201
0202 """
0203 Object that represents the parsed function signature for a given
0204 function.
0205 """
0206
0207 def __init__(self, func):
0208 self.argnames, self.varargs, self.varkw, defaults = (
0209 inspect.getargspec(func))
0210 self.required_args = []
0211 self.required_path_args = []
0212 self.optional_path_args = []
0213
0214 if self.argnames and self.argnames[0] == 'self':
0215 self.argnames = self.argnames[1:]
0216 funcargs = [FunctionArg(name) for name in self.argnames]
0217 argnames = [arg.name for arg in funcargs]
0218 for name in argnames:
0219 if argnames.count(name) > 1:
0220 raise TypeError("Argument names must be unique. "
0221 "More than one found for %r" % name)
0222 self.funcargs = dict(zip(argnames, funcargs))
0223
0224 while funcargs and funcargs[0].from_path:
0225 if len(defaults or ()) == len(funcargs):
0226
0227 self.optional_path_args.append(funcargs.pop(0))
0228 defaults = defaults[1:]
0229 else:
0230 self.required_path_args.append(funcargs.pop(0))
0231 if not defaults:
0232 self.required_args = funcargs
0233 else:
0234 self.required_args = funcargs[:-len(defaults)]
0235
0236 def unpack_args(self, path_parts, fields):
0237 args = []
0238 found_args = []
0239 kw = {}
0240 if len(self.required_path_args) > len(path_parts):
0241 raise HTTPBadRequest(
0242 "Not enough parameters on the URL (expected %i more "
0243 "path segments)" % (len(self.required_path_args)-len(path_parts)))
0244 if (not self.varargs
0245 and (len(self.required_path_args)+len(self.optional_path_args))
0246 < len(path_parts)):
0247 raise HTTPBadRequest(
0248 "Too many parameters on the URL (expected %i less path "
0249 "segments)" % (len(path_parts)-len(self.required_path_args)
0250 -len(self.optional_path_args)))
0251
0252 all_path_args = self.required_path_args + self.optional_path_args
0253 for i, value in enumerate(path_parts):
0254 if len(all_path_args) > i:
0255 value = all_path_args[i].coerce(value)
0256 args.append(value)
0257 for name, value in fields.iteritems():
0258 if not self.varkw and name not in self.funcargs:
0259 raise HTTPBadRequest("Variable %r not expected" % name)
0260 if name in self.funcargs:
0261 value = self.funcargs[name].coerce(value)
0262 name = self.funcargs[name].argname
0263 kw[name] = value
0264 for arg in self.required_args:
0265 if arg.argname not in kw:
0266 raise HTTPBadRequest("Variable %r required" % arg.name)
0267 return args, kw
0268
0269def make_int(v):
0270 if isinstance(v, list):
0271 return map(int, v)
0272 else:
0273 return int(v)
0274
0275def make_float(v):
0276 if isinstance(v, list):
0277 return map(float, v)
0278 else:
0279 return float(v)
0280
0281def make_list(v):
0282 if isinstance(v, list):
0283 return v
0284 else:
0285 return [v]
0286
0287def make_required(s):
0288 if s is None:
0289 raise TypeError
0290 return s
0291
0292def normal(v):
0293 if isinstance(v, list):
0294 raise ValueError("List not expected")
0295 return v