mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 08:53:37 +00:00
557 lines
17 KiB
Python
557 lines
17 KiB
Python
|
"""
|
||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
you may not use this file except in compliance with the License.
|
||
|
You may obtain a copy of the License at
|
||
|
|
||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
||
|
Unless required by applicable law or agreed to in writing, software
|
||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
See the License for the specific language governing permissions and
|
||
|
limitations under the License.
|
||
|
|
||
|
============================
|
||
|
JSONRPC Library (jsonrpclib)
|
||
|
============================
|
||
|
|
||
|
This library is a JSON-RPC v.2 (proposed) implementation which
|
||
|
follows the xmlrpclib API for portability between clients. It
|
||
|
uses the same Server / ServerProxy, loads, dumps, etc. syntax,
|
||
|
while providing features not present in XML-RPC like:
|
||
|
|
||
|
* Keyword arguments
|
||
|
* Notifications
|
||
|
* Versioning
|
||
|
* Batches and batch notifications
|
||
|
|
||
|
Eventually, I'll add a SimpleXMLRPCServer compatible library,
|
||
|
and other things to tie the thing off nicely. :)
|
||
|
|
||
|
For a quick-start, just open a console and type the following,
|
||
|
replacing the server address, method, and parameters
|
||
|
appropriately.
|
||
|
>>> import jsonrpclib
|
||
|
>>> server = jsonrpclib.Server('http://localhost:8181')
|
||
|
>>> server.add(5, 6)
|
||
|
11
|
||
|
>>> server._notify.add(5, 6)
|
||
|
>>> batch = jsonrpclib.MultiCall(server)
|
||
|
>>> batch.add(3, 50)
|
||
|
>>> batch.add(2, 3)
|
||
|
>>> batch._notify.add(3, 5)
|
||
|
>>> batch()
|
||
|
[53, 5]
|
||
|
|
||
|
See http://code.google.com/p/jsonrpclib/ for more info.
|
||
|
"""
|
||
|
|
||
|
import types
|
||
|
import sys
|
||
|
from xmlrpclib import Transport as XMLTransport
|
||
|
from xmlrpclib import SafeTransport as XMLSafeTransport
|
||
|
from xmlrpclib import ServerProxy as XMLServerProxy
|
||
|
from xmlrpclib import _Method as XML_Method
|
||
|
import time
|
||
|
import string
|
||
|
import random
|
||
|
|
||
|
# Library includes
|
||
|
import lib.jsonrpclib
|
||
|
from lib.jsonrpclib import config
|
||
|
from lib.jsonrpclib import history
|
||
|
|
||
|
# JSON library importing
|
||
|
cjson = None
|
||
|
json = None
|
||
|
try:
|
||
|
import cjson
|
||
|
except ImportError:
|
||
|
try:
|
||
|
import json
|
||
|
except ImportError:
|
||
|
try:
|
||
|
import lib.simplejson as json
|
||
|
except ImportError:
|
||
|
raise ImportError(
|
||
|
'You must have the cjson, json, or simplejson ' +
|
||
|
'module(s) available.'
|
||
|
)
|
||
|
|
||
|
IDCHARS = string.ascii_lowercase+string.digits
|
||
|
|
||
|
class UnixSocketMissing(Exception):
|
||
|
"""
|
||
|
Just a properly named Exception if Unix Sockets usage is
|
||
|
attempted on a platform that doesn't support them (Windows)
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
#JSON Abstractions
|
||
|
|
||
|
def jdumps(obj, encoding='utf-8'):
|
||
|
# Do 'serialize' test at some point for other classes
|
||
|
global cjson
|
||
|
if cjson:
|
||
|
return cjson.encode(obj)
|
||
|
else:
|
||
|
return json.dumps(obj, encoding=encoding)
|
||
|
|
||
|
def jloads(json_string):
|
||
|
global cjson
|
||
|
if cjson:
|
||
|
return cjson.decode(json_string)
|
||
|
else:
|
||
|
return json.loads(json_string)
|
||
|
|
||
|
|
||
|
# XMLRPClib re-implementations
|
||
|
|
||
|
class ProtocolError(Exception):
|
||
|
pass
|
||
|
|
||
|
class TransportMixIn(object):
|
||
|
""" Just extends the XMLRPC transport where necessary. """
|
||
|
user_agent = config.user_agent
|
||
|
# for Python 2.7 support
|
||
|
_connection = None
|
||
|
|
||
|
def send_content(self, connection, request_body):
|
||
|
connection.putheader("Content-Type", "application/json-rpc")
|
||
|
connection.putheader("Content-Length", str(len(request_body)))
|
||
|
connection.endheaders()
|
||
|
if request_body:
|
||
|
connection.send(request_body)
|
||
|
|
||
|
def getparser(self):
|
||
|
target = JSONTarget()
|
||
|
return JSONParser(target), target
|
||
|
|
||
|
class JSONParser(object):
|
||
|
def __init__(self, target):
|
||
|
self.target = target
|
||
|
|
||
|
def feed(self, data):
|
||
|
self.target.feed(data)
|
||
|
|
||
|
def close(self):
|
||
|
pass
|
||
|
|
||
|
class JSONTarget(object):
|
||
|
def __init__(self):
|
||
|
self.data = []
|
||
|
|
||
|
def feed(self, data):
|
||
|
self.data.append(data)
|
||
|
|
||
|
def close(self):
|
||
|
return ''.join(self.data)
|
||
|
|
||
|
class Transport(TransportMixIn, XMLTransport):
|
||
|
pass
|
||
|
|
||
|
class SafeTransport(TransportMixIn, XMLSafeTransport):
|
||
|
pass
|
||
|
from httplib import HTTP, HTTPConnection
|
||
|
from socket import socket
|
||
|
|
||
|
USE_UNIX_SOCKETS = False
|
||
|
|
||
|
try:
|
||
|
from socket import AF_UNIX, SOCK_STREAM
|
||
|
USE_UNIX_SOCKETS = True
|
||
|
except ImportError:
|
||
|
pass
|
||
|
|
||
|
if (USE_UNIX_SOCKETS):
|
||
|
|
||
|
class UnixHTTPConnection(HTTPConnection):
|
||
|
def connect(self):
|
||
|
self.sock = socket(AF_UNIX, SOCK_STREAM)
|
||
|
self.sock.connect(self.host)
|
||
|
|
||
|
class UnixHTTP(HTTP):
|
||
|
_connection_class = UnixHTTPConnection
|
||
|
|
||
|
class UnixTransport(TransportMixIn, XMLTransport):
|
||
|
def make_connection(self, host):
|
||
|
import httplib
|
||
|
host, extra_headers, x509 = self.get_host_info(host)
|
||
|
return UnixHTTP(host)
|
||
|
|
||
|
|
||
|
class ServerProxy(XMLServerProxy):
|
||
|
"""
|
||
|
Unfortunately, much more of this class has to be copied since
|
||
|
so much of it does the serialization.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, uri, transport=None, encoding=None,
|
||
|
verbose=0, version=None):
|
||
|
import urllib
|
||
|
if not version:
|
||
|
version = config.version
|
||
|
self.__version = version
|
||
|
schema, uri = urllib.splittype(uri)
|
||
|
if schema not in ('http', 'https', 'unix'):
|
||
|
raise IOError('Unsupported JSON-RPC protocol.')
|
||
|
if schema == 'unix':
|
||
|
if not USE_UNIX_SOCKETS:
|
||
|
# Don't like the "generic" Exception...
|
||
|
raise UnixSocketMissing("Unix sockets not available.")
|
||
|
self.__host = uri
|
||
|
self.__handler = '/'
|
||
|
else:
|
||
|
self.__host, self.__handler = urllib.splithost(uri)
|
||
|
if not self.__handler:
|
||
|
# Not sure if this is in the JSON spec?
|
||
|
#self.__handler = '/'
|
||
|
self.__handler == '/'
|
||
|
if transport is None:
|
||
|
if schema == 'unix':
|
||
|
transport = UnixTransport()
|
||
|
elif schema == 'https':
|
||
|
transport = SafeTransport()
|
||
|
else:
|
||
|
transport = Transport()
|
||
|
self.__transport = transport
|
||
|
self.__encoding = encoding
|
||
|
self.__verbose = verbose
|
||
|
|
||
|
def _request(self, methodname, params, rpcid=None):
|
||
|
request = dumps(params, methodname, encoding=self.__encoding,
|
||
|
rpcid=rpcid, version=self.__version)
|
||
|
response = self._run_request(request)
|
||
|
check_for_errors(response)
|
||
|
return response['result']
|
||
|
|
||
|
def _request_notify(self, methodname, params, rpcid=None):
|
||
|
request = dumps(params, methodname, encoding=self.__encoding,
|
||
|
rpcid=rpcid, version=self.__version, notify=True)
|
||
|
response = self._run_request(request, notify=True)
|
||
|
check_for_errors(response)
|
||
|
return
|
||
|
|
||
|
def _run_request(self, request, notify=None):
|
||
|
history.add_request(request)
|
||
|
|
||
|
response = self.__transport.request(
|
||
|
self.__host,
|
||
|
self.__handler,
|
||
|
request,
|
||
|
verbose=self.__verbose
|
||
|
)
|
||
|
|
||
|
# Here, the XMLRPC library translates a single list
|
||
|
# response to the single value -- should we do the
|
||
|
# same, and require a tuple / list to be passed to
|
||
|
# the response object, or expect the Server to be
|
||
|
# outputting the response appropriately?
|
||
|
|
||
|
history.add_response(response)
|
||
|
if not response:
|
||
|
return None
|
||
|
return_obj = loads(response)
|
||
|
return return_obj
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
# Same as original, just with new _Method reference
|
||
|
return _Method(self._request, name)
|
||
|
|
||
|
@property
|
||
|
def _notify(self):
|
||
|
# Just like __getattr__, but with notify namespace.
|
||
|
return _Notify(self._request_notify)
|
||
|
|
||
|
|
||
|
class _Method(XML_Method):
|
||
|
|
||
|
def __call__(self, *args, **kwargs):
|
||
|
if len(args) > 0 and len(kwargs) > 0:
|
||
|
raise ProtocolError('Cannot use both positional ' +
|
||
|
'and keyword arguments (according to JSON-RPC spec.)')
|
||
|
if len(args) > 0:
|
||
|
return self.__send(self.__name, args)
|
||
|
else:
|
||
|
return self.__send(self.__name, kwargs)
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
self.__name = '%s.%s' % (self.__name, name)
|
||
|
return self
|
||
|
# The old method returned a new instance, but this seemed wasteful.
|
||
|
# The only thing that changes is the name.
|
||
|
#return _Method(self.__send, "%s.%s" % (self.__name, name))
|
||
|
|
||
|
class _Notify(object):
|
||
|
def __init__(self, request):
|
||
|
self._request = request
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
return _Method(self._request, name)
|
||
|
|
||
|
# Batch implementation
|
||
|
|
||
|
class MultiCallMethod(object):
|
||
|
|
||
|
def __init__(self, method, notify=False):
|
||
|
self.method = method
|
||
|
self.params = []
|
||
|
self.notify = notify
|
||
|
|
||
|
def __call__(self, *args, **kwargs):
|
||
|
if len(kwargs) > 0 and len(args) > 0:
|
||
|
raise ProtocolError('JSON-RPC does not support both ' +
|
||
|
'positional and keyword arguments.')
|
||
|
if len(kwargs) > 0:
|
||
|
self.params = kwargs
|
||
|
else:
|
||
|
self.params = args
|
||
|
|
||
|
def request(self, encoding=None, rpcid=None):
|
||
|
return dumps(self.params, self.method, version=2.0,
|
||
|
encoding=encoding, rpcid=rpcid, notify=self.notify)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '%s' % self.request()
|
||
|
|
||
|
def __getattr__(self, method):
|
||
|
new_method = '%s.%s' % (self.method, method)
|
||
|
self.method = new_method
|
||
|
return self
|
||
|
|
||
|
class MultiCallNotify(object):
|
||
|
|
||
|
def __init__(self, multicall):
|
||
|
self.multicall = multicall
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
new_job = MultiCallMethod(name, notify=True)
|
||
|
self.multicall._job_list.append(new_job)
|
||
|
return new_job
|
||
|
|
||
|
class MultiCallIterator(object):
|
||
|
|
||
|
def __init__(self, results):
|
||
|
self.results = results
|
||
|
|
||
|
def __iter__(self):
|
||
|
for i in range(0, len(self.results)):
|
||
|
yield self[i]
|
||
|
raise StopIteration
|
||
|
|
||
|
def __getitem__(self, i):
|
||
|
item = self.results[i]
|
||
|
check_for_errors(item)
|
||
|
return item['result']
|
||
|
|
||
|
def __len__(self):
|
||
|
return len(self.results)
|
||
|
|
||
|
class MultiCall(object):
|
||
|
|
||
|
def __init__(self, server):
|
||
|
self._server = server
|
||
|
self._job_list = []
|
||
|
|
||
|
def _request(self):
|
||
|
if len(self._job_list) < 1:
|
||
|
# Should we alert? This /is/ pretty obvious.
|
||
|
return
|
||
|
request_body = '[ %s ]' % ','.join([job.request() for
|
||
|
job in self._job_list])
|
||
|
responses = self._server._run_request(request_body)
|
||
|
del self._job_list[:]
|
||
|
if not responses:
|
||
|
responses = []
|
||
|
return MultiCallIterator(responses)
|
||
|
|
||
|
@property
|
||
|
def _notify(self):
|
||
|
return MultiCallNotify(self)
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
new_job = MultiCallMethod(name)
|
||
|
self._job_list.append(new_job)
|
||
|
return new_job
|
||
|
|
||
|
__call__ = _request
|
||
|
|
||
|
# These lines conform to xmlrpclib's "compatibility" line.
|
||
|
# Not really sure if we should include these, but oh well.
|
||
|
Server = ServerProxy
|
||
|
|
||
|
class Fault(object):
|
||
|
# JSON-RPC error class
|
||
|
def __init__(self, code=-32000, message='Server error', rpcid=None):
|
||
|
self.faultCode = code
|
||
|
self.faultString = message
|
||
|
self.rpcid = rpcid
|
||
|
|
||
|
def error(self):
|
||
|
return {'code':self.faultCode, 'message':self.faultString}
|
||
|
|
||
|
def response(self, rpcid=None, version=None):
|
||
|
if not version:
|
||
|
version = config.version
|
||
|
if rpcid:
|
||
|
self.rpcid = rpcid
|
||
|
return dumps(
|
||
|
self, methodresponse=True, rpcid=self.rpcid, version=version
|
||
|
)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<Fault %s: %s>' % (self.faultCode, self.faultString)
|
||
|
|
||
|
def random_id(length=8):
|
||
|
return_id = ''
|
||
|
for i in range(length):
|
||
|
return_id += random.choice(IDCHARS)
|
||
|
return return_id
|
||
|
|
||
|
class Payload(dict):
|
||
|
def __init__(self, rpcid=None, version=None):
|
||
|
if not version:
|
||
|
version = config.version
|
||
|
self.id = rpcid
|
||
|
self.version = float(version)
|
||
|
|
||
|
def request(self, method, params=[]):
|
||
|
if type(method) not in types.StringTypes:
|
||
|
raise ValueError('Method name must be a string.')
|
||
|
if not self.id:
|
||
|
self.id = random_id()
|
||
|
request = { 'id':self.id, 'method':method }
|
||
|
if params:
|
||
|
request['params'] = params
|
||
|
if self.version >= 2:
|
||
|
request['jsonrpc'] = str(self.version)
|
||
|
return request
|
||
|
|
||
|
def notify(self, method, params=[]):
|
||
|
request = self.request(method, params)
|
||
|
if self.version >= 2:
|
||
|
del request['id']
|
||
|
else:
|
||
|
request['id'] = None
|
||
|
return request
|
||
|
|
||
|
def response(self, result=None):
|
||
|
response = {'result':result, 'id':self.id}
|
||
|
if self.version >= 2:
|
||
|
response['jsonrpc'] = str(self.version)
|
||
|
else:
|
||
|
response['error'] = None
|
||
|
return response
|
||
|
|
||
|
def error(self, code=-32000, message='Server error.'):
|
||
|
error = self.response()
|
||
|
if self.version >= 2:
|
||
|
del error['result']
|
||
|
else:
|
||
|
error['result'] = None
|
||
|
error['error'] = {'code':code, 'message':message}
|
||
|
return error
|
||
|
|
||
|
def dumps(params=[], methodname=None, methodresponse=None,
|
||
|
encoding=None, rpcid=None, version=None, notify=None):
|
||
|
"""
|
||
|
This differs from the Python implementation in that it implements
|
||
|
the rpcid argument since the 2.0 spec requires it for responses.
|
||
|
"""
|
||
|
if not version:
|
||
|
version = config.version
|
||
|
valid_params = (types.TupleType, types.ListType, types.DictType)
|
||
|
if methodname in types.StringTypes and \
|
||
|
type(params) not in valid_params and \
|
||
|
not isinstance(params, Fault):
|
||
|
"""
|
||
|
If a method, and params are not in a listish or a Fault,
|
||
|
error out.
|
||
|
"""
|
||
|
raise TypeError('Params must be a dict, list, tuple or Fault ' +
|
||
|
'instance.')
|
||
|
# Begin parsing object
|
||
|
payload = Payload(rpcid=rpcid, version=version)
|
||
|
if not encoding:
|
||
|
encoding = 'utf-8'
|
||
|
if type(params) is Fault:
|
||
|
response = payload.error(params.faultCode, params.faultString)
|
||
|
return jdumps(response, encoding=encoding)
|
||
|
if type(methodname) not in types.StringTypes and methodresponse != True:
|
||
|
raise ValueError('Method name must be a string, or methodresponse '+
|
||
|
'must be set to True.')
|
||
|
if config.use_jsonclass == True:
|
||
|
from lib.jsonrpclib import jsonclass
|
||
|
params = jsonclass.dump(params)
|
||
|
if methodresponse is True:
|
||
|
if rpcid is None:
|
||
|
raise ValueError('A method response must have an rpcid.')
|
||
|
response = payload.response(params)
|
||
|
return jdumps(response, encoding=encoding)
|
||
|
request = None
|
||
|
if notify == True:
|
||
|
request = payload.notify(methodname, params)
|
||
|
else:
|
||
|
request = payload.request(methodname, params)
|
||
|
return jdumps(request, encoding=encoding)
|
||
|
|
||
|
def loads(data):
|
||
|
"""
|
||
|
This differs from the Python implementation, in that it returns
|
||
|
the request structure in Dict format instead of the method, params.
|
||
|
It will return a list in the case of a batch request / response.
|
||
|
"""
|
||
|
if data == '':
|
||
|
# notification
|
||
|
return None
|
||
|
result = jloads(data)
|
||
|
# if the above raises an error, the implementing server code
|
||
|
# should return something like the following:
|
||
|
# { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
|
||
|
if config.use_jsonclass == True:
|
||
|
from lib.jsonrpclib import jsonclass
|
||
|
result = jsonclass.load(result)
|
||
|
return result
|
||
|
|
||
|
def check_for_errors(result):
|
||
|
if not result:
|
||
|
# Notification
|
||
|
return result
|
||
|
if type(result) is not types.DictType:
|
||
|
raise TypeError('Response is not a dict.')
|
||
|
if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
|
||
|
raise NotImplementedError('JSON-RPC version not yet supported.')
|
||
|
if 'result' not in result.keys() and 'error' not in result.keys():
|
||
|
raise ValueError('Response does not have a result or error key.')
|
||
|
if 'error' in result.keys() and result['error'] != None:
|
||
|
code = result['error']['code']
|
||
|
message = result['error']['message']
|
||
|
raise ProtocolError((code, message))
|
||
|
return result
|
||
|
|
||
|
def isbatch(result):
|
||
|
if type(result) not in (types.ListType, types.TupleType):
|
||
|
return False
|
||
|
if len(result) < 1:
|
||
|
return False
|
||
|
if type(result[0]) is not types.DictType:
|
||
|
return False
|
||
|
if 'jsonrpc' not in result[0].keys():
|
||
|
return False
|
||
|
try:
|
||
|
version = float(result[0]['jsonrpc'])
|
||
|
except ValueError:
|
||
|
raise ProtocolError('"jsonrpc" key must be a float(able) value.')
|
||
|
if version < 2:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def isnotification(request):
|
||
|
if 'id' not in request.keys():
|
||
|
# 2.0 notification
|
||
|
return True
|
||
|
if request['id'] == None:
|
||
|
# 1.0 notification
|
||
|
return True
|
||
|
return False
|