Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
--HG--
branch : trunk
  • Loading branch information
bbangert committed Nov 9, 2010
2 parents e3dc1df + 61a95cb commit fed5113
Show file tree
Hide file tree
Showing 4 changed files with 432 additions and 2 deletions.
1 change: 1 addition & 0 deletions pylons/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Standard Controllers intended for subclassing by web developers"""
from pylons.controllers.core import WSGIController
from pylons.controllers.xmlrpc import XMLRPCController
from pylons.controllers.jsonrpc import JSONRPCController, JSONRPCError
216 changes: 216 additions & 0 deletions pylons/controllers/jsonrpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"""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', 'JSONRPCError',
'JSONRPC_PARSE_ERROR',
'JSONRPC_INVALID_REQUEST',
'JSONRPC_METHOD_NOT_FOUND',
'JSONRPC_INVALID_PARAMS',
'JSONRPC_INTERNAL_ERROR']

log = logging.getLogger(__name__)

JSONRPC_VERSION = '2.0'


class JSONRPCError(BaseException):

def __init__(self, code, message):
self.code = code
self.message = message
self.data = None

def __str__(self):
return str(self.code) + ': ' + self.message

def as_dict(self):
"""Return a dictionary representation of this object for
serialization in a JSON-RPC response."""
error = dict(code=self.code,
message=self.message)
if self.data:
error['data'] = self.data

return error


JSONRPC_PARSE_ERROR = JSONRPCError(-32700, "Parse error")
JSONRPC_INVALID_REQUEST = JSONRPCError( -32600, "Invalid Request")
JSONRPC_METHOD_NOT_FOUND = JSONRPCError( -32601, "Method not found")
JSONRPC_INVALID_PARAMS = JSONRPCError( -32602, "Invalid params")
JSONRPC_INTERNAL_ERROR = JSONRPCError( -32603, "Internal error")
_reserved_errors = dict(parse_error=JSONRPC_PARSE_ERROR,
invalid_request=JSONRPC_INVALID_REQUEST,
method_not_found=JSONRPC_METHOD_NOT_FOUND,
invalid_params=JSONRPC_INVALID_PARAMS,
internal_error=JSONRPC_INTERNAL_ERROR)

def jsonrpc_error(req_id, error):
"""Generate a Response object with a JSON-RPC error body. Used to
raise top-level pre-defined errors that happen outside the
controller."""
if error in _reserved_errors:
err = _reserved_errors[error]
return Response(body=json.dumps(dict(jsonrpc=JSONRPC_VERSION,
id=req_id,
error=err.as_dict())))

class JSONRPCController(WSGIController):
"""
A WSGI-speaking JSON-RPC 2.0 controller class
See the specification:
`<http://groups.google.com/group/json-rpc/web/json-rpc-2-0>`.
Many parts of this controller are modelled after XMLRPCController
from Pylons 0.9.7
Valid controller return values should be json-serializable objects.
Sub-classes should catch their exceptions and raise JSONRPCError
if they want to pass meaningful errors to the client. Unhandled
errors should be caught and return JSONRPC_INTERNAL_ERROR to the
client.
Parts of the specification not supported (yet):
- Notifications
- Batch
"""

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 'CONTENT_LENGTH' not in environ:
log.debug("No Content-Length")
abort(411)
else:
if environ['CONTENT_LENGTH'] == '':
abort(411)
length = int(environ['CONTENT_LENGTH'])
log.debug('Content-Length: %s', length)
if length == 0:
log.debug("Content-Length is 0")
abort(411)

raw_body = environ['wsgi.input'].read(length)
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._error = None
try:
self._func = self._find_method()
except AttributeError, e:
err = jsonrpc_error(self._req_id, 'method_not_found')
return err(environ, start_response)

# now that we have a method, make sure we have enough
# parameters and pass off control to the controller.
if not isinstance(self._req_params, dict):
# JSON-RPC version 1 request.
arglist = inspect.getargspec(self._func)[0][1:]
if len(self._req_params) < len(arglist):
err = jsonrpc_error(self._req_id, 'invalid_params')
return err(environ, start_response)
else:
kargs = dict(zip(arglist, self._req_params))
else:
# JSON-RPC version 2 request. Params may be default, and
# are already a dict, so skip the parameter length check here.
kargs = self._req_params

# XX Fix this namespace clash. One cannot use names below as
# method argument names as this stands!
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"""
try:
raw_response = self._inspect_call(self._func)
except JSONRPCError, e:
self._error = e.as_dict()
except TypeError, e:
# Insufficient args in an arguments dict v2 call.
if 'takes at least' in str(e):
err = _reserved_errors['invalid_params']
self._error = err.as_dict()
else:
raise
except Exception, e:
log.debug('Encountered unhandled exception: %s', repr(e))
err = _reserved_errors['internal_error']
self._error = err.as_dict()

response = dict(jsonrpc=JSONRPC_VERSION,
id=self._req_id)
if self._error is not None:
response['error'] = self._error
else:
response['result'] = raw_response

try:
return json.dumps(response)
except TypeError, e:
log.debug('Error encoding response: %s', e)
err = _reserved_errors['internal_error']
return json.dumps(dict(
jsonrpc=JSONRPC_VERSION,
id=self._req_id,
error=err.as_dict()))

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('_'):
raise AttributeError, "Method not allowed"

try:
func = getattr(self, self._req_method, None)
except UnicodeEncodeError:
# XMLRPCController catches this, not sure why.
raise AttributeError, ("Problem decoding unicode in requested "
"method name.")

if isinstance(func, types.MethodType):
return func
else:
raise AttributeError, "No such method: %s" % self._req_method
20 changes: 18 additions & 2 deletions tests/test_units/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import os
import sys
from unittest import TestCase
from urllib import quote_plus
from xmlrpclib import loads, dumps

data_dir = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -62,6 +64,20 @@ def xmlreq(self, method, args=None):
args = ()
ee = dict(CONTENT_TYPE='text/xml')
data = dumps(args, methodname=method)
self.response = response = self.app.post('/', params = data, extra_environ=ee)
self.response = response = self.app.post('/', params = data,
extra_environ=ee)
return loads(response.body)[0][0]


def jsonreq(self, method, args=()):
assert(isinstance(args, list) or
isinstance(args, tuple) or
isinstance(args, dict))

ee = dict(CONTENT_TYPE='application/json')
data = json.dumps(dict(id='test',
method=method,
params=args))
self.response = response = self.app.post('/', params=quote_plus(data),
extra_environ=ee)
return json.loads(response.body)

Loading

0 comments on commit fed5113

Please sign in to comment.