Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/pylons/pylons/controllers/xmlrpc.py
0001"""The base WSGI XMLRPCController"""
0002import inspect
0003import logging
0004import sys
0005import xmlrpclib
0006
0007from paste.response import replace_header
0008from paste.wsgiwrappers import WSGIResponse
0009
0010from pylons.controllers import WSGIController
0011from pylons.controllers.util import abort
0012
0013__all__ = ['XMLRPCController']
0014
0015log = logging.getLogger(__name__)
0016
0017XMLRPC_MAPPING = ((basestring, 'string'), (list, 'array'), (bool, 'boolean'),
0018                  (int, 'int'), (float, 'double'), (dict, 'struct'),
0019                  (xmlrpclib.DateTime, 'dateTime.iso8601'),
0020                  (xmlrpclib.Binary, 'base64'))
0021
0022def xmlrpc_sig(args):
0023    """Returns a list of the function signature in string format based on a 
0024    tuple provided by xmlrpclib."""
0025    signature = []
0026    for param in args:
0027        for type, xml_name in XMLRPC_MAPPING:
0028            if isinstance(param, type):
0029                signature.append(xml_name)
0030                break
0031    return signature
0032
0033
0034def xmlrpc_fault(code, message):
0035    """Convienence method to return a Pylons response XMLRPC Fault"""
0036    fault = xmlrpclib.Fault(code, message)
0037    return WSGIResponse(xmlrpclib.dumps(fault, methodresponse=True))
0038
0039
0040class XMLRPCController(WSGIController):
0041    """XML-RPC Controller that speaks WSGI
0042    
0043    This controller handles XML-RPC responses and complies with the 
0044    `XML-RPC Specification <http://www.xmlrpc.com/spec>`_ as well as the
0045    `XML-RPC Introspection
0046    <http://scripts.incutio.com/xmlrpc/introspection.html>`_ specification.
0047    
0048    By default, methods with names containing a dot are translated to use an
0049    underscore. For example, the `system.methodHelp` is handled by the method 
0050    `system_methodHelp`.
0051    
0052    Methods in the XML-RPC controller will be called with the method given in 
0053    the XMLRPC body. Methods may be annotated with a signature attribute to 
0054    declare the valid arguments and return types.
0055    
0056    For example::
0057        
0058        class MyXML(XMLRPCController):
0059            def userstatus(self):
0060                return 'basic string'
0061            userstatus.signature = [ ['string'] ]
0062            
0063            def userinfo(self, username, age=None):
0064                user = LookUpUser(username)
0065                response = {'username':user.name}
0066                if age and age > 10:
0067                    response['age'] = age
0068                return response
0069            userinfo.signature = [['struct', 'string'],
0070                                  ['struct', 'string', 'int']]
0071    
0072    Since XML-RPC methods can take different sets of data, each set of valid
0073    arguments is its own list. The first value in the list is the type of the
0074    return argument. The rest of the arguments are the types of the data that
0075    must be passed in.
0076    
0077    In the last method in the example above, since the method can optionally 
0078    take an integer value both sets of valid parameter lists should be
0079    provided.
0080    
0081    Valid types that can be checked in the signature and their corresponding
0082    Python types::
0083
0084        'string' - str
0085        'array' - list
0086        'boolean' - bool
0087        'int' - int
0088        'double' - float
0089        'struct' - dict
0090        'dateTime.iso8601' - xmlrpclib.DateTime
0091        'base64' - xmlrpclib.Binary
0092    
0093    The class variable ``allow_none`` is passed to xmlrpclib.dumps; enabling it
0094    allows translating ``None`` to XML (an extension to the XML-RPC
0095    specification)
0096
0097    Note::
0098
0099        Requiring a signature is optional.
0100    """
0101    allow_none = False
0102    max_body_length = 4194304
0103
0104    def _get_method_args(self):
0105        return self.rpc_kargs
0106
0107    def __call__(self, environ, start_response):
0108        """Parse an XMLRPC body for the method, and call it with the 
0109        appropriate arguments"""
0110        # Pull out the length, return an error if there is no valid
0111        # length or if the length is larger than the max_body_length.
0112        length = environ.get('CONTENT_LENGTH')
0113        if length:
0114            length = int(length)
0115        else:
0116            # No valid Content-Length header found
0117            log.debug("No Content-Length found, returning 411 error")
0118            abort(411)
0119        if length > self.max_body_length or length == 0:
0120            log.debug("Content-Length larger than max body length. Max: %s,"
0121                      " Sent: %s. Returning 413 error", self.max_body_length,
0122                      length)
0123            abort(413, "XML body too large")
0124
0125        body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
0126        rpc_args, orig_method = xmlrpclib.loads(body)
0127
0128        method = self._find_method_name(orig_method)
0129        func = self._find_method(method)
0130        if not func:
0131            log.debug("Method: %r not found, returning xmlrpc fault", method)
0132            return xmlrpc_fault(0, "No such method name")(environ,
0133                                                          start_response)
0134
0135        # Signature checking for params
0136        if hasattr(func, 'signature'):
0137            log.debug("Checking XMLRPC argument signature")
0138            valid_args = False
0139            params = xmlrpc_sig(rpc_args)
0140            for sig in func.signature:
0141                # Next sig if we don't have the same amount of args
0142                if len(sig)-1 != len(rpc_args):
0143                    continue
0144
0145                # If the params match, we're valid
0146                if params == sig[1:]:
0147                    valid_args = True
0148                    break
0149
0150            if not valid_args:
0151                log.debug("Bad argument signature recieved, returning xmlrpc"
0152                          " fault")
0153                msg = ("Incorrect argument signature. %r recieved does not "
0154                       "match %r signature for method %r" %                              (params, func.signature, orig_method))
0156                return xmlrpc_fault(0, msg)(environ, start_response)
0157
0158        # Change the arg list into a keyword dict based off the arg
0159        # names in the functions definition
0160        arglist = inspect.getargspec(func)[0][1:]
0161        kargs = dict(zip(arglist, rpc_args))
0162        kargs['action'], kargs['environ'] = method, environ
0163        kargs['start_response'] = start_response
0164        self.rpc_kargs = kargs
0165        self._func = func
0166
0167        # Now that we know the method is valid, and the args are valid,
0168        # we can dispatch control to the default WSGIController
0169        status = []
0170        headers = []
0171        exc_info = []
0172        def change_content(new_status, new_headers, new_exc_info=None):
0173            status.append(new_status)
0174            headers.extend(new_headers)
0175            exc_info.append(new_exc_info)
0176        output = WSGIController.__call__(self, environ, change_content)
0177        replace_header(headers, 'Content-Type', 'text/xml')
0178        start_response(status[0], headers, exc_info[0])
0179        return output
0180
0181    def _dispatch_call(self):
0182        """Dispatch the call to the function chosen by __call__"""
0183        raw_response = self._inspect_call(self._func)
0184        if not isinstance(raw_response, xmlrpclib.Fault):
0185            raw_response = (raw_response,)
0186
0187        response = xmlrpclib.dumps(raw_response, methodresponse=True,
0188                                   allow_none=self.allow_none)
0189        return WSGIResponse(response)
0190
0191    def _find_method(self, name):
0192        """Locate a method in the controller by the specified name and return
0193        it
0194        """
0195        log.debug("Looking for XMLRPC method: %r", name)
0196        try:
0197            return getattr(self, name, None)
0198        except UnicodeEncodeError:
0199            return None
0200
0201    def _find_method_name(self, name):
0202        """Locate a method in the controller by the appropriate name
0203        
0204        By default, this translates method names like 'system.methodHelp' into
0205        'system_methodHelp'.
0206        """
0207        return name.replace('.', '_')
0208
0209    def _publish_method_name(self, name):
0210        """Translate an internal method name to a publicly viewable one
0211        
0212        By default, this translates internal method names like 'blog_view' into
0213        'blog.view'.
0214        """
0215        return name.replace('_', '.')
0216
0217    def system_listMethods(self):
0218        """Returns a list of XML-RPC methods for this XML-RPC resource"""
0219        methods = []
0220        for method in dir(self):
0221            meth = getattr(self, method)
0222
0223            # Only methods have this attribute
0224            if not method.startswith('_') and hasattr(meth, 'im_self'):
0225                methods.append(self._publish_method_name(method))
0226        return methods
0227    system_listMethods.signature = [['array']]
0228
0229    def system_methodSignature(self, name):
0230        """Returns an array of array's for the valid signatures for a method.
0231
0232        The first value of each array is the return value of the method. The
0233        result is an array to indicate multiple signatures a method may be
0234        capable of.
0235        """
0236        method = self._find_method(self._find_method_name(name))
0237        if method:
0238            return getattr(method, 'signature', '')
0239        else:
0240            return xmlrpclib.Fault(0, 'No such method name')
0241    system_methodSignature.signature = [['array', 'string'],
0242                                        ['string', 'string']]
0243
0244    def system_methodHelp(self, name):
0245        """Returns the documentation for a method"""
0246        method = self._find_method(self._find_method_name(name))
0247        if method:
0248            help = MethodHelp.getdoc(method)
0249            sig = getattr(method, 'signature', None)
0250            if sig:
0251                help += "\n\nMethod signature: %s" % sig
0252            return help
0253        return xmlrpclib.Fault(0, "No such method name")
0254    system_methodHelp.signature = [['string', 'string']]
0255
0256
0257class MethodHelp(object):
0258    """Wrapper for formatting doc strings from XMLRPCController methods"""
0259    def __init__(self, doc):
0260        self.__doc__ = doc
0261
0262    def getdoc(method):
0263        """Return a formatted doc string, via inspect.getdoc, from the
0264        specified XMLRPCController method
0265
0266        The method's help attribute is used if it exists, otherwise the
0267        method's doc string is used.
0268        """
0269        help = getattr(method, 'help', None)
0270        if help is None:
0271            help = method.__doc__
0272        doc = inspect.getdoc(MethodHelp(help))
0273        if doc is None:
0274            return ''
0275        return doc
0276    getdoc = staticmethod(getdoc)

Top