import inspect import json import logging import uuid _LOGGER = logging.getLogger(__name__) class JSONRPCError(Exception): """Root exception for all errors related to this library""" class TransportError(JSONRPCError): """An error occurred while performing a connection to the server""" def __init__(self, exception_text, message=None, *args): """Create the transport error for the attempted message.""" if message: super(TransportError, self).__init__( '%s: %s' % (message.transport_error_text, exception_text), *args) else: super(TransportError, self).__init__(exception_text, *args) class ProtocolError(JSONRPCError): """An error occurred while dealing with the JSON-RPC protocol""" class Server(object): """A connection to a JSON-RPC server""" def __init__(self): """Initialize the json-rpc server object.""" self._server_request_handlers = {} def send_message(self, message): """Issue the request to the server and return the method result. This method must be implemented by the child class. """ raise NotImplementedError() def receive_request(self, request): """Called when a request is received from the server.""" result = None error = None args, kwargs = request.get_args() if request.method in self._server_request_handlers: try: handler = self._server_request_handlers[request.method] if inspect.iscoroutinefunction(handler): raise TypeError( "Async handlers are not supported in " "synchronous sever implementations") else: result = handler(*args, **kwargs) except Exception as exc: _LOGGER.error(exc, exc_info=exc) error = { 'code': -32000, 'message': 'Server Error: %s' % exc, } else: error = { 'code': -32601, 'message': 'Method not found', } if request.msg_id is not None: return Response(request, result, error) else: return None async def async_receive_request(self, request): """Called when a request is received from the server. If the implementation calls async_receive_request instead of receive_request, asynchronous request handlers are also supported """ result = None error = None args, kwargs = request.get_args() if request.method in self._server_request_handlers: try: handler = self._server_request_handlers[request.method] if inspect.iscoroutinefunction(handler): result = await handler(*args, **kwargs) else: result = handler(*args, **kwargs) except Exception as exc: _LOGGER.error(exc, exc_info=exc) error = { 'code': -32000, 'message': 'Server Error: %s' % exc, } else: error = { 'code': -32601, 'message': 'Method not found', } if request.msg_id is not None: return Response(request, result, error) else: return None def __getattr__(self, method_name): if method_name.startswith("_"): # prevent calls for private methods raise AttributeError("invalid attribute '%s'" % method_name) return Method(self.__request, self.__register, method_name) def __setattr__(self, method_name, callback): if method_name.startswith("_"): # prevent calls for private methods return super(Server, self).__setattr__(method_name, callback) return self.__register(method_name, callback) def __request(self, method_name, args=None, kwargs=None): """Perform the actual RPC call. If _notification=True, don't wait for a response """ if kwargs.pop('_notification', False): msg_id = None else: msg_id = str(uuid.uuid4()) if args and kwargs: raise ProtocolError( 'JSON-RPC spec forbids mixing arguments and keyword arguments') return self.send_message(Request(method_name, args or kwargs, msg_id)) def __register(self, method_name, callback): """Register a callback for if the server sends this request.""" self._server_request_handlers[method_name] = callback class Message(object): """Message to be sent to the jsonrpc server.""" @property def response_id(self): return None def serialize(self): """Generate the raw JSON message to be sent to the server""" raise NotImplementedError() def parse_response(self, response): """Parse the response from the server and return the result.""" raise NotImplementedError() @property def transport_error_text(self): """Exception text for a transport error.""" raise NotImplementedError() def __str__(self): return self.serialize() class Request(Message): """Request a method call on the server.""" def __init__(self, method=None, params=None, msg_id=None): self.method = method self.params = params self.msg_id = msg_id @staticmethod def parse(data): """Generate a request object by parsing the json data.""" if 'method' not in data: raise ProtocolError('Request from server does not contain method') method = data.get('method') params = data.get('params') msg_id = data.get('id') if ( not isinstance(params, list) and not isinstance(params, dict) and params is not None): raise ProtocolError( 'Parameters must either be a positional list or named dict.') return Request(method, params, msg_id) @property def response_id(self): return self.msg_id def serialize(self): """Generate the raw JSON message to be sent to the server""" data = {'jsonrpc': '2.0', 'method': self.method} if self.params: data['params'] = self.params if self.msg_id is not None: data['id'] = self.msg_id return json.dumps(data) def parse_response(self, data): """Parse the response from the server and return the result.""" if self.msg_id is None: # Don't parse results for notification requests return None if not isinstance(data, dict): raise ProtocolError('Response is not a dictionary') if data.get('error') is not None: code = data['error'].get('code', '') message = data['error'].get('message', '') raise ProtocolError(code, message, data) elif 'result' not in data: raise ProtocolError('Response without a result field') else: return data['result'] @property def transport_error_text(self): """Exception text for a transport error.""" return 'Error calling method %r' % self.method def get_args(self): """Transform the request parameters into args/kwargs""" args = [] kwargs = {} if isinstance(self.params, list): args = self.params elif isinstance(self.params, dict): kwargs = self.params elif self.params is not None: raise ProtocolError( 'Parameters must either be a positional list or named dict.') return args, kwargs class Response(Message): """Respond to a method call from the server.""" def __init__(self, request, result=None, error=None): self.request = request self.result = result self.error = error def serialize(self): """Generate the raw JSON message to be sent to the server""" data = {'jsonrpc': '2.0', 'id': self.request.msg_id} if self.error is not None: data['error'] = self.error else: data['result'] = self.result return json.dumps(data) def parse_response(self, response): """Parse the response from the server and return the result.""" # Don't parse results from response messages return None @property def transport_error_text(self): """Exception text for a transport error.""" return 'Error responding to server method %r' % self.request.method class Method(object): """Map the methods called on the server to json-rpc methods.""" def __init__(self, request_method, register_method, method_name): self.__request_method = request_method self.__register_method = register_method self.__method_name = method_name def __getattr__(self, method_name): if method_name.startswith("_"): # prevent calls for private methods raise AttributeError("invalid attribute '%s'" % method_name) return Method(self.__request_method, self.__register_method, "%s.%s" % (self.__method_name, method_name)) def __call__(self, *args, **kwargs): return self.__request_method(self.__method_name, args, kwargs) def __setattr__(self, method_name, callback): if method_name.startswith("_"): # prevent calls for private methods return super(Method, self).__setattr__(method_name, callback) return self.__register_method( "%s.%s" % (self.__method_name, method_name), callback)