git.fiddlerwoaroof.com
jsonrpc/proxy.py
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