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                # This is an optional path segment
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