build: Initialize Python virtual environment and install project dependencies.
Some checks failed
WLED CI / wled_build (push) Has been cancelled
Some checks failed
WLED CI / wled_build (push) Has been cancelled
This commit is contained in:
5
.venv/lib/python3.11/site-packages/ajsonrpc/__init__.py
Normal file
5
.venv/lib/python3.11/site-packages/ajsonrpc/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .dispatcher import Dispatcher
|
||||
from .manager import AsyncJSONRPCResponseManager
|
||||
|
||||
__version__ = "0.0.0" # replaced with release tag in GitHub action
|
||||
__version__ = "1.2.0"
|
||||
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
from ..dispatcher import Dispatcher
|
||||
from ..manager import AsyncJSONRPCResponseManager
|
||||
|
||||
class CommonBackend:
|
||||
def __init__(self, serialize=json.dumps, deserialize=json.loads):
|
||||
self.manager = AsyncJSONRPCResponseManager(
|
||||
Dispatcher(),
|
||||
serialize=serialize,
|
||||
deserialize=deserialize
|
||||
)
|
||||
|
||||
def add_class(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_class(*args, **kwargs)
|
||||
|
||||
def add_object(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_object(*args, **kwargs)
|
||||
|
||||
def add_function(self, *args, **kwargs):
|
||||
return self.manager.dispatcher.add_function(*args, **kwargs)
|
||||
17
.venv/lib/python3.11/site-packages/ajsonrpc/backend/quart.py
Normal file
17
.venv/lib/python3.11/site-packages/ajsonrpc/backend/quart.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import json
|
||||
|
||||
from quart import Response, request
|
||||
from .common import CommonBackend
|
||||
|
||||
|
||||
class JSONRPCQuart(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Quart Handler"""
|
||||
|
||||
async def handle():
|
||||
request_body = await request.body
|
||||
response = await self.manager.get_response_for_payload(request_body)
|
||||
return Response(json.dumps(response.body), mimetype="application/json")
|
||||
|
||||
return handle
|
||||
12
.venv/lib/python3.11/site-packages/ajsonrpc/backend/sanic.py
Normal file
12
.venv/lib/python3.11/site-packages/ajsonrpc/backend/sanic.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from sanic.response import json as json_response
|
||||
from .common import CommonBackend
|
||||
|
||||
class JSONRPCSanic(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Sanic Handler"""
|
||||
async def handle(request):
|
||||
response = await self.manager.get_response_for_payload(request.body)
|
||||
return json_response(response.body, dumps=self.manager.serialize)
|
||||
|
||||
return handle
|
||||
@@ -0,0 +1,18 @@
|
||||
import tornado.web
|
||||
from .common import CommonBackend
|
||||
|
||||
|
||||
class JSONRPCTornado(CommonBackend):
|
||||
@property
|
||||
def handler(self):
|
||||
"""Get Tornado Handler"""
|
||||
manager = self.manager
|
||||
|
||||
class JSONRPCTornadoHandler(tornado.web.RequestHandler):
|
||||
async def post(self):
|
||||
self.set_header("Content-Type", "application/json")
|
||||
payload = await manager.get_payload_for_payload(self.request.body)
|
||||
self.write(payload)
|
||||
|
||||
return JSONRPCTornadoHandler
|
||||
|
||||
609
.venv/lib/python3.11/site-packages/ajsonrpc/core.py
Normal file
609
.venv/lib/python3.11/site-packages/ajsonrpc/core.py
Normal file
@@ -0,0 +1,609 @@
|
||||
from typing import Union, Optional, Any, Iterable, Mapping, List, Dict
|
||||
from numbers import Number
|
||||
import warnings
|
||||
import collections.abc
|
||||
|
||||
|
||||
class JSONRPC20RequestIdWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20ResponseIdWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20Request:
|
||||
"""JSON-RPC 2.0 Request object.
|
||||
|
||||
A rpc call is represented by sending a Request object to a Server.
|
||||
The Request object has the following members:
|
||||
|
||||
jsonrpc
|
||||
A String specifying the version of the JSON-RPC protocol. MUST be
|
||||
exactly "2.0".
|
||||
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.
|
||||
params
|
||||
A Structured value that holds the parameter values to be used during
|
||||
the invocation of the method. This member MAY be omitted.
|
||||
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].
|
||||
|
||||
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.
|
||||
|
||||
Notification
|
||||
A Notification is a Request object without an "id" member. A Request
|
||||
object that is a Notification signifies the Client's lack of interest
|
||||
in the corresponding Response object, and as such no Response object
|
||||
needs to be returned to the client. The Server MUST NOT reply to a
|
||||
Notification, including those that are within a batch request.
|
||||
|
||||
Notifications are not confirmable by definition, since they do not have
|
||||
a Response object to be returned. As such, the Client would not be
|
||||
aware of any errors (like e.g. "Invalid params","Internal error").
|
||||
|
||||
Parameter Structures
|
||||
If present, parameters for the rpc call MUST be provided as a
|
||||
Structured value. Either by-position through an Array or by-name
|
||||
through an Object.
|
||||
|
||||
by-position: params MUST be an Array, containing the values in the
|
||||
Server expected order.
|
||||
by-name: params MUST be an Object, with member names that match the
|
||||
Server expected parameter names. The absence of expected names MAY
|
||||
result in an error being generated. The names MUST match exactly,
|
||||
including case, to the method's expected parameters.
|
||||
|
||||
Note:
|
||||
Design principles:
|
||||
* if an object is created or modified without exceptions, its state is
|
||||
valid.
|
||||
* There is a signle source of truth (see Attributes), modification should
|
||||
be done via setters.
|
||||
|
||||
Args:
|
||||
method (str):
|
||||
method to call.
|
||||
params (:obj:dict, optional):
|
||||
a mapping of method argument values or a list of positional arguments.
|
||||
id:
|
||||
an id of the request. By default equals to None and raises warning.
|
||||
For notifications set is_notification=True so id would not be
|
||||
included in the body.
|
||||
is_notification:
|
||||
a boolean flag indicating whether to include id in the body or not.
|
||||
|
||||
Attributes:
|
||||
_body (dict): body of the request. It should always contain valid data and
|
||||
should be modified via setters to ensure validity.
|
||||
|
||||
Examples:
|
||||
modification via self.body["method"] vs self.method
|
||||
modifications via self._body
|
||||
"""
|
||||
def __init__(self,
|
||||
method: str,
|
||||
params: Optional[Union[Mapping[str, Any], Iterable[Any]]] = None,
|
||||
id: Optional[Union[str, int]] = None,
|
||||
is_notification: bool = False
|
||||
) -> None:
|
||||
request_body = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
}
|
||||
|
||||
if params is not None:
|
||||
# If params are not present, they should not be in body
|
||||
request_body["params"] = params
|
||||
|
||||
if not is_notification:
|
||||
# For non-notifications "id" has to be in body, even if null
|
||||
request_body["id"] = id
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = request_body
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value: Mapping[str, Any]) -> None:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ValueError("request body has to be of type Mapping")
|
||||
|
||||
extra_keys = set(value.keys()) - {"jsonrpc", "method", "params", "id"}
|
||||
if len(extra_keys) > 0:
|
||||
raise ValueError("unexpected keys {}".format(extra_keys))
|
||||
|
||||
if value.get("jsonrpc") != "2.0":
|
||||
raise ValueError("value of key 'jsonrpc' has to be '2.0'")
|
||||
|
||||
self.validate_method(value.get("method"))
|
||||
if "params" in value:
|
||||
self.validate_params(value["params"])
|
||||
|
||||
# Validate id for non-notification
|
||||
if "id" in value:
|
||||
self.validate_id(value["id"])
|
||||
|
||||
self._body = value
|
||||
|
||||
@property
|
||||
def method(self) -> str:
|
||||
return self.body["method"]
|
||||
|
||||
@staticmethod
|
||||
def validate_method(value: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
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.")
|
||||
|
||||
@method.setter
|
||||
def method(self, value: str) -> None:
|
||||
self.validate_method(value)
|
||||
self._body["method"] = value
|
||||
|
||||
@property
|
||||
def params(self) -> Optional[Union[Mapping[str, Any], Iterable[Any]]]:
|
||||
return self.body.get("params")
|
||||
|
||||
@staticmethod
|
||||
def validate_params(value: Optional[Union[Mapping[str, Any], Iterable[Any]]]) -> None:
|
||||
"""
|
||||
Note: params has to be None, dict or iterable. In the latter case it would be
|
||||
converted to a list. It is possible to set param as tuple or even string as they
|
||||
are iterables, they would be converted to lists, e.g. ["h", "e", "l", "l", "o"]
|
||||
"""
|
||||
if not isinstance(value, (Mapping, Iterable)):
|
||||
raise ValueError("Incorrect params {0}".format(value))
|
||||
|
||||
@params.setter
|
||||
def params(self, value: Optional[Union[Mapping[str, Any], Iterable[Any]]]) -> None:
|
||||
self.validate_params(value)
|
||||
self._body["params"] = value
|
||||
|
||||
@params.deleter
|
||||
def params(self):
|
||||
del self._body["params"]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.body["id"]
|
||||
|
||||
@staticmethod
|
||||
def validate_id(value: Optional[Union[str, Number]]) -> None:
|
||||
if value is None:
|
||||
warnings.warn(
|
||||
"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.",
|
||||
JSONRPC20RequestIdWarning
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(value, (str, Number)):
|
||||
raise ValueError("id MUST contain a String, Number, or NULL value")
|
||||
|
||||
if isinstance(value, Number) and not isinstance(value, int):
|
||||
warnings.warn(
|
||||
"Fractional parts may be problematic, since many decimal "
|
||||
"fractions cannot be represented exactly as binary fractions.",
|
||||
JSONRPC20RequestIdWarning
|
||||
)
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[Union[str, Number]]) -> None:
|
||||
self.validate_id(value)
|
||||
self._body["id"] = value
|
||||
|
||||
@id.deleter
|
||||
def id(self):
|
||||
del self._body["id"]
|
||||
|
||||
@property
|
||||
def is_notification(self):
|
||||
"""Check if request is a notification.
|
||||
There is no API to make a request notification as this has to remove
|
||||
"id" from body and might cause confusion. To make a request
|
||||
notification delete "id" explicitly.
|
||||
"""
|
||||
return "id" not in self.body
|
||||
|
||||
@property
|
||||
def args(self) -> List:
|
||||
""" Method position arguments.
|
||||
:return list args: method position arguments.
|
||||
note: dict is also iterable, so exclude it from args.
|
||||
"""
|
||||
# if not none and not mapping
|
||||
return list(self.params) if isinstance(self.params, Iterable) and not isinstance(self.params, Mapping) else []
|
||||
|
||||
@property
|
||||
def kwargs(self) -> Dict:
|
||||
""" Method named arguments.
|
||||
:return dict kwargs: method named arguments.
|
||||
"""
|
||||
# if mapping
|
||||
return dict(self.params) if isinstance(self.params, Mapping) else {}
|
||||
|
||||
@staticmethod
|
||||
def from_body(body: Mapping):
|
||||
request = JSONRPC20Request(method="", id=0)
|
||||
request.body = body
|
||||
return request
|
||||
|
||||
|
||||
class JSONRPC20BatchRequest(collections.abc.MutableSequence):
|
||||
def __init__(self, requests: List[JSONRPC20Request] = None):
|
||||
self.requests = requests or []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.requests[index]
|
||||
|
||||
def __setitem__(self, index, value: JSONRPC20Request):
|
||||
self.requests[index] = value
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.requests[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.requests)
|
||||
|
||||
def insert(self, index, value: JSONRPC20Request):
|
||||
self.requests.insert(index, value)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return [request.body for request in self]
|
||||
|
||||
|
||||
class JSONRPC20Error:
|
||||
|
||||
"""Error object.
|
||||
|
||||
When a rpc call encounters an error, the Response Object MUST contain the
|
||||
error member with a value that is a Object with the following members:
|
||||
|
||||
code
|
||||
A Number that indicates the error type that occurred.
|
||||
This MUST be an integer.
|
||||
message
|
||||
A String providing a short description of the error.
|
||||
The message SHOULD be limited to a concise single sentence.
|
||||
data
|
||||
A Primitive or Structured value that contains additional information
|
||||
about the error.
|
||||
This may be omitted.
|
||||
The value of this member is defined by the Server (e.g. detailed error
|
||||
information, nested errors etc.).
|
||||
|
||||
The error codes from and including -32768 to -32000 are reserved for
|
||||
pre-defined errors. Any code within this range, but not defined explicitly
|
||||
below is reserved for future use. The error codes are nearly the same as
|
||||
those suggested for XML-RPC at the following url:
|
||||
http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
||||
|
||||
code | message | meaning
|
||||
-----------------|------------------|-------------------------------------
|
||||
-32700 | Parse error | Invalid JSON was received by the server.
|
||||
| An error occurred on the server while parsing the JSON text.
|
||||
-32600 | Invalid Request | The JSON sent is not a valid Request object.
|
||||
-32601 | Method not found | The method does not exist / is not available.
|
||||
-32602 | Invalid params | Invalid method parameter(s).
|
||||
-32603 | Internal error | Internal JSON-RPC error.
|
||||
-32000 to -32099 | Server error | Reserved for implementation-defined server-errors.
|
||||
|
||||
The remainder of the space is available for application defined errors.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str, data: Any = None):
|
||||
error_body = {
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
if data is not None:
|
||||
# NOTE: if not set in constructor, do not add 'data' to payload.
|
||||
# If data = null is requred, set it after object initialization.
|
||||
error_body["data"] = data
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = error_body
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.code == other.code \
|
||||
and self.message == other.message \
|
||||
and self.data == other.data
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("value has to be of type dict")
|
||||
|
||||
self.validate_code(value["code"])
|
||||
self.validate_message(value["message"])
|
||||
self._body = value
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self.body["code"]
|
||||
|
||||
@staticmethod
|
||||
def validate_code(value: int) -> None:
|
||||
if not isinstance(value, int):
|
||||
raise ValueError("Error code MUST be an integer")
|
||||
|
||||
@code.setter
|
||||
def code(self, value: int) -> None:
|
||||
self.validate_code(value)
|
||||
self._body["code"] = value
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.body["message"]
|
||||
|
||||
@staticmethod
|
||||
def validate_message(value: str) -> None:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Error message should be string")
|
||||
|
||||
@message.setter
|
||||
def message(self, value: str):
|
||||
self.validate_message(value)
|
||||
self._body["message"] = value
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._body.get("data")
|
||||
|
||||
@data.setter
|
||||
def data(self, value):
|
||||
self._body["data"] = value
|
||||
|
||||
@data.deleter
|
||||
def data(self):
|
||||
del self._body["data"]
|
||||
|
||||
@staticmethod
|
||||
def validate_body(value: dict) -> None:
|
||||
if not (set(value.keys()) <= {"code", "message", "data"}):
|
||||
raise ValueError("Error body could have only 'code', 'message' and 'data' keys")
|
||||
|
||||
JSONRPC20Error.validate_code(value.get("code"))
|
||||
JSONRPC20Error.validate_message(value.get("message"))
|
||||
|
||||
|
||||
class JSONRPC20SpecificError(JSONRPC20Error):
|
||||
|
||||
"""Base class for errors with fixed code and message.
|
||||
|
||||
Keep only data in constructor and forbid code and message modifications.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data: Any = None):
|
||||
super(JSONRPC20SpecificError, self).__init__(getattr(self.__class__, "CODE"), getattr(self.__class__, "MESSAGE"), data)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr == "code":
|
||||
raise NotImplementedError("Code modification is forbidden")
|
||||
elif attr == "message":
|
||||
raise NotImplementedError("Message modification is forbidden")
|
||||
else:
|
||||
super(JSONRPC20SpecificError, self).__setattr__(attr, value)
|
||||
|
||||
|
||||
class JSONRPC20ParseError(JSONRPC20SpecificError):
|
||||
|
||||
"""Parse Error.
|
||||
Invalid JSON was received by the server.
|
||||
An error occurred on the server while parsing the JSON text.
|
||||
"""
|
||||
|
||||
CODE = -32700
|
||||
MESSAGE = "Parse error"
|
||||
|
||||
|
||||
class JSONRPC20InvalidRequest(JSONRPC20SpecificError):
|
||||
|
||||
"""Invalid Request.
|
||||
The JSON sent is not a valid Request object.
|
||||
"""
|
||||
|
||||
CODE = -32600
|
||||
MESSAGE = "Invalid Request"
|
||||
|
||||
|
||||
class JSONRPC20MethodNotFound(JSONRPC20SpecificError):
|
||||
|
||||
"""Method not found.
|
||||
The method does not exist / is not available.
|
||||
"""
|
||||
|
||||
CODE = -32601
|
||||
MESSAGE = "Method not found"
|
||||
|
||||
|
||||
class JSONRPC20InvalidParams(JSONRPC20SpecificError):
|
||||
|
||||
"""Invalid params.
|
||||
Invalid method parameter(s).
|
||||
"""
|
||||
|
||||
CODE = -32602
|
||||
MESSAGE = "Invalid params"
|
||||
|
||||
|
||||
class JSONRPC20InternalError(JSONRPC20SpecificError):
|
||||
|
||||
"""Internal error.
|
||||
Internal JSON-RPC error.
|
||||
"""
|
||||
|
||||
CODE = -32603
|
||||
MESSAGE = "Internal error"
|
||||
|
||||
|
||||
class JSONRPC20ServerError(JSONRPC20SpecificError):
|
||||
|
||||
"""Server error.
|
||||
Reserved for implementation-defined server-errors.
|
||||
"""
|
||||
|
||||
CODE = -32000
|
||||
MESSAGE = "Server error"
|
||||
|
||||
|
||||
class JSONRPC20Response:
|
||||
def __init__(self,
|
||||
result: Optional[Any] = None,
|
||||
error: Optional[JSONRPC20Error] = None,
|
||||
id: Optional[Union[str, int]] = None,
|
||||
) -> None:
|
||||
response_body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
}
|
||||
|
||||
if result is not None:
|
||||
response_body["result"] = result
|
||||
|
||||
if error is not None:
|
||||
response_body["error"] = error.body
|
||||
|
||||
self._body = {} # init body
|
||||
self.body = response_body
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value: Mapping[str, Any]) -> None:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError("value has to be of type dict")
|
||||
|
||||
if value.get("jsonrpc") != "2.0":
|
||||
raise ValueError("value['jsonrpc'] has to be '2.0'")
|
||||
|
||||
if "result" not in value and "error" not in value:
|
||||
raise ValueError("Either result or error should exist")
|
||||
|
||||
if "result" in value and "error" in value:
|
||||
raise ValueError("Only one result or error should exist")
|
||||
|
||||
if "error" in value:
|
||||
self.validate_error(value["error"])
|
||||
|
||||
self.validate_id(value["id"])
|
||||
|
||||
self._body = {
|
||||
k: v for (k, v) in value.items()
|
||||
if k in ["jsonrpc", "result", "error", "id"]
|
||||
}
|
||||
|
||||
@property
|
||||
def result(self) -> Optional[Any]:
|
||||
return self.body.get("result")
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[JSONRPC20Error]:
|
||||
if "error" in self.body:
|
||||
return JSONRPC20Error(**self.body["error"])
|
||||
|
||||
@staticmethod
|
||||
def validate_error(error_body: dict) -> None:
|
||||
JSONRPC20Error.validate_body(error_body)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.body["id"]
|
||||
|
||||
@staticmethod
|
||||
def validate_id(value: Optional[Union[str, Number]]) -> None:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if not isinstance(value, (str, Number)):
|
||||
raise ValueError("id MUST contain a String, Number, or NULL value")
|
||||
|
||||
if isinstance(value, Number) and not isinstance(value, int):
|
||||
warnings.warn(
|
||||
"Fractional parts may be problematic, since many decimal "
|
||||
"fractions cannot be represented exactly as binary fractions.",
|
||||
JSONRPC20ResponseIdWarning
|
||||
)
|
||||
|
||||
@id.setter
|
||||
def id(self, value: Optional[Union[str, Number]]) -> None:
|
||||
self.validate_id(value)
|
||||
self._body["id"] = value
|
||||
|
||||
|
||||
class JSONRPC20BatchResponse(collections.abc.MutableSequence):
|
||||
def __init__(self, requests: List[JSONRPC20Response] = None):
|
||||
self.requests = requests or []
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.requests[index]
|
||||
|
||||
def __setitem__(self, index, value: JSONRPC20Response):
|
||||
self.requests[index] = value
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.requests[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.requests)
|
||||
|
||||
def insert(self, index, value: JSONRPC20Response):
|
||||
self.requests.insert(index, value)
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return [request.body for request in self]
|
||||
|
||||
|
||||
class JSONRPC20Exception(Exception):
|
||||
|
||||
"""JSON-RPC Exception class."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class JSONRPC20DispatchException(JSONRPC20Exception):
|
||||
|
||||
"""JSON-RPC Base Exception for dispatcher methods."""
|
||||
|
||||
def __init__(self, code=None, message=None, data=None, *args, **kwargs):
|
||||
super(JSONRPC20DispatchException, self).__init__(args, kwargs)
|
||||
self.error = JSONRPC20Error(code=code, data=data, message=message)
|
||||
164
.venv/lib/python3.11/site-packages/ajsonrpc/dispatcher.py
Normal file
164
.venv/lib/python3.11/site-packages/ajsonrpc/dispatcher.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Method name to method mapper.
|
||||
|
||||
Dispatcher is a dict-like object which maps method_name to method.
|
||||
For usage examples see :meth:`~Dispatcher.add_function`
|
||||
|
||||
"""
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from typing import Any, Optional, Mapping
|
||||
from collections.abc import Mapping as CollectionsMapping, MutableMapping, Callable
|
||||
|
||||
|
||||
class Dispatcher(MutableMapping):
|
||||
|
||||
"""Dictionary-like object which maps method_name to method."""
|
||||
|
||||
def __init__(self, prototype: Any = None, prefix: Optional[str] = None) -> None:
|
||||
""" Build method dispatcher.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prototype : object or dict, optional
|
||||
Initial method mapping.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Init object with method dictionary.
|
||||
|
||||
>>> Dispatcher({"sum": lambda a, b: a + b})
|
||||
None
|
||||
|
||||
"""
|
||||
self.method_map: Mapping[str, Callable] = dict()
|
||||
|
||||
if prototype is not None:
|
||||
self.add_prototype(prototype, prefix=prefix)
|
||||
|
||||
def __getitem__(self, key: str) -> Callable:
|
||||
return self.method_map[key]
|
||||
|
||||
def __setitem__(self, key: str, value: Callable) -> None:
|
||||
self.method_map[key] = value
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self.method_map[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.method_map)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.method_map)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.method_map)
|
||||
|
||||
@staticmethod
|
||||
def _getattr_function(prototype: Any, attr: str) -> Callable:
|
||||
"""Fix the issue of accessing instance method of a class.
|
||||
|
||||
Class.method(self, *args **kwargs) requires the first argument to be
|
||||
instance, but it was not given. Substitute method with a partial
|
||||
function where the first argument is an empty class constructor.
|
||||
|
||||
"""
|
||||
|
||||
method = getattr(prototype, attr)
|
||||
if inspect.isclass(prototype) and isinstance(prototype.__dict__[attr], types.FunctionType):
|
||||
return functools.partial(method, prototype())
|
||||
return method
|
||||
|
||||
@staticmethod
|
||||
def _extract_methods(prototype: Any, prefix: str = "") -> Mapping[str, Callable]:
|
||||
return {
|
||||
prefix + attr: Dispatcher._getattr_function(prototype, attr)
|
||||
for attr in dir(prototype)
|
||||
if not attr.startswith("_")
|
||||
}
|
||||
|
||||
def add_class(self, cls: Any, prefix: Optional[str] = None) -> None:
|
||||
"""Add class to dispatcher.
|
||||
|
||||
Adds all of the public methods to dispatcher.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If class has instance methods (e.g. no @classmethod decorator),
|
||||
they likely would not work. Use :meth:`~add_object` instead.
|
||||
At the moment, dispatcher creates an object with empty constructor
|
||||
for instance methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cls : type
|
||||
class with methods to be added to dispatcher
|
||||
prefix : str, optional
|
||||
Method prefix. If not present, lowercased class name is used.
|
||||
|
||||
"""
|
||||
if prefix is None:
|
||||
prefix = cls.__name__.lower() + '.'
|
||||
|
||||
self.update(Dispatcher._extract_methods(cls, prefix=prefix))
|
||||
|
||||
def add_object(self, obj: Any, prefix: Optional[str] = None) -> None:
|
||||
if prefix is None:
|
||||
prefix = obj.__class__.__name__.lower() + '.'
|
||||
|
||||
self.update(Dispatcher._extract_methods(obj, prefix=prefix))
|
||||
|
||||
def add_prototype(self, prototype: Any, prefix: Optional[str] = None) -> None:
|
||||
if isinstance(prototype, CollectionsMapping):
|
||||
self.update({
|
||||
(prefix or "") + key: value
|
||||
for key, value in prototype.items()
|
||||
})
|
||||
elif inspect.isclass(prototype):
|
||||
self.add_class(prototype, prefix=prefix)
|
||||
else:
|
||||
self.add_object(prototype, prefix=prefix)
|
||||
|
||||
def add_function(self, f: Callable = None, name: Optional[str] = None) -> Callable:
|
||||
""" Add a method to the dispatcher.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Callable to be added.
|
||||
name : str, optional
|
||||
Name to register (the default is function **f** name)
|
||||
|
||||
Notes
|
||||
-----
|
||||
When used as a decorator keeps callable object unmodified.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Use as method
|
||||
|
||||
>>> d = Dispatcher()
|
||||
>>> d.add_function(lambda a, b: a + b, name="sum")
|
||||
<function __main__.<lambda>>
|
||||
|
||||
Or use as decorator
|
||||
|
||||
>>> d = Dispatcher()
|
||||
>>> @d.add_function
|
||||
def mymethod(*args, **kwargs):
|
||||
print(args, kwargs)
|
||||
|
||||
Or use as a decorator with a different function name
|
||||
>>> d = Dispatcher()
|
||||
>>> @d.add_function(name="my.method")
|
||||
def mymethod(*args, **kwargs):
|
||||
print(args, kwargs)
|
||||
|
||||
"""
|
||||
if name and not f:
|
||||
return functools.partial(self.add_function, name=name)
|
||||
|
||||
self[name or f.__name__] = f
|
||||
return f
|
||||
118
.venv/lib/python3.11/site-packages/ajsonrpc/manager.py
Normal file
118
.venv/lib/python3.11/site-packages/ajsonrpc/manager.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import json
|
||||
import inspect
|
||||
import asyncio
|
||||
from typing import Optional, Union, Iterable, Mapping
|
||||
|
||||
from .core import (
|
||||
JSONRPC20Request, JSONRPC20BatchRequest, JSONRPC20Response,
|
||||
JSONRPC20BatchResponse, JSONRPC20MethodNotFound, JSONRPC20InvalidParams,
|
||||
JSONRPC20ServerError, JSONRPC20ParseError, JSONRPC20InvalidRequest,
|
||||
JSONRPC20DispatchException,
|
||||
)
|
||||
from .dispatcher import Dispatcher
|
||||
from .utils import is_invalid_params
|
||||
|
||||
|
||||
class AsyncJSONRPCResponseManager:
|
||||
|
||||
"""Async JSON-RPC Response manager."""
|
||||
|
||||
def __init__(self, dispatcher: Dispatcher, serialize=json.dumps, deserialize=json.loads, is_server_error_verbose=False):
|
||||
self.dispatcher = dispatcher
|
||||
self.serialize = serialize
|
||||
self.deserialize = deserialize
|
||||
self.is_server_error_verbose = is_server_error_verbose
|
||||
|
||||
async def get_response_for_request(self, request: JSONRPC20Request) -> Optional[JSONRPC20Response]:
|
||||
"""Get response for an individual request."""
|
||||
output = None
|
||||
response_id = request.id if not request.is_notification else None
|
||||
try:
|
||||
method = self.dispatcher[request.method]
|
||||
except KeyError:
|
||||
# method not found
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20MethodNotFound(),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
try:
|
||||
result = await method(*request.args, **request.kwargs) \
|
||||
if inspect.iscoroutinefunction(method) \
|
||||
else method(*request.args, **request.kwargs)
|
||||
except JSONRPC20DispatchException as dispatch_error:
|
||||
# Dispatcher method raised exception with controlled "data"
|
||||
output = JSONRPC20Response(
|
||||
error=dispatch_error.error,
|
||||
id=response_id
|
||||
)
|
||||
except Exception as e:
|
||||
if is_invalid_params(method, *request.args, **request.kwargs):
|
||||
# Method's parameters are incorrect
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20InvalidParams(),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
# Dispatcher method raised exception
|
||||
output = JSONRPC20Response(
|
||||
error=JSONRPC20ServerError(
|
||||
data={
|
||||
"type": e.__class__.__name__,
|
||||
"args": e.args,
|
||||
"message": str(e),
|
||||
} if self.is_server_error_verbose else None
|
||||
),
|
||||
id=response_id
|
||||
)
|
||||
else:
|
||||
output = JSONRPC20Response(result=result, id=response_id)
|
||||
|
||||
if not request.is_notification:
|
||||
return output
|
||||
|
||||
async def get_response_for_request_body(self, request_body) -> Optional[JSONRPC20Response]:
|
||||
"""Catch parse error as well"""
|
||||
try:
|
||||
request = JSONRPC20Request.from_body(request_body)
|
||||
except ValueError:
|
||||
return JSONRPC20Response(error=JSONRPC20InvalidRequest())
|
||||
else:
|
||||
return await self.get_response_for_request(request)
|
||||
|
||||
async def get_response_for_payload(self, payload: str) -> Optional[Union[JSONRPC20Response, JSONRPC20BatchResponse]]:
|
||||
"""Top level handler
|
||||
|
||||
NOTE: top level handler, accepts string payload.
|
||||
|
||||
"""
|
||||
try:
|
||||
request_data = self.deserialize(payload)
|
||||
except (TypeError, ValueError):
|
||||
return JSONRPC20Response(error=JSONRPC20ParseError())
|
||||
|
||||
# check if iterable, and determine what request to instantiate.
|
||||
is_batch_request = isinstance(request_data, Iterable) \
|
||||
and not isinstance(request_data, Mapping)
|
||||
if is_batch_request and len(request_data) == 0:
|
||||
return JSONRPC20Response(error=JSONRPC20InvalidRequest())
|
||||
|
||||
requests_bodies = request_data if is_batch_request else [request_data]
|
||||
responses = await asyncio.gather(*[
|
||||
self.get_response_for_request_body(request_body)
|
||||
for request_body in requests_bodies
|
||||
])
|
||||
nonempty_responses = [r for r in responses if r is not None]
|
||||
if is_batch_request:
|
||||
if len(nonempty_responses) > 0:
|
||||
return JSONRPC20BatchResponse(nonempty_responses)
|
||||
elif len(nonempty_responses) > 0:
|
||||
return nonempty_responses[0]
|
||||
|
||||
async def get_payload_for_payload(self, payload: str) -> str:
|
||||
response = await self.get_response_for_payload(payload)
|
||||
|
||||
if response is None:
|
||||
return ""
|
||||
|
||||
return self.serialize(response.body)
|
||||
107
.venv/lib/python3.11/site-packages/ajsonrpc/scripts/server.py
Normal file
107
.venv/lib/python3.11/site-packages/ajsonrpc/scripts/server.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import importlib.util
|
||||
import sys
|
||||
from inspect import getmembers, isfunction
|
||||
from ajsonrpc import __version__
|
||||
from ajsonrpc.dispatcher import Dispatcher
|
||||
from ajsonrpc.manager import AsyncJSONRPCResponseManager
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Helper funciont to create asyncio task
|
||||
# see: https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
||||
if sys.version_info >= (3, 7):
|
||||
create_task = asyncio.create_task
|
||||
else:
|
||||
create_task = asyncio.ensure_future
|
||||
|
||||
|
||||
class JSONRPCProtocol(asyncio.Protocol):
|
||||
def __init__(self, json_rpc_manager):
|
||||
self.json_rpc_manager = json_rpc_manager
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
|
||||
def data_received(self, data):
|
||||
message = data.decode()
|
||||
request_method, request_message = message.split('\r\n', 1)
|
||||
if not request_method.startswith('POST'):
|
||||
logger.warning('Incorrect HTTP method, should be POST')
|
||||
|
||||
_, payload = request_message.split('\r\n\r\n', 1)
|
||||
task = create_task(self.json_rpc_manager.get_payload_for_payload(payload))
|
||||
task.add_done_callback(self.handle_task_result)
|
||||
|
||||
def handle_task_result(self, task):
|
||||
res = task.result()
|
||||
self.transport.write((
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"\r\n"
|
||||
+ str(res)
|
||||
).encode("utf-8"))
|
||||
|
||||
logger.info('Close the client socket')
|
||||
self.transport.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Usage: % examples.methods"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=True,
|
||||
description="Start async JSON-RPC 2.0 server")
|
||||
parser.add_argument(
|
||||
'--version', action='version',
|
||||
version='%(prog)s {version}'.format(version=__version__))
|
||||
parser.add_argument("--host", dest="host", default="127.0.0.1")
|
||||
parser.add_argument("--port", dest="port")
|
||||
parser.add_argument('module')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
spec = importlib.util.spec_from_file_location("module", args.module)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# get functions from the module
|
||||
methods = getmembers(module, isfunction)
|
||||
logger.info('Extracted methods: {}'.format(methods))
|
||||
dispatcher = Dispatcher(dict(methods))
|
||||
|
||||
json_rpc_manager = AsyncJSONRPCResponseManager(dispatcher=dispatcher)
|
||||
loop = asyncio.get_event_loop()
|
||||
# Each client connection will create a new protocol instance
|
||||
coro = loop.create_server(
|
||||
lambda: JSONRPCProtocol(json_rpc_manager),
|
||||
host=args.host,
|
||||
port=args.port
|
||||
)
|
||||
server = loop.run_until_complete(coro)
|
||||
|
||||
# Serve requests until Ctrl+C is pressed
|
||||
logger.info('Serving on {}'.format(server.sockets[0].getsockname()))
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# Close the server
|
||||
server.close()
|
||||
loop.run_until_complete(server.wait_closed())
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# setup console logging
|
||||
logger.setLevel(logging.DEBUG)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter("%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
main()
|
||||
407
.venv/lib/python3.11/site-packages/ajsonrpc/tests/test_core.py
Normal file
407
.venv/lib/python3.11/site-packages/ajsonrpc/tests/test_core.py
Normal file
@@ -0,0 +1,407 @@
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from ..core import (JSONRPC20BatchRequest, JSONRPC20BatchResponse,
|
||||
JSONRPC20Error, JSONRPC20InternalError,
|
||||
JSONRPC20InvalidParams, JSONRPC20InvalidRequest,
|
||||
JSONRPC20MethodNotFound, JSONRPC20ParseError,
|
||||
JSONRPC20Request, JSONRPC20RequestIdWarning,
|
||||
JSONRPC20Response, JSONRPC20ServerError)
|
||||
|
||||
|
||||
class TestJSONRPC20Request(unittest.TestCase):
|
||||
|
||||
"""Test JSONRPC20Request.
|
||||
|
||||
On creation and after modification the request object has to be valid. As
|
||||
these scenarios are almost identical, test both in test cases.
|
||||
|
||||
"""
|
||||
|
||||
#############################################
|
||||
# "method" tests
|
||||
#############################################
|
||||
|
||||
def test_method_validation_correct(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
self.assertEqual(r.method, "valid")
|
||||
r.method = "also_valid"
|
||||
self.assertEqual(r.method, "also_valid")
|
||||
|
||||
def test_method_validation_not_str(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method=[], id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = []
|
||||
|
||||
# invalid setters should not modify the object
|
||||
self.assertEqual(r.method, "valid")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method={}, id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = {}
|
||||
|
||||
def test_method_validation_invalid_rpc_prefix(self):
|
||||
""" Test method SHOULD NOT starts with rpc."""
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method="rpc.", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = "rpc."
|
||||
|
||||
# invalid setters should not modify the object
|
||||
self.assertEqual(r.method, "valid")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request(method="rpc.test", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.method = "rpc.test"
|
||||
|
||||
JSONRPC20Request(method="rpcvalid", id=0)
|
||||
JSONRPC20Request(method="rpc", id=0)
|
||||
|
||||
#############################################
|
||||
# "params" tests
|
||||
#############################################
|
||||
|
||||
def test_params_validation_none(self):
|
||||
r1 = JSONRPC20Request("null_params", params=None, id=1)
|
||||
self.assertFalse("params" in r1.body)
|
||||
|
||||
# Remove params
|
||||
r2 = JSONRPC20Request("null_params", params=[], id=2)
|
||||
self.assertTrue("params" in r2.body)
|
||||
del r2.params
|
||||
self.assertFalse("params" in r2.body)
|
||||
|
||||
def test_params_validation_list(self):
|
||||
r = JSONRPC20Request("list", params=[], id=0)
|
||||
self.assertEqual(r.params, [])
|
||||
r.params = [0, 1]
|
||||
self.assertEqual(r.params, [0, 1])
|
||||
|
||||
def test_params_validation_tuple(self):
|
||||
r = JSONRPC20Request("tuple", params=(), id=0)
|
||||
self.assertEqual(r.params, ()) # keep the same iterable
|
||||
r.params = (0, 1)
|
||||
self.assertEqual(r.params, (0, 1))
|
||||
|
||||
def test_params_validation_iterable(self):
|
||||
r1 = JSONRPC20Request("string_params", params="string", id=1)
|
||||
self.assertEqual(r1.params, "string")
|
||||
r1.params = "another string"
|
||||
self.assertEqual(r1.params, "another string")
|
||||
|
||||
r2 = JSONRPC20Request("range_params", params=range(1), id=2)
|
||||
self.assertEqual(r2.params, range(1))
|
||||
r2.params = range(2)
|
||||
self.assertEqual(r2.params, range(2))
|
||||
|
||||
def test_params_validation_dict(self):
|
||||
r1 = JSONRPC20Request("dict_params", params={}, id=1)
|
||||
self.assertEqual(r1.params, {})
|
||||
r1.params = {"a": 0}
|
||||
self.assertEqual(r1.params, {"a": 0})
|
||||
|
||||
r2 = JSONRPC20Request("dict_params", params={"a": 0}, id=2)
|
||||
self.assertEqual(r2.params, {"a": 0})
|
||||
r2.params = {"a": {}}
|
||||
self.assertEqual(r2.params, {"a": {}})
|
||||
|
||||
def test_params_validation_invalid(self):
|
||||
r = JSONRPC20Request("list", params=[], id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("invalid_params", params=0, id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.params = 0
|
||||
|
||||
self.assertEqual(r.params, [])
|
||||
|
||||
#############################################
|
||||
# "id" tests
|
||||
#############################################
|
||||
|
||||
def test_id_validation_valid(self):
|
||||
r1 = JSONRPC20Request("string_id", id="id")
|
||||
self.assertEqual(r1.id, "id")
|
||||
r1.id = "another_id"
|
||||
self.assertEqual(r1.id, "another_id")
|
||||
|
||||
r2 = JSONRPC20Request("int_id", id=0)
|
||||
self.assertEqual(r2.id, 0)
|
||||
r2.id = 1
|
||||
self.assertEqual(r2.id, 1)
|
||||
|
||||
# Null ids are possible but discouraged. Omit id for notifications.
|
||||
with warnings.catch_warnings(record=True) as _warnings:
|
||||
warnings.simplefilter("always")
|
||||
JSONRPC20Request("null_id", id=None)
|
||||
assert len(_warnings) == 1
|
||||
assert issubclass(_warnings[-1].category, JSONRPC20RequestIdWarning)
|
||||
assert "Null as a value" in str(_warnings[-1].message)
|
||||
|
||||
# Float ids are possible but discouraged
|
||||
with warnings.catch_warnings(record=True) as _warnings:
|
||||
warnings.simplefilter("always")
|
||||
JSONRPC20Request("float_id", id=0.1)
|
||||
|
||||
assert len(_warnings) == 1
|
||||
assert issubclass(_warnings[-1].category, JSONRPC20RequestIdWarning)
|
||||
assert "Fractional parts" in str(_warnings[-1].message)
|
||||
|
||||
|
||||
def test_id_validation_invalid(self):
|
||||
r = JSONRPC20Request("valid_id", id=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("list_id", id=[])
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.id = []
|
||||
|
||||
self.assertEqual(r.id, 0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Request("dict_id", id={})
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.id = {}
|
||||
|
||||
#############################################
|
||||
# Notification tests
|
||||
#############################################
|
||||
|
||||
def test_notification_init(self):
|
||||
r = JSONRPC20Request("notification", is_notification=True)
|
||||
self.assertTrue(r.is_notification)
|
||||
with self.assertRaises(KeyError):
|
||||
r.id
|
||||
|
||||
def test_notification_conversion(self):
|
||||
r = JSONRPC20Request("notification", id=0)
|
||||
self.assertFalse(r.is_notification)
|
||||
del r.id
|
||||
self.assertTrue(r.is_notification)
|
||||
|
||||
#############################################
|
||||
# Auxiliary methods tests
|
||||
#############################################
|
||||
|
||||
def test_request_args(self):
|
||||
self.assertEqual(JSONRPC20Request("add", id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", [], id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", "str", id=0).args, ["s", "t", "r"])
|
||||
self.assertEqual(JSONRPC20Request("add", {"a": 1}, id=0).args, [])
|
||||
self.assertEqual(JSONRPC20Request("add", [1, 2], id=0).args, [1, 2])
|
||||
|
||||
def test_request_kwargs(self):
|
||||
self.assertEqual(JSONRPC20Request("add", id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", [1, 2], id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", {}, id=0).kwargs, {})
|
||||
self.assertEqual(JSONRPC20Request("add", {"a": 1}, id=0).kwargs, {"a": 1})
|
||||
|
||||
#############################################
|
||||
# body methods tests
|
||||
#############################################
|
||||
def test_body_validation(self):
|
||||
r = JSONRPC20Request(method="valid", id=0)
|
||||
self.assertEqual(
|
||||
r.body,
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "valid",
|
||||
"id": 0,
|
||||
}
|
||||
)
|
||||
r.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "new",
|
||||
}
|
||||
self.assertEqual(r.id, 1)
|
||||
self.assertEqual(r.method, "new")
|
||||
|
||||
# body has to have "jsonrpc" in it
|
||||
with self.assertRaises(ValueError):
|
||||
r.body = {"id": 1, "method": "new"}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
r.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": [],
|
||||
"method": 0,
|
||||
"params": 0,
|
||||
}
|
||||
|
||||
self.assertEqual(r.id, 1)
|
||||
self.assertEqual(r.method, "new")
|
||||
|
||||
|
||||
class TestJSONRPC20BatchRequest(unittest.TestCase):
|
||||
def test_init(self):
|
||||
br = JSONRPC20BatchRequest()
|
||||
self.assertEqual(len(br), 0)
|
||||
|
||||
br.append(JSONRPC20Request("first", id=1))
|
||||
br.extend([JSONRPC20Request("second", id=2)])
|
||||
self.assertEqual(len(br), 2)
|
||||
self.assertEqual(br[-1].method, "second")
|
||||
|
||||
|
||||
class TestJSONRPC20Error(unittest.TestCase):
|
||||
|
||||
"""Test JSONRPC20Error.
|
||||
|
||||
On creation and after modification the request object has to be valid. As
|
||||
these scenarios are almost identical, test both in test cases.
|
||||
|
||||
"""
|
||||
|
||||
#############################################
|
||||
# "code" tests
|
||||
#############################################
|
||||
|
||||
def test_code_validation_valid_numeric(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
self.assertEqual(e.code, 0)
|
||||
# Allow numeric codes. Though, prefer using integers
|
||||
e.code = 1
|
||||
self.assertEqual(e.code, 1)
|
||||
|
||||
def test_code_validation_not_number(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Error(code="0", message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
e.code = "0"
|
||||
|
||||
#############################################
|
||||
# "message" tests
|
||||
#############################################
|
||||
|
||||
def test_message_validation_valid_str(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
self.assertEqual(e.message, "error")
|
||||
e.message = "specific error"
|
||||
self.assertEqual(e.message, "specific error")
|
||||
|
||||
def test_message_validation_not_str(self):
|
||||
e = JSONRPC20Error(code=0, message="error")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Error(code=0, message=0)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
e.message = 0
|
||||
|
||||
#############################################
|
||||
# "data" tests
|
||||
#############################################
|
||||
def test_data_validation_valid(self):
|
||||
e = JSONRPC20Error(code=0, message="error", data=0)
|
||||
self.assertEqual(e.data, 0)
|
||||
e.data = {"timestamp": 0}
|
||||
self.assertEqual(e.data, {"timestamp": 0})
|
||||
|
||||
def test_could_not_change_code_message_predefined_errors(self):
|
||||
errors = [
|
||||
JSONRPC20ParseError(),
|
||||
JSONRPC20InvalidRequest(),
|
||||
JSONRPC20MethodNotFound(),
|
||||
JSONRPC20InvalidParams(),
|
||||
JSONRPC20InternalError(),
|
||||
JSONRPC20ServerError(),
|
||||
]
|
||||
|
||||
for error in errors:
|
||||
with self.assertRaises(NotImplementedError):
|
||||
error.code = 0
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
error.message = ""
|
||||
|
||||
|
||||
class TestJSONRPC20Response(unittest.TestCase):
|
||||
def test_valid_result(self):
|
||||
response = JSONRPC20Response(result="valid")
|
||||
self.assertEqual(response.result, "valid")
|
||||
self.assertIsNone(response.error)
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
{"jsonrpc": "2.0", "id": None, "result": "valid"}
|
||||
)
|
||||
|
||||
def test_valid_error(self):
|
||||
error = JSONRPC20MethodNotFound()
|
||||
response = JSONRPC20Response(error=error)
|
||||
self.assertIsNone(response.result)
|
||||
self.assertEqual(response.error, error)
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
{"jsonrpc": "2.0", "id": None, "error": error.body}
|
||||
)
|
||||
|
||||
def test_set_valid_body(self):
|
||||
response = JSONRPC20Response(result="")
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "",
|
||||
}
|
||||
}
|
||||
self.assertIsInstance(response.error, JSONRPC20Error)
|
||||
|
||||
def test_set_body_result_and_error(self):
|
||||
response = JSONRPC20Response(result="")
|
||||
with self.assertRaises(ValueError):
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"result": "",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "",
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
JSONRPC20Response(
|
||||
result="",
|
||||
error=JSONRPC20Error(code=0, message="")
|
||||
)
|
||||
|
||||
@unittest.skip("TODO: Implement later")
|
||||
def test_set_body_error_correct_error_class(self):
|
||||
"""Return error class matching pre-defined error codes."""
|
||||
response = JSONRPC20Response(result="")
|
||||
response.body = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": JSONRPC20MethodNotFound().body,
|
||||
}
|
||||
self.assertIsInstance(response.error, JSONRPC20MethodNotFound)
|
||||
|
||||
|
||||
class TestJSONRPC20BatchResponse(unittest.TestCase):
|
||||
def test_init(self):
|
||||
batch = JSONRPC20BatchResponse()
|
||||
self.assertEqual(len(batch), 0)
|
||||
|
||||
batch.append(JSONRPC20Response(result="first", id=1))
|
||||
batch.extend([JSONRPC20Response(result="second", id=2)])
|
||||
self.assertEqual(len(batch), 2)
|
||||
self.assertEqual(batch[-1].result, "second")
|
||||
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
from ..dispatcher import Dispatcher
|
||||
|
||||
|
||||
class Math:
|
||||
@staticmethod
|
||||
def sum(a, b):
|
||||
return a + b
|
||||
|
||||
@classmethod
|
||||
def diff(cls, a, b):
|
||||
return a - b
|
||||
|
||||
def mul(self, a, b):
|
||||
return a * b
|
||||
|
||||
|
||||
class TestDispatcher(unittest.TestCase):
|
||||
def test_empty(self):
|
||||
self.assertEqual(len(Dispatcher()), 0)
|
||||
|
||||
def test_add_function(self):
|
||||
d = Dispatcher()
|
||||
|
||||
@d.add_function
|
||||
def one():
|
||||
return 1
|
||||
|
||||
def two():
|
||||
return 2
|
||||
|
||||
d.add_function(two)
|
||||
d.add_function(two, name="two_alias")
|
||||
|
||||
self.assertIn("one", d)
|
||||
self.assertEqual(d["one"](), 1)
|
||||
self.assertIsNotNone(one) # do not remove function from the scope
|
||||
self.assertIn("two", d)
|
||||
self.assertIn("two_alias", d)
|
||||
|
||||
def test_class(self):
|
||||
d1 = Dispatcher()
|
||||
d1.add_class(Math)
|
||||
self.assertIn("math.sum", d1)
|
||||
self.assertIn("math.diff", d1)
|
||||
self.assertIn("math.mul", d1)
|
||||
self.assertEqual(d1["math.sum"](3, 8), 11)
|
||||
self.assertEqual(d1["math.diff"](6, 9), -3)
|
||||
self.assertEqual(d1["math.mul"](2, 3), 6)
|
||||
|
||||
d2 = Dispatcher(Math)
|
||||
self.assertNotIn("__class__", d2)
|
||||
self.assertEqual(d1.keys(), d2.keys())
|
||||
for method in ["math.sum", "math.diff"]:
|
||||
self.assertEqual(d1[method], d2[method])
|
||||
|
||||
def test_class_prefix(self):
|
||||
d = Dispatcher(Math, prefix="")
|
||||
self.assertIn("sum", d)
|
||||
self.assertNotIn("math.sum", d)
|
||||
|
||||
def test_object(self):
|
||||
math = Math()
|
||||
d1 = Dispatcher()
|
||||
d1.add_object(math)
|
||||
self.assertIn("math.sum", d1)
|
||||
self.assertIn("math.diff", d1)
|
||||
self.assertEqual(d1["math.sum"](3, 8), 11)
|
||||
self.assertEqual(d1["math.diff"](6, 9), -3)
|
||||
|
||||
d2 = Dispatcher(math)
|
||||
self.assertNotIn("__class__", d2)
|
||||
self.assertEqual(d1, d2)
|
||||
|
||||
def test_object_prefix(self):
|
||||
d = Dispatcher(Math(), prefix="")
|
||||
self.assertIn("sum", d)
|
||||
self.assertNotIn("math.sum", d)
|
||||
|
||||
def test_add_dict(self):
|
||||
d = Dispatcher()
|
||||
d.add_prototype({"sum": lambda *args: sum(args)}, "util.")
|
||||
|
||||
self.assertIn("util.sum", d)
|
||||
self.assertEqual(d["util.sum"](13, -2), 11)
|
||||
|
||||
def test_init_from_dict(self):
|
||||
d = Dispatcher({
|
||||
"one": lambda: 1,
|
||||
"two": lambda: 2,
|
||||
})
|
||||
|
||||
self.assertIn("one", d)
|
||||
self.assertIn("two", d)
|
||||
|
||||
def test_del_method(self):
|
||||
d = Dispatcher()
|
||||
d["method"] = lambda: ""
|
||||
self.assertIn("method", d)
|
||||
|
||||
del d["method"]
|
||||
self.assertNotIn("method", d)
|
||||
|
||||
def test_to_dict(self):
|
||||
d = Dispatcher()
|
||||
|
||||
def func():
|
||||
return ""
|
||||
|
||||
d["method"] = func
|
||||
self.assertEqual(dict(d), {"method": func})
|
||||
|
||||
def test__getattr_function(self):
|
||||
# class
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "sum")(3, 2), 5)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "diff")(3, 2), 1)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math, "mul")(3, 2), 6)
|
||||
|
||||
# object
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "sum")(3, 2), 5)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "diff")(3, 2), 1)
|
||||
self.assertEqual(Dispatcher._getattr_function(Math(), "mul")(3, 2), 6)
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Test Async JSON-RPC Response manager."""
|
||||
import unittest
|
||||
import json
|
||||
|
||||
from ..core import JSONRPC20Request, JSONRPC20Response, JSONRPC20MethodNotFound, JSONRPC20InvalidParams, JSONRPC20ServerError, JSONRPC20DispatchException
|
||||
from ..manager import AsyncJSONRPCResponseManager
|
||||
|
||||
|
||||
class TestAsyncJSONRPCResponseManager(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
def subtract(minuend, subtrahend):
|
||||
return minuend - subtrahend
|
||||
|
||||
def raise_(e: Exception):
|
||||
raise e
|
||||
|
||||
async def async_sum(*args):
|
||||
return sum(args)
|
||||
|
||||
self.dispatcher = {
|
||||
"subtract": subtract,
|
||||
"async_sum": async_sum,
|
||||
"dispatch_exception": lambda: raise_(
|
||||
JSONRPC20DispatchException(
|
||||
code=4000, message="error", data={"param": 1}
|
||||
)
|
||||
),
|
||||
"unexpected_exception": lambda: raise_(ValueError("Unexpected")),
|
||||
}
|
||||
|
||||
self.manager = AsyncJSONRPCResponseManager(dispatcher=self.dispatcher)
|
||||
|
||||
async def test_get_response(self):
|
||||
req = JSONRPC20Request("subtract", params=[5, 3], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.result, 2)
|
||||
|
||||
async def test_get_response_notification(self):
|
||||
req = JSONRPC20Request("subtract", params=[5, 3], is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_async_response(self):
|
||||
req = JSONRPC20Request("async_sum", params=[1, 2, 3], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.result, 6)
|
||||
|
||||
async def test_get_response_method_not_found(self):
|
||||
req = JSONRPC20Request("does_not_exist", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20MethodNotFound())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_not_found_notification(self):
|
||||
req = JSONRPC20Request("does_not_exist", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_incorrect_arguments(self):
|
||||
req = JSONRPC20Request("subtract", params=[0], id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20InvalidParams())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_incorrect_arguments_notification(self):
|
||||
req = JSONRPC20Request("subtract", params=[0], is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_method_expected_error(self):
|
||||
req = JSONRPC20Request("dispatch_exception", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error.body, dict(code=4000, message="error", data={"param": 1}))
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_expected_error_notification(self):
|
||||
req = JSONRPC20Request("dispatch_exception", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_method_unexpected_error(self):
|
||||
req = JSONRPC20Request("unexpected_exception", id=0)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertTrue(isinstance(res, JSONRPC20Response))
|
||||
self.assertEqual(res.error, JSONRPC20ServerError())
|
||||
self.assertEqual(res.id, req.id)
|
||||
|
||||
async def test_get_response_method_unexpected_error_notification(self):
|
||||
req = JSONRPC20Request("unexpected_exception", is_notification=True)
|
||||
res = await self.manager.get_response_for_request(req)
|
||||
self.assertIsNone(res)
|
||||
|
||||
async def test_get_response_for_payload_batch(self):
|
||||
response = await self.manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "subtract", "params": [3, 4], "id": 1},
|
||||
{"jsonrpc": "2.0"}
|
||||
]))
|
||||
self.assertEqual(
|
||||
response.body,
|
||||
[
|
||||
{"jsonrpc": "2.0", "result": -1, "id": 1},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {"code": -32600, "message": "Invalid Request"},
|
||||
"id": None
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
async def test_verbose_error(self):
|
||||
manager = AsyncJSONRPCResponseManager(
|
||||
dispatcher=self.dispatcher, is_server_error_verbose=True)
|
||||
req = JSONRPC20Request("unexpected_exception", id=0)
|
||||
res = await manager.get_response_for_request(req)
|
||||
self.assertEqual(
|
||||
res.error.data,
|
||||
{'type': 'ValueError', 'args': ('Unexpected',), 'message': 'Unexpected'}
|
||||
)
|
||||
|
||||
manager.is_server_error_verbose = False
|
||||
res = await manager.get_response_for_request(req)
|
||||
self.assertIsNone(res.error.data)
|
||||
|
||||
#############################################
|
||||
# Test examples from https://www.jsonrpc.org/specification
|
||||
#############################################
|
||||
|
||||
async def test_examples_positional_parameters(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}'
|
||||
)
|
||||
self.assertEqual(response1.body, {"jsonrpc": "2.0", "result": 19, "id": 1})
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}'
|
||||
)
|
||||
self.assertEqual(response2.body, {"jsonrpc": "2.0", "result": -19, "id": 2})
|
||||
|
||||
async def test_examples_named_parameters(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}'
|
||||
)
|
||||
self.assertEqual(response1.body, {"jsonrpc": "2.0", "result": 19, "id": 3})
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}'
|
||||
)
|
||||
self.assertEqual(response2.body, {"jsonrpc": "2.0", "result": 19, "id": 4})
|
||||
|
||||
async def test_examples_notification(self):
|
||||
response1 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}'
|
||||
)
|
||||
self.assertIsNone(response1)
|
||||
|
||||
response2 = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar"}'
|
||||
)
|
||||
self.assertIsNone(response2)
|
||||
|
||||
async def test_examples_rpc_call_of_nonexistent_method(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar", "id": "1"}'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"})
|
||||
|
||||
async def test_exampels_rpc_call_with_invalid_json(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_invalid_request_object(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
'{"jsonrpc": "2.0", "method": 1, "params": "bar"}'
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_batch_invalid_json(self):
|
||||
response = await self.manager.get_response_for_payload(
|
||||
"""[
|
||||
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
|
||||
{"jsonrpc": "2.0", "method"
|
||||
]"""
|
||||
)
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_an_empty_array(self):
|
||||
response = await self.manager.get_response_for_payload('[]')
|
||||
self.assertEqual(response.body, {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None})
|
||||
|
||||
async def test_examples_rpc_call_with_an_invalid_batch_but_not_empty(self):
|
||||
response = await self.manager.get_response_for_payload('[1]')
|
||||
self.assertEqual(response.body, [{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}])
|
||||
|
||||
async def test_examples_rpc_call_with_invalid_batch(self):
|
||||
response = await self.manager.get_response_for_payload('[1,2,3]')
|
||||
self.assertEqual(response.body, [
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None}
|
||||
])
|
||||
|
||||
async def test_examples_rpc_call_batch(self):
|
||||
dispatcher = {
|
||||
"sum": lambda *values: sum(values),
|
||||
"subtract": lambda a, b: a - b,
|
||||
"get_data": lambda: ["hello", 5],
|
||||
}
|
||||
manager = AsyncJSONRPCResponseManager(dispatcher=dispatcher)
|
||||
|
||||
response = await manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"},
|
||||
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
|
||||
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"},
|
||||
{"foo": "boo"},
|
||||
{"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
|
||||
{"jsonrpc": "2.0", "method": "get_data", "id": "9"},
|
||||
]))
|
||||
|
||||
self.assertEqual(response.body, [
|
||||
{"jsonrpc": "2.0", "result": 7, "id": "1"},
|
||||
{"jsonrpc": "2.0", "result": 19, "id": "2"},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": None},
|
||||
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "5"},
|
||||
{"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
|
||||
])
|
||||
|
||||
async def test_examples_rpc_call_batch_all_notifications(self):
|
||||
response = await self.manager.get_response_for_payload(json.dumps([
|
||||
{"jsonrpc": "2.0", "method": "notify_sum", "params": [1, 2, 4]},
|
||||
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
|
||||
]))
|
||||
self.assertIsNone(response)
|
||||
35
.venv/lib/python3.11/site-packages/ajsonrpc/utils.py
Normal file
35
.venv/lib/python3.11/site-packages/ajsonrpc/utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import inspect
|
||||
|
||||
|
||||
def is_invalid_params(func, *args, **kwargs):
|
||||
"""
|
||||
Method:
|
||||
Validate pre-defined criteria, if any is True - function is invalid
|
||||
0. func should be callable
|
||||
1. kwargs should not have unexpected keywords
|
||||
2. remove kwargs.keys from func.parameters
|
||||
3. number of args should be <= remaining func.parameters
|
||||
4. number of args should be >= remaining func.parameters less default
|
||||
"""
|
||||
# For builtin functions inspect.getargspec(funct) return error. If builtin
|
||||
# function generates TypeError, it is because of wrong parameters.
|
||||
if not inspect.isfunction(func):
|
||||
return True
|
||||
|
||||
signature = inspect.signature(func)
|
||||
parameters = signature.parameters
|
||||
|
||||
unexpected = set(kwargs.keys()) - set(parameters.keys())
|
||||
if len(unexpected) > 0:
|
||||
return True
|
||||
|
||||
params = [
|
||||
parameter for name, parameter in parameters.items()
|
||||
if name not in kwargs
|
||||
]
|
||||
params_required = [
|
||||
param for param in params
|
||||
if param.default is param.empty
|
||||
]
|
||||
|
||||
return not (len(params_required) <= len(args) <= len(params))
|
||||
Reference in New Issue
Block a user