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
|