build: Initialize Python virtual environment and install project dependencies.
Some checks failed
WLED CI / wled_build (push) Has been cancelled

This commit is contained in:
2026-02-19 22:31:26 +08:00
parent ca1319462e
commit bb17574d0c
1668 changed files with 469578 additions and 0 deletions

View 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"

View File

@@ -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)

View 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

View 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

View File

@@ -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

View 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)

View 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

View 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)

View 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()

View 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")

View File

@@ -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)

View File

@@ -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)

View 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))