Olemis Lang avatar Olemis Lang committed 2643efa

New API methods and exceptions. JSON protocol refactored and tested ?

Comments (0)

Files changed (3)

 # Make protocols pluggable (including default supported ones)
 # Also fixes: #5848 (permission error response code)
 # Also fixes: #5436 (rename plugin to TracRPC - note need to completely remove old code)
+# TODO: Check (XML | JSON) RPC error codes
 t5437/t5437-protocol_api-r7194.diff
 
+# Protocol API version 2
+# TODO: Add new API methods
+# TODO: Add RPC exceptions
+# TODO: Refactor XML protocol handler
+# TODO: Refactor JSON protocol handler
+# TODO: 
+t5437/t5437-protocol_api_v2-r7194.diff
+

t5437/t5437-protocol_api-r7194.diff

+Added `twill` to `tests_require`
+
 diff --git a/trunk/README.wiki b/trunk/README.wiki
 --- a/trunk/README.wiki
 +++ b/trunk/README.wiki
 diff --git a/trunk/setup.py b/trunk/setup.py
 --- a/trunk/setup.py
 +++ b/trunk/setup.py
-@@ -10,19 +10,20 @@
+@@ -10,19 +10,21 @@
  
  setup(
      name='TracXMLRPC',
 +    description='RPC interface to Trac',
      zip_safe=True,
      test_suite = 'tracrpc.tests.suite',
++    tests_require = ['twill'],
      packages=find_packages(exclude=['*.tests']),
      package_data={
 -        'tracrpc': ['templates/*.html']

t5437\t5437-protocol_api_v2-r7194.diff

+New API methods and exceptions. JSON protocol refactored and tested ?
+
+diff -r d083179296d3 trunk/tracrpc/api.py
+--- a/trunk/tracrpc/api.py	Fri Mar 12 22:05:22 2010 -0500
++++ b/trunk/tracrpc/api.py	Sat Mar 13 19:27:36 2010 -0500
+@@ -21,13 +21,34 @@
+     """ RPC Binary type. Currently == xmlrpclib.Binary. """
+     pass
+ 
++#----------------------------------------------------------------
++# RPC Exception classes
++#----------------------------------------------------------------
+ class RPCError(TracError):
+     """ Error class for general RPC-related errors. """
+-    pass
+ 
+ class MethodNotFound(RPCError):
+     """ Error to raise when requested method is not found. """
+-    pass
++
++class _CompositeRpcError(RPCError):
++    def __init__(self, details, title=None, show_traceback=False):
++        if isinstance(details, Exception):
++          self._exc = details
++          message = unicode(details)
++        else :
++          self._exc = None
++          message = details
++        RPCError.__init__(self, message, title, show_traceback)
++    def __unicode__(self):
++        return u"%s details : %s" % (self.__class__.__name__, self.message)
++
++class ProtocolException(_CompositeRpcError):
++    """Protocol could not handle RPC request. Usually this means 
++    that the request has some sort of syntactic error, a library 
++    needed to parse the RPC call is not available, or similar errors."""
++
++class ServiceException(_CompositeRpcError):
++    """The called method threw an exception. Helpful to identify bugs ;o)"""
+ 
+ RPC_TYPES = {int: 'int', bool: 'boolean', str: 'string', float: 'double',
+              datetime: 'dateTime.iso8601', Binary: 'base64',
+@@ -63,8 +84,56 @@
+                    (/login)?/<path_item>. Answer to 'rpc' only if possible.
+         content_type: Starts-with check of 'Content-Type' request header. """
+ 
+-    def rpc_process(req, content_type):
+-        """ Process the RPC request and finalize response. """
++    def parse_rpc_request(req, content_type):
++        """ Parse RPC requests. 
++        
++        req          :        HTTP request object
++        content_type :        Input MIME type
++        
++        Return a dictionary with the following keys set. All the other 
++        values included in this mapping will be ignored by the core 
++        RPC subsystem, will be protocol-specific, and SHOULD NOT be 
++        needed in order to invoke a given method.
++        (TODO: reuse `req` ?)
++        
++        method  (MANDATORY): target method name (e.g. 'ticket.get')
++        params  (OPTIONAL) : a tuple containing input positional arguments
++        headers (OPTIONAL) : if the protocol supports custom headers set 
++                              by the client, then this value SHOULD be a 
++                              dictionary binding `header name` to `value`. 
++                              However, protocol handlers as well as target 
++                              RPC methods *MUST (SHOULD ?) NOT* rely on 
++                              specific values assigned to a particular 
++                              header in order to send a response back 
++                              to the client.
++        mimetype           : request MIME-type. This value will be set 
++                              by core RPC components after calling 
++                              this method so, please, ignore
++        
++        If the request cannot be parsed this method *MUST* raise 
++        an instance of `ProtocolException` wrapping another exception 
++        containing details about the failure.
++        """
++
++    def send_rpc_result(req, rpcreq, result):
++        """Serialize the result of the RPC call and send it back to 
++        the client.
++        
++        rpcreq  : The same object returned by `parse_rpc_request` 
++                  (see above).
++        result  : The value returned by the target RPC method
++        """
++
++    def send_rpc_error(req, rpcreq, e):
++        """Send a fault message back to the caller. Exception type 
++        and message are used for this purpose. This message *SHOULD* 
++        handle `RPCError`, `PermissionError`, and `ResourceNotFound` 
++        and subclasses.
++        
++        rpcreq  : The same object returned by `parse_rpc_request` 
++                  (see above).
++        e       : exception object describing the failure
++        """
+ 
+ class IXMLRPCHandler(Interface):
+ 
+diff -r d083179296d3 trunk/tracrpc/json_rpc.py
+--- a/trunk/tracrpc/json_rpc.py	Fri Mar 12 22:05:22 2010 -0500
++++ b/trunk/tracrpc/json_rpc.py	Sat Mar 13 19:27:36 2010 -0500
+@@ -5,8 +5,9 @@
+ (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
+ """
+ 
++import datetime
++from itertools import izip
+ import re
+-import datetime
+ from types import GeneratorType
+ 
+ from trac.core import *
+@@ -16,7 +17,7 @@
+ from trac.util.text import to_unicode
+ 
+ from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \
+-        RPCError, MethodNotFound
++        RPCError, MethodNotFound, ProtocolException
+ from tracrpc.util import exception_to_unicode, empty, prepare_docs
+ 
+ __all__ = ['JsonRpcProtocol']
+@@ -92,6 +93,12 @@
+         obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
+         return self._normalize(obj)
+ 
++class JsonProtocolException(ProtocolException):
++    """Impossible to handle JSON-RPC request."""
++    def __init__(self, details, code=-32603, title=None, show_traceback=False):
++        ProtocolException.__init__(self, details, title, show_traceback)
++        self.code = code
++
+ class JsonRpcProtocol(Component):
+     r"""
+     Example `POST` request using `curl` with `Content-Type` header
+@@ -126,36 +133,40 @@
+         # Legacy path - provided for backwards compatibility:
+         yield ('jsonrpc', 'application/json')
+ 
+-    def rpc_process(self, req, content_type):
+-        """ Handles JSON-RPC requests """
++    def parse_rpc_request(self, req, content_type):
++        """ Parse JSON-RPC requests"""
+         if not json:
+             self.log.debug("RPC(json) call ignored (not available).")
+-            self._send_response(req, "Error: JSON-RPC not available.\n",
+-                                content_type)
++            # TODO: Specify correponding error code
++            raise JsonProtocolException("Error: JSON-RPC not available.\n")
+         try:
+             data = json.load(req, cls=TracRpcJSONDecoder)
++            if data.get('method') == 'system.multicall':
++              # Prepare for multicall
++              for signature in data.itervalues() :
++                signature['methodName'] = signature.get('method', '')
++            return data
+         except Exception, e:
+             # Abort with exception - no data can be read
+-            self.log.error("RPC(json) decode error %s" % \
++            self.log.error("RPC(json) decode error %s", \
+                     exception_to_unicode(e, traceback=True))
+-            response = json.dumps(self._json_error(e, -32700),
+-                                    cls=TracRpcJSONEncoder)
+-            self._send_response(req, response + '\n', content_type)
+-            return
+-        self.log.debug("RPC(json) call by '%s': %s" % (req.authname, data))
+-        args = data.get('params') or []
+-        r_id = data.get('id', None)
+-        method = data.get('method', '')
++            raise JsonProtocolException(e, -32700)
++
++    def send_rpc_result(self, req, rpcreq, result):
++        """Send JSON-RPC response back to the caller."""
++        r_id = rpcreq.get('id')
+         try:
+-            req.perm.require('XML_RPC') # Need at least XML_RPC
+-            if method == 'system.multicall': # Custom multicall
+-                results = []
+-                for mc in args:
+-                    results.append(self._json_call(req, mc.get('method', ''),
+-                        mc.get('params') or [], mc.get('id') or r_id))
+-                response = {'result': results, 'error': None, 'id': r_id}
++            if rpcreq.get('method') == 'system.multicall': 
++                # Custom multicall
++                args = rpcreq.get('params') or []
++                mcresults = [self._json_result(value, sig.get('id') or r_id) \
++                              for sig, value in izip(args, result)]
++                
++                # TODO: Which one is better ?
++                # response = self._json_result(req, mcresults, r_id)
++                response = {'result': mcresults, 'error': None, 'id': r_id}
+             else:
+-                response = self._json_call(req, method, args, r_id)
++                response = self._json_result(result, r_id)
+             try: # JSON encoding
+                 self.log.debug("RPC(json) result: %s" % repr(response))
+                 response = json.dumps(response, cls=TracRpcJSONEncoder)
+@@ -168,7 +179,13 @@
+             response = json.dumps(self._json_error(e, r_id=r_id),
+                             cls=TracRpcJSONEncoder)
+         self.log.debug("RPC(json) encoded result: %s" % response)
+-        self._send_response(req, response + '\n', content_type)
++        self._send_response(req, response + '\n', rpcreq['mimetype'])
++
++    def send_rpc_error(self, req, rpcreq, e):
++        """Send a JSON-RPC fault message back to the caller. """
++        # TODO : Check error codes and RPC exceptions
++        response = json.dumps(self._json_error(e), cls=TracRpcJSONEncoder)
++        self._send_response(req, response + '\n', rpcreq['mimetype'])
+ 
+     # Internal methods
+ 
+@@ -180,14 +197,11 @@
+         req.end_headers()
+         req.write(response)
+ 
+-    def _json_call(self, req, method, args, r_id=None):
+-        """ Call method and create response dictionary. """
+-        try:
+-            result = (XMLRPCSystem(self.env).get_method(method)(req, args))[0]
+-            if isinstance(result, GeneratorType):
+-                result = list(result)
++    def _json_result(self, result, r_id=None):
++        """ Create JSON-RPC response dictionary. """
++        if not isinstance(result, Exception):
+             return {'result': result, 'error': None, 'id': r_id}
+-        except Exception, e:
++        else :
+             return self._json_error(e, r_id=r_id)
+ 
+     def _json_error(self, e, c=None, r_id=None):
+diff -r d083179296d3 trunk/tracrpc/web_ui.py
+--- a/trunk/tracrpc/web_ui.py	Fri Mar 12 22:05:22 2010 -0500
++++ b/trunk/tracrpc/web_ui.py	Sat Mar 13 19:27:36 2010 -0500
+@@ -6,6 +6,10 @@
+ (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
+ """
+ 
++import sys
++from traceback import format_exc
++from types import GeneratorType
++
+ from pkg_resources import resource_filename
+ 
+ from genshi.builder import tag
+@@ -13,6 +17,8 @@
+ from genshi.template.text import TextTemplate
+ 
+ from trac.core import *
++from trac.perm import PermissionError
++from trac.resource import ResourceNotFound
+ from trac.util.translation import _
+ from trac.web.api import RequestDone, HTTPUnsupportedMediaType
+ from trac.web.main import IRequestHandler
+@@ -20,7 +26,8 @@
+                             add_stylesheet, add_script, add_ctxtnav
+ from trac.wiki.formatter import wiki_to_oneliner
+ 
+-from tracrpc.api import XMLRPCSystem, IRPCProtocol
++from tracrpc.api import XMLRPCSystem, IRPCProtocol, ProtocolException, \
++                          RPCError, ServiceException
+ from tracrpc.util import accepts_mimetype
+ 
+ __all__ = ['RPCWeb']
+@@ -56,7 +63,7 @@
+         content_type = req.get_header('Content-Type') or 'text/html'
+         if protocol:
+             # Perform the method call
+-            protocol.rpc_process(req, content_type)
++            self._rpc_process(req, protocol, content_type)
+             raise RequestDone
+         elif accepts_mimetype(req, 'text/html'):
+             return self._dump_docs(req)
+@@ -67,6 +74,8 @@
+             req.send_error(None, template='', content_type='text/plain',
+                     status=HTTPUnsupportedMediaType.code, env=None, data=body)
+ 
++    # Internal methods
++
+     def _dump_docs(self, req):
+         # Dump RPC documentation
+         req.perm.require('XML_RPC') # Need at least XML_RPC
+@@ -119,6 +128,47 @@
+             return "Error rendering protocol documentation. " \
+                        "Contact your '''Trac''' administrator for details"
+ 
++    def _rpc_process(self, req, protocol, content_type):
++        """Process incoming RPC request and finalize response."""
++        req.perm.require('XML_RPC') # Need at least XML_RPC
++        proto_id = protocol.rpc_info()[0]
++        rpcreq = {'mimetype': content_type}
++        try :
++            self.log.debug("RPC(%s) call by '%s'", proto_id, req.authname)
++            rpcreq = protocol.parse_rpc_request(req, content_type)
++            rpcreq['mimetype'] = content_type
++            method_name = rpcreq.get('method')
++            if method_name is None :
++                raise ProtocolException('Missing method name')
++            args = rpcreq.get('params') or []
++            self.log.debug("RPC(%s) call by '%s' %s", proto_id, \
++                                              req.authname, method_name)
++            try :
++                result = (XMLRPCSystem(self.env).get_method(method_name)(req, args))[0]
++                if isinstance(result, GeneratorType):
++                    result = list(result)
++            except (RPCError, PermissionError, ResourceNotFound), e:
++                raise
++            except Exception:
++                e, tb = sys.exc_info()[-2:]
++                raise ServiceException(e), None, tb
++            else :
++                protocol.send_rpc_result(req, rpcreq, result)
++        except (RPCError, PermissionError, ResourceNotFound), e:
++            self.log.exception("RPC(%s) Error", proto_id)
++            protocol.send_rpc_error(req, rpcreq, e)
++        except Exception, e :
++            self.log.exception("RPC(%s) Unknown protocol error", proto_id)
++            self._send_unknown_error(req, rpcreq, e)
++
++    def _send_unknown_error(self, req, rpcreq, e):
++        """Last recourse if protocol cannot handle the RPC request | error"""
++        stackTrace = format_exc()
++        method_name = rpcreq and rpcreq.get('method') or '(undefined)'
++        body = "Can not send response for '%s'\n\n%s" % (method_name, stackTrace)
++        req.send_error(None, template='', content_type='text/plain',
++                            env=None, data=body)
++
+     # ITemplateProvider methods
+ 
+     def get_htdocs_dirs(self):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.