604c186e |
#
|
d149e5b8 |
# Copyright (c) 2011 Edward Langley
# All rights reserved.
|
604c186e |
#
|
d149e5b8 |
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
|
604c186e |
#
|
d149e5b8 |
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
|
604c186e |
#
|
d149e5b8 |
# 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.
|
604c186e |
#
|
d149e5b8 |
# 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.
|
604c186e |
#
|
d149e5b8 |
# 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.
|
604c186e |
#
|
d149e5b8 |
#
import copy
|
b5b667ca |
import cookielib
import urllib2
|
d149e5b8 |
import urlparse
import itertools
import traceback
import random
import time
import UserDict, collections
collections.Mapping.register(UserDict.DictMixin)
from hashlib import sha1
import jsonrpc.jsonutil
|
cfdee63b |
from jsonrpc import __version__
|
604c186e |
from jsonrpc.common import Response, Request
|
d149e5b8 |
__all__ = ['JSONRPCProxy', 'ProxyEvents']
class NewStyleBaseException(Exception):
def _get_message(self):
return self._message
def _set_message(self, message):
self._message = message
message = property(_get_message, _set_message)
class IDGen(object):
def __init__(self):
self._hasher = sha1()
self._id = 0
def __get__(self, *_, **__):
self._id += 1
self._hasher.update(str(self._id))
self._hasher.update(time.ctime())
self._hasher.update(str(random.random))
return self._hasher.hexdigest()
class ProxyEvents(object):
'''An event handler for JSONRPCProxy'''
#: an instance of a class which defines a __get__ method, used to generate a request id
|
730c5511 |
IDGen = IDGen()
|
d149e5b8 |
def __init__(self, proxy):
'''Allow a subclass to do its own initialization, gets any arguments leftover from __init__'''
self.proxy = proxy
|
cc11cbe4 |
def get_params(self, args, kwargs):
|
d149e5b8 |
'''allow a subclass to modify the method's arguments
e.g. if an authentication token is necessary, the subclass can automatically insert it into every call'''
return args, kwargs
def proc_response(self, data):
'''allow a subclass to access the response data before it is returned to the user'''
return data
|
cfdee63b |
class JSONRPCProcessor(urllib2.BaseHandler):
def __init__(self):
self.handler_order = 100
def http_request(self, request):
request.add_header('content-type', 'application/json')
request.add_header('user-agent', 'jsonrpc/'+__version__)
return request
|
dd92152e |
https_request = http_request
|
cfdee63b |
|
d149e5b8 |
class JSONRPCProxy(object):
'''A class implementing a JSON-RPC Proxy.
:param str host: The HTTP server hosting the JSON-RPC server
:param str path: The path where the JSON-RPC server can be found
There are two ways of instantiating this class:
- JSONRPCProxy.from_url(url) -- give the absolute url to the JSON-RPC server
- JSONRPC(host, path) -- break up the url into smaller parts
'''
#: Override this attribute to customize proxy behavior
_eventhandler = ProxyEvents
def customize(self, eventhandler):
self._eventhandler = eventhandler(self)
|
cc11cbe4 |
return self
|
d149e5b8 |
def _transformURL(self, serviceURL, path):
|
cc11cbe4 |
if serviceURL.endswith('/'):
|
d149e5b8 |
serviceURL = serviceURL[:-1]
|
cc11cbe4 |
if path.endswith('/'):
path = path[:-1]
if path.startswith('/'):
path = path[1:]
|
d149e5b8 |
return serviceURL, path
## Public interface
@classmethod
def from_url(cls, url, ctxid=None, serviceName=None):
'''Create a JSONRPCProxy from a URL'''
urlsp = urlparse.urlsplit(url)
|
264b3fa1 |
url = '{0}://{1}'.format(urlsp.scheme, urlsp.netloc)
|
d149e5b8 |
path = urlsp.path
|
264b3fa1 |
if urlsp.query: path = '{0}?{1}'.format(path, urlsp.query)
if urlsp.fragment: path = '{0}#{1}'.format(path, urlsp.fragment)
|
d149e5b8 |
return cls(url, path, serviceName, ctxid)
|
cc11cbe4 |
def __init__(self, host, path='jsonrpc', serviceName=None, *args, **kwargs):
|
d149e5b8 |
self.serviceURL = host
self._serviceName = serviceName
self._path = path
self.serviceURL, self._path = self._transformURL(host, path)
self.customize(self._eventhandler)
|
b5b667ca |
cj = cookielib.CookieJar()
self._opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
|
cfdee63b |
self._opener.add_handler(JSONRPCProcessor())
|
d149e5b8 |
|
b5b667ca |
def _set_opener(self, opener):
self._opener = opener
return self
|
d149e5b8 |
def __getattr__(self, name):
if self._serviceName != None:
|
264b3fa1 |
name = "{0}.{1}".format(self._serviceName, name)
|
b5b667ca |
return self.__class__(self.serviceURL, path=self._path, serviceName=name).customize(type(self._eventhandler))._set_opener(self._opener)
|
d149e5b8 |
|
47256d4d |
def _get_postdata(self, args=None, kwargs=None):
|
cc11cbe4 |
_args, _kwargs = self._eventhandler.get_params(args, kwargs)
|
47256d4d |
id = self._eventhandler.IDGen
|
cc11cbe4 |
result = Request(id, self._serviceName, _args, _kwargs)
|
604c186e |
return jsonrpc.jsonutil.encode(result)
|
d149e5b8 |
|
cc11cbe4 |
def _get_url(self):
result = [self.serviceURL]
if self._path:
result.append(self._path)
|
ed95901c |
#result.append('')
|
cc11cbe4 |
return '/'.join(result)
|
d149e5b8 |
|
b5b667ca |
def _post(self, url, data):
return self._opener.open(url, data)
|
d149e5b8 |
def __call__(self, *args, **kwargs):
|
cc11cbe4 |
url = self._get_url()
|
d149e5b8 |
postdata = self._get_postdata(args, kwargs)
|
b5b667ca |
#respdata = urllib2.urlopen(url, postdata).read()
respdata = self._post(url, postdata).read()
|
604c186e |
resp = Response.from_dict(jsonrpc.jsonutil.decode(respdata))
resp = self._eventhandler.proc_response(resp)
|
d149e5b8 |
|
604c186e |
return resp.get_result()
|
d149e5b8 |
def call(self, method, *args, **kwargs):
'''call a JSON-RPC method
It's better to use instance.<methodname>(\\*args, \\*\\*kwargs),
but this version might be useful occasionally
'''
p = self.__class__(self.serviceURL, path=self._path, serviceName=method)
return p(*args, **kwargs)
def batch_call(self, methods):
'''call several methods at once, return a list of (result, error) pairs
:param names: a dictionary { method: (args, kwargs) }
:returns: a list of pairs (result, error) where only one is not None
'''
|
604c186e |
result = None
|
d149e5b8 |
if hasattr(methods, 'items'): methods = methods.items()
data = [ getattr(self, k)._get_postdata(*v) for k, v in methods ]
|
264b3fa1 |
postdata = '{0}'.format(','.join(data))
|
d0c856ab |
respdata = self._post(self._get_url(), postdata).read()
|
604c186e |
resp = Response.from_json(respdata)
try:
result = resp.get_result()
except AttributeError:
result = [res.get_output() for res in resp]
return result
|