Good evening folks. An update here on the Pylons + JSON-RPC + Pyjamas saga. I bring you a sneak-peek at the JSONRPCController class. It’s modelled after the XMLRPCController class that already ships with Pylons 0.9.7 and will be more first forays into contributing to the Pylons community. I hope to get some feedback from the Pylons developers as my intent is to submit this class for inclusion in Pylons.
So please feel free to leave constructive comments and suggestions.
First the controller class you’ll need to implement your own JSON-RPC service:
"""The base WSGI JSONRPCController"""
import inspect
import json
import logging
import types
import urllib
from paste.response import replace_header
from pylons.controllers import WSGIController
from pylons.controllers.util import abort, Response
__all__ = ['JSONRPCController']
log = logging.getLogger(__name__)
class JSONRPCController(WSGIController):
"""
A WSGI-speaking JSON-RPC controller class
See the specification:
`.
Many parts of this controller are modelled after XMLRPCController
from Pylons 0.9.7
Parts of the specification not supported (yet):
- Notifications
"""
def _get_method_args(self):
"""Return `self._rpc_args` to dispatched controller method
chosen by __call__"""
return self._rpc_args
def __call__(self, environ, start_response):
"""Parse the request body as JSON, look up the method on the
controller and if it exists, dispatch to it.
"""
length = 0
if not environ.has_key('CONTENT_LENGTH'):
log.debug("No Content-Length")
abort(411)
else:
length = int(environ['CONTENT_LENGTH'])
log.debug('Content-Length: %s', length)
if length == 0:
log.debug("Content-Length is 0")
abort(413)
raw_body = environ['wsgi.input'].read(length)[0:-1]
json_body = json.loads(urllib.unquote_plus(raw_body))
self._req_id = json_body['id']
self._req_method = json_body['method']
self._req_params = json_body['params']
log.debug('id: %s, method: %s, params: %s',
self._req_id,
self._req_method,
self._req_params)
self._func = self._find_method()
self._error = None
log.debug('found method: %s', self._func.func_name)
# now that we have a method, dispatch control to WSGIController
# store argument parameters as dict for _get_method_args
arglist = inspect.getargspec(self._func)[0][1:]
kargs = dict(zip(arglist, self._req_params))
kargs['action'], kargs['environ'] = self._req_method, environ
kargs['start_response'] = start_response
self._rpc_args = kargs
status = []
headers = []
exc_info = []
def change_content(new_status, new_headers, new_exc_info=None):
status.append(new_status)
headers.extend(new_headers)
exc_info.append(new_exc_info)
output = WSGIController.__call__(self, environ, change_content)
output = list(output)
headers.append(('Content-Length', str(len(output[0]))))
replace_header(headers, 'Content-Type', 'application/json')
start_response(status[0], headers, exc_info[0])
return output
def _dispatch_call(self):
"""Implement dispatch interface specified by WSGIController"""
raw_response = self._inspect_call(self._func)
response = dict(
id=self._req_id,
result=raw_response,
error=self._error)
return json.dumps(response)
def _find_method(self):
"""Return method named by `self._req_method` in controller if able"""
log.debug('Trying to find JSON-RPC method: %s', self._req_method)
if self._req_method.startswith('_'):
self._error = "No such method"
try:
func = getattr(self, self._req_method, None)
except UnicodeEncodeError:
return # should handle this better
func = func if isinstance(func, types.MethodType) else None
return func
There are some potential rough corners to handle. This class needs a good set of offensive tests to try and find the corner cases it should protect against. It also doesn’t implement the notification response from the protocol specification. It also needs error handling. If you find any other holes I should plug before submitting it to the Pylons project, please leave a comment or send me a mail.
Anyhow, once you have this class you’ll need to subclass it as usual like a normal Pylons controller:
import logging
from pylons import request, response, session, url
from pylons.controllers.util import abort, redirect
from jsontest.lib.jsonrpc import JSONRPCController
log = logging.getLogger(__name__)
class EchoController(JSONRPCController):
def echo(self, message):
return 'Got message: %s' % message
Make sure to add a route to your controller. The default mappings don’t really work AFAIK.
map.connect('/echoservice', controller='echo')
To test it, I prefer the python json-rpc client. Fire up the paster local development server and at a python interpreter try:
>>> from jsonrpc import ServiceProxy
>>> s = ServiceProxy('http://localhost:5000/echoservice')
>>> s.echo("Hello, world!")
'Got message: Hello, world!'
>>>
And voila!
Happy hacking.