git.fiddlerwoaroof.com
jsonrpc/server.py
d149e5b8
 #
 #  Copyright (c) 2011 Edward Langley
 #  All rights reserved.
 #
 #  Redistribution and use in source and binary forms, with or without
 #  modification, are permitted provided that the following conditions
 #  are met:
 #
 #  Redistributions of source code must retain the above copyright notice,
 #  this list of conditions and the following disclaimer.
 #
 #  Redistributions in binary form must reproduce the above copyright
 #  notice, this list of conditions and the following disclaimer in the
 #  documentation and/or other materials provided with the distribution.
 #
 #  Neither the name of the project's author nor the names of its
 #  contributors may be used to endorse or promote products derived from
 #  this software without specific prior written permission.
 #
 #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 #  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 #  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 #  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 #  HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 #  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 #  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 #  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 #  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 #  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #
 #
92ada279
 import traceback
d149e5b8
 import jsonrpc.jsonutil
8445cb9a
 from jsonrpc.utilities import public
604c186e
 import jsonrpc.common
d149e5b8
 
 # Twisted imports
 from twisted.web import server
 from twisted.internet import threads
18f5a305
 from twisted.internet import defer
d149e5b8
 from twisted.web.resource import Resource
 
92ada279
 import abc
d149e5b8
 
ed8582fc
 import copy
d149e5b8
 import UserDict, collections
 collections.Mapping.register(UserDict.DictMixin)
 
8445cb9a
 @public
d149e5b8
 class ServerEvents(object):
1b353cdf
 	'''Subclass this and pass to :py:meth:`JSON_RPC.customize` to customize the JSON-RPC server'''
d149e5b8
 
92ada279
 	DEBUG = False
 
 	#: an object defining a 'get' method which contains the methods
 	methods = None
 
8445cb9a
 	def __init__(self, server):
d149e5b8
 		#: A link to the JSON-RPC server instance
8445cb9a
 		self.server = server
d149e5b8
 
ed8582fc
 	def callmethod(self, txrequest, rpcrequest, **extra):
1b353cdf
 		'''Find the method and call it with the specified args
 
 		:returns: the result of the method'''
92ada279
 
ed8582fc
 		extra.update(rpcrequest.kwargs)
d149e5b8
 
92ada279
 		method, postprocess_result = self.findmethod(rpcrequest.method, rpcrequest.args, extra), False
 		if hasattr('method', '__iter__'):
 			method, postprocess_result = method
 
 		if self.DEBUG:
 			# Debugging: raise AssertionError if type of method is invalid
 			assert method is None or callable(method), 'the returned method is not callable'
 
 		if not callable(method): raise jsonrpc.common.MethodNotFound
 
 		result = method(*rpcrequest.args, **extra)
 
 		#if the result needs to be adjusted/validated, do it
 		if postprocess_result:
 			result = self.methods.postprocess(rpcrequest.method, result, rpcrequest.args, extra)
 
 		return result
d149e5b8
 
92ada279
 	def findmethod(self, method_name, args=None, kwargs=None):
1b353cdf
 		'''Return the callable associated with the method name
 
92ada279
 		:returns: a callable or None if the method is not found'''
 		if self.methods is not None:
7b0a8395
 			return self.methods.get(method_name)
92ada279
 		else:
 			raise NotImplementedError
d149e5b8
 
ed8582fc
 	def processrequest(self, result, args, **kw):
d149e5b8
 		'''Override to implement custom handling of the method result and request'''
 		return result
 
09f3cd17
 	def log(self, response, txrequest, error=False):
1b353cdf
 		'''Override to implement custom logging'''
d149e5b8
 		pass
 
 	def processcontent(self, content, request):
1b353cdf
 		'''Given the freshly decoded content of the request, return the content that should be used
 
 		:returns: an object which implements the :py:class:`collections.MutableMapping` interface'''
d149e5b8
 		return content
 
dcf444bd
 	def getresponsecode(self, result):
1b353cdf
 		'''Take the result, and return an appropriate HTTP response code, returns 200 by default
 
 		NOTE: if an error code is returned, the client error messages will be much less helpful!
 
948c3544
 		for example
 
 			def getresponsecode(self, result):
92ada279
 				code = 200
 				if not isinstance(result, list):
 					if result is not None and result.error is not None:
 						code = result.error.code or 500
 				return code
948c3544
 
 
1b353cdf
 		:returns: :py:class:`int`'''
dcf444bd
 		# returns 200 so that the python client can see something useful
 		return 200
1b353cdf
 
6adb2d95
 	def defer(self, method, *a, **kw):
1b353cdf
 		'''Defer to thread. Override this method if you are using a different ThreadPool, or if you want to return immediately.
 
 		:returns: :py:class:`twisted.internet.defer.Deferred`'''
6adb2d95
 		return threads.deferToThread(method, *a, **kw)
 
92ada279
 	def defer_with_rpcrequest(self, method, rpcrequest, *a, **kw):
7b0a8395
 		d = self.defer(method, rpcrequest, *a, **kw)
92ada279
 
 		@d.addCallback
 		def _inner(result):
 			return result, rpcrequest
 		@d.addErrback
 		def _inner(result):
 			result.rpcrequest = rpcrequest
 			return result
 
 		return d
 
d149e5b8
 
8445cb9a
 
d149e5b8
 ## Base class providing a JSON-RPC 2.0 implementation with 2 customizable hooks
8445cb9a
 @public
d149e5b8
 class JSON_RPC(Resource):
 	'''This class implements a JSON-RPC 2.0 server as a Twisted Resource'''
 	isLeaf = True
 
1b353cdf
 	### NOTE: these comments are used by Sphinx as documentation.
 	#: An instance of :py:class:`ServerEvents` which supplies callbacks to
 	#: customize the operation of the server.  The proper way to initialize this
 	#: is either to subclass and set it manually, or, preferably, to call :py:meth:`customize`.
d149e5b8
 	eventhandler = ServerEvents
 
 	def customize(self, eventhandler):
 		'''customize the behavior of the server'''
 		self.eventhandler = eventhandler(self)
 		return self
 
 
 	def __init__(self, *args, **kwargs):
 		self.customize(self.eventhandler)
 		Resource.__init__(self,*args, **kwargs)
 
 
 	def render(self, request):
5d78ff86
 		result = ''
d149e5b8
 		request.content.seek(0, 0)
 		try:
 			try:
 				content = jsonrpc.jsonutil.decode(request.content.read())
7b0a8395
 			except ValueError:
 				self.eventhandler.log(None, request, True)
 				raise jsonrpc.common.ParseError
3ae840e0
 
d149e5b8
 			content = self.eventhandler.processcontent(content, request)
3ae840e0
 
cc11cbe4
 			content = jsonrpc.common.Request.from_json(content)
8445cb9a
 
 			try:
 				if hasattr(content, 'check'):
 					content.check()
 				else:
 					for item in content: item.check()
 
604c186e
 			except jsonrpc.common.RPCError, e:
8445cb9a
 				self._ebRender(e, request, content.id if hasattr(content, 'id') else None)
 
0bc3c63b
 			else:
18f5a305
 				d = self._action(request, content)
0bc3c63b
 				d.addCallback(self._cbRender, request)
 				d.addErrback(self._ebRender, request, content.id if hasattr(content, 'id') else None)
d149e5b8
 		except BaseException, e:
 			self._ebRender(e, request, None)
 
 		return server.NOT_DONE_YET
 
5d78ff86
 
 
ed8582fc
 	def _action(self, request, contents, **kw):
8445cb9a
 		result = []
 
 		islist = (True if isinstance(contents, list) else False)
 		if not islist: contents = [contents]
 
604c186e
 		if contents == []: raise jsonrpc.common.InvalidRequest
8445cb9a
 
92ada279
 		def callmethod(rpcrequest, request, add, **kwargs):
18f5a305
 			add.update(kwargs)
 			result = self.eventhandler.callmethod(request, rpcrequest, **add)
 			return result
 
 		deferreds = []
8445cb9a
 		for rpcrequest in contents:
 			res = None
18f5a305
 			add = copy.deepcopy(rpcrequest.extra)
 			add.update(kw)
92ada279
 			deferreds.append(self.eventhandler.defer_with_rpcrequest(callmethod, rpcrequest, request, add))
18f5a305
 		deferreds = defer.DeferredList(deferreds, consumeErrors=True)
 
 		@deferreds.addCallback
 		def helper(deferredresults):
 			result = []
92ada279
 			try:
 				for success, methodresult in deferredresults:
 					res = None
 					if success:
 						methodresult, rpcrequest = methodresult
 						res = jsonrpc.common.Response(id=rpcrequest.id, result=methodresult)
 						res = self.eventhandler.processrequest(res, request.args, **kw)
 					else:
 						rpcrequest = methodresult.rpcrequest
 						try:
 							methodresult.raiseException()
 						except Exception, e:
 							res = self.render_error(e, rpcrequest.id)
7b0a8395
 							self.eventhandler.log(res, request, error=True)
92ada279
 
 					if res.id is not None:
 						result.append(res)
 			except Exception, e:
 				traceback.print_exc()
 				raise
8445cb9a
 
18f5a305
 			if result != []:
 				if not islist: result = result[0]
 			else: result = None
 			return result
8445cb9a
 
18f5a305
 		return deferreds
8445cb9a
 
 
d149e5b8
 	def _cbRender(self, result, request):
6adb2d95
 		@self.eventhandler.defer
18f5a305
 		def _inner(*args, **_):
6adb2d95
 			code = self.eventhandler.getresponsecode(result)
dcf444bd
 			request.setResponseCode(code)
18f5a305
 			self.eventhandler.log(result, request, error=False)
 			if result is not None:
 				request.setHeader("content-type", 'application/json')
 				result_ = jsonrpc.jsonutil.encode(result).encode('utf-8')
 				request.setHeader("content-length", len(result_))
 				request.write(result_)
 			request.finish()
 		return _inner
d149e5b8
 
 	def _ebRender(self, result, request, id, finish=True):
6adb2d95
 		@self.eventhandler.defer
18f5a305
 		def _inner(*args, **_):
 			err = None
 			if not isinstance(result, BaseException):
 				try: result.raiseException()
 				except BaseException, e:
 					err = e
 					self.eventhandler.log(err, request, error=True)
 			else: err = result
 			err = self.render_error(err, id)
dcf444bd
 
6adb2d95
 			code = self.eventhandler.getresponsecode(result)
dcf444bd
 			request.setResponseCode(code)
18f5a305
 
 			request.setHeader("content-type", 'application/json')
 			result_ = jsonrpc.jsonutil.encode(err).encode('utf-8')
 			request.setHeader("content-length", len(result_))
 			request.write(result_)
 			if finish: request.finish()
 		return _inner
d149e5b8
 
 	def render_error(self, e, id):
604c186e
 		if isinstance(e, jsonrpc.common.RPCError):
 			err = jsonrpc.common.Response(id=id, error=e)
d149e5b8
 		else:
604c186e
 			err = jsonrpc.common.Response(id=id, error=dict(code=0, message=str(e), data=e.args))
d149e5b8
 
 		return err