from . import six import json from .exceptions import JSONRPCError, JSONRPCInvalidRequestException from .base import JSONRPCBaseRequest, JSONRPCBaseResponse class JSONRPC20Request(JSONRPCBaseRequest): """ A rpc call is represented by sending a Request object to a Server. :param str method: A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else. :param params: A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted. :type params: iterable or dict :param _id: An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2]. :type _id: str or int or None :param bool is_notification: Whether request is notification or not. If value is True, _id is not included to request. It allows to create requests with id = null. The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects. [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling. [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions. """ JSONRPC_VERSION = "2.0" REQUIRED_FIELDS = set(["jsonrpc", "method"]) POSSIBLE_FIELDS = set(["jsonrpc", "method", "params", "id"]) @property def data(self): data = dict( (k, v) for k, v in self._data.items() if not (k == "id" and self.is_notification) ) data["jsonrpc"] = self.JSONRPC_VERSION return data @data.setter def data(self, value): if not isinstance(value, dict): raise ValueError("data should be dict") self._data = value @property def method(self): return self._data.get("method") @method.setter def method(self, value): if not isinstance(value, six.string_types): raise ValueError("Method should be string") if value.startswith("rpc."): raise ValueError( "Method names that begin with the word rpc followed by a " + "period character (U+002E or ASCII 46) are reserved for " + "rpc-internal methods and extensions and MUST NOT be used " + "for anything else.") self._data["method"] = str(value) @property def params(self): return self._data.get("params") @params.setter def params(self, value): if value is not None and not isinstance(value, (list, tuple, dict)): raise ValueError("Incorrect params {0}".format(value)) value = list(value) if isinstance(value, tuple) else value if value is not None: self._data["params"] = value @property def _id(self): return self._data.get("id") @_id.setter def _id(self, value): if value is not None and \ not isinstance(value, six.string_types + six.integer_types): raise ValueError("id should be string or integer") self._data["id"] = value @classmethod def from_json(cls, json_str): data = cls.deserialize(json_str) return cls.from_data(data) @classmethod def from_data(cls, data): is_batch = isinstance(data, list) data = data if is_batch else [data] if not data: raise JSONRPCInvalidRequestException("[] value is not accepted") if not all(isinstance(d, dict) for d in data): raise JSONRPCInvalidRequestException( "Each request should be an object (dict)") result = [] for d in data: if not cls.REQUIRED_FIELDS <= set(d.keys()) <= cls.POSSIBLE_FIELDS: extra = set(d.keys()) - cls.POSSIBLE_FIELDS missed = cls.REQUIRED_FIELDS - set(d.keys()) msg = "Invalid request. Extra fields: {0}, Missed fields: {1}" raise JSONRPCInvalidRequestException(msg.format(extra, missed)) try: result.append(JSONRPC20Request( method=d["method"], params=d.get("params"), _id=d.get("id"), is_notification="id" not in d, )) except ValueError as e: raise JSONRPCInvalidRequestException(str(e)) return JSONRPC20BatchRequest(*result) if is_batch else result[0] class JSONRPC20BatchRequest(object): """ Batch JSON-RPC 2.0 Request. :param JSONRPC20Request *requests: requests """ JSONRPC_VERSION = "2.0" def __init__(self, *requests): self.requests = requests @classmethod def from_json(cls, json_str): return JSONRPC20Request.from_json(json_str) @property def json(self): return json.dumps([r.data for r in self.requests]) def __iter__(self): return iter(self.requests) class JSONRPC20Response(JSONRPCBaseResponse): """ JSON-RPC response object to JSONRPC20Request. When a rpc call is made, the Server MUST reply with a Response, except for in the case of Notifications. The Response is expressed as a single JSON Object, with the following members: :param str jsonrpc: A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". :param result: This member is REQUIRED on success. This member MUST NOT exist if there was an error invoking the method. The value of this member is determined by the method invoked on the Server. :param dict error: This member is REQUIRED on error. This member MUST NOT exist if there was no error triggered during invocation. The value for this member MUST be an Object. :param id: This member is REQUIRED. It MUST be the same as the value of the id member in the Request Object. If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null. :type id: str or int or None Either the result member or error member MUST be included, but both members MUST NOT be included. """ JSONRPC_VERSION = "2.0" @property def data(self): data = dict((k, v) for k, v in self._data.items()) data["jsonrpc"] = self.JSONRPC_VERSION return data @data.setter def data(self, value): if not isinstance(value, dict): raise ValueError("data should be dict") self._data = value @property def result(self): return self._data.get("result") @result.setter def result(self, value): if self.error: raise ValueError("Either result or error should be used") self._data["result"] = value @property def error(self): return self._data.get("error") @error.setter def error(self, value): self._data.pop('value', None) if value: self._data["error"] = value # Test error JSONRPCError(**value) @property def _id(self): return self._data.get("id") @_id.setter def _id(self, value): if value is not None and \ not isinstance(value, six.string_types + six.integer_types): raise ValueError("id should be string or integer") self._data["id"] = value class JSONRPC20BatchResponse(object): JSONRPC_VERSION = "2.0" def __init__(self, *responses): self.responses = responses self.request = None # type: JSONRPC20BatchRequest @property def data(self): return [r.data for r in self.responses] @property def json(self): return json.dumps(self.data) def __iter__(self): return iter(self.responses)