jsonrpyc#
Minimal python RPC implementation based on the JSON-RPC 2.0 specs.
Original source hosted at GitHub.
Usage#
jsonrpyc.RPC instances basically wrap an input stream and an output stream in order to communicate with other services.
A service is not even forced to be written in Python as long as it strictly implements the JSON-RPC 2.0 specs.
A suitable implementation for NodeJs is node-json-rpc.
Request parameters can be passed as a plain list (positional arguments) or object (keyword arguments), or in the {"args": [...], "kwargs": {...}} format used by jsonrpyc itself.
A jsonrpyc.RPC instance may wrap a target object.
Incoming requests will be routed to methods of this object whose result might be sent back as a response.
Nested attributes can be addressed via dots (e.g. "sub.method"), while attributes starting with two underscores are not accessible.
Example implementation:
server.py#
import jsonrpyc
class MyTarget:
def greet(self, name: str) -> str:
return f"Hi, {name}!"
jsonrpyc.RPC(MyTarget())
client.py#
import jsonrpyc
from subprocess import Popen, PIPE
p = Popen(["python", "server.py"], stdin=PIPE, stdout=PIPE)
rpc = jsonrpyc.RPC(stdout=p.stdin, stdin=p.stdout)
#
# sync usage
#
print(rpc("greet", args=("John",), block=0.1))
# => "Hi, John!"
#
# async usage
#
def cb(err: Exception | None, res: str | None = None) -> None:
if err:
raise err
print(f"callback got: {res}")
rpc("greet", args=("John",), callback=cb)
# cb is called asynchronously which prints
# => "callback got: Hi, John!"
#
# shutdown
#
rpc.close()
p.stdin.close()
p.stdout.close()
p.terminate()
p.wait()
API Docs#
Minimal python RPC implementation based on the JSON-RPC 2.0 specs from http://www.jsonrpc.org/specification.
Classes#
- class RPC(target: Any | None = None, stdin: InputStream | None = None, stdout: OutputStream | None = None, *, watch: bool = True, watch_kwargs: dict[str, Any] | None = None)[source]#
Bases:
objectThe main class of jsonrpyc. Instances of this class wrap an input stream stdin and an output stream stdout in order to communicate with other services. A service is not even forced to be written in Python as long as it strictly implements the JSON-RPC 2.0 specification. RPC instances may wrap a target object. By means of a
Watchdoginstance, incoming requests are routed to methods of this object whose result might be sent back as a response. The watchdog instance is created but not started yet, when watch is not True.- Parameters:
target – The target object to wrap.
stdin – The input stream.
stdout – The output stream.
watch – Whether to start the watchdog.
watch_kwargs – Additional keyword arguments for the watchdog.
Example implementation:
server.py
import jsonrpyc class MyTarget(object): def greet(self, name): return f"Hi, {name}!" jsonrpyc.RPC(MyTarget())
client.py
import jsonrpyc from subprocess import Popen, PIPE p = Popen(["python", "server.py"], stdin=PIPE, stdout=PIPE) rpc = jsonrpyc.RPC(stdout=p.stdin, stdin=p.stdout) # non-blocking remote procedure call with callback and js-like signature def cb(err, res=None): if err: raise err print(f"callback got: {res}") rpc("greet", args=("John",), callback=cb) # cb is called asynchronously which prints # => "callback got: Hi, John!" # blocking remote procedure call with 0.1s polling print(rpc("greet", args=("John",), block=0.1)) # => "Hi, John!" # shutdown the rpc and the process rpc.close() p.stdin.close() p.stdout.close() p.terminate() p.wait()
- target#
The wrapped target object. Might be None when no object is wrapped, e.g. for the client RPC instance.
- stdin#
The input stream, re-opened with
"rb".
- stdout#
The output stream, re-opened with
"wb".
Instances can be used as context managers, calling
close()on exit.- close(timeout: float | None = None) None[source]#
Stops the watchdog and waits for it to finish for at most timeout seconds (indefinitely when None). On Windows, a watchdog that is waiting for input only finishes once new content or EOF is received, so passing a timeout is recommended there.
- Parameters:
timeout – The maximum time in seconds to wait for the watchdog to finish.
- Returns:
None.
- call(method: str, args: tuple[Any, ...] = (), kwargs: dict[str, Any] | None = None, *, callback: Callable[[Exception | None, Any], None] | None = None, block: float = 0, timeout: float = 0) Any[source]#
Performs an actual remote procedure call by writing a request representation (a string) to the output stream. The remote RPC instance uses method to route to the actual method to call with args and kwargs.
When callback is set, it will be called with the result of the remote call. When block is larger than 0, the calling thread is blocked until the result is received. In this case, block will be the poll interval, emulating synchronous return value behavior. When both callback is None and block is 0 or smaller, the request is considered a notification and the remote RPC instance will not send a response.
If timeout is not zero, raise TimeoutError after timeout seconds with no response.
- Parameters:
method – The method to call.
args – The positional arguments of the method.
kwargs – The keyword arguments of the method.
callback – The callback function.
block – The poll interval in seconds.
timeout – The timeout in seconds.
- Returns:
None if block is non-positive, otherwise the result of the remote call.
- Raises:
TimeoutError – When the request times out.
- class Spec[source]#
Bases:
objectThis class wraps methods that create JSON-RPC 2.0 compatible string representations of request, response and error objects. All methods are class members, so you might never want to create an instance of this class, but rather use the methods directly:
Spec.request("my_method", 18) # the id is optional # => '{"jsonrpc":"2.0","method":"my_method","id":18}' Spec.response(18, "some_result") # => '{"jsonrpc":"2.0","id":18,"result":"some_result"}' Spec.error(18, -32603) # => '{"jsonrpc":"2.0","id":18,"error":{"code":-32603,"message":"Internal error"}}'
- classmethod check_id(id: str | int | None, *, allow_empty: bool = False) None[source]#
Value check for id entries. When allow_empty is True, id is allowed to be None. Raises a TypeError when id is neither an integer nor a string (booleans are rejected as well).
- Parameters:
id – The id to check.
allow_empty – Whether id is allowed to be None.
- Returns:
None.
- Raises:
TypeError – When id is invalid.
- classmethod check_method(method: str, /) None[source]#
Value check for method entries. Raises a TypeError when method is not a string.
- Parameters:
method – The method to check.
- Returns:
None.
- Raises:
TypeError – When method is invalid.
- classmethod check_code(code: int, /) None[source]#
Value check for code entries. Raises a TypeError when code is not an integer or when there is no
RPCErrorsubclass registered for that code.- Parameters:
code – The error code to check.
- Returns:
None.
- Raises:
TypeError – When code is invalid.
- classmethod request(method: str, id: str | int | None = None, *, params: Any = None) str[source]#
Creates the string representation of a request that calls method with optional params which are encoded by
json.dumps. When id is None, the request is considered a notification.- Parameters:
method – The method to call.
id – The id of the request.
params – The parameters of the request.
- Returns:
The request string.
- Raises:
RPCInvalidRequest – When method or id are invalid.
RPCParseError – When params could not be encoded.
- classmethod response(id: str | int | None, result: Any, /) str[source]#
Creates the string representation of a response that was triggered by a request with id. A result is required, even if it is None.
- Parameters:
id – The id of the request that triggered this response.
result – The result of the request.
- Returns:
The response string.
- Raises:
RPCInvalidRequest – When id is invalid.
RPCParseError – When result could not be encoded.
- classmethod error(id: str | int | None, code: int, *, data: Any | None = None) str[source]#
Creates the string representation of an error that occurred while processing a request with id. id might be None when the id of the request could not be determined, e.g. for parse errors. code must lead to a registered
RPCError. data might contain additional, detailed error information and is encoded byjson.dumpswhen set.- Parameters:
id – The id of the request that triggered this error.
code – The error code.
data – Additional error data.
- Returns:
The error string.
- Raises:
RPCInvalidRequest – When id or code are invalid.
RPCParseError – When data could not be encoded.
- class Watchdog(rpc: RPC, name: str = 'watchdog', interval: float = 0.1, daemon: bool = False, start: bool = True)[source]#
Bases:
ThreadThis class represents a thread that watches the input stream of an
RPCinstance for incoming content and dispatches requests to it.- Parameters:
rpc – The
RPCinstance to watch.name – The thread’s name.
interval – The polling interval of the run loop.
daemon – The thread’s daemon flag.
start – Whether to start the thread immediately.
- name#
The thread’s name.
- interval#
The polling interval of the run loop.
- daemon#
The thread’s daemon flag.
- exception RPCError(data: Any = None, *, code: int | None = None, title: str | None = None)[source]#
Bases:
ExceptionBase class for RPC errors.
- Parameters:
data – Additional error data.
- code_range#
The range of error codes that this error class is responsible for.
- code#
The error code of this error.
- title#
The title of this error.
- message#
The message of this error, i.e.,
"<title> (<code>)[, data: <data>]".
- data#
Additional data of this error.
code and title can be passed to override the class-level defaults, e.g. for errors with codes that have no registered error class.
- register_error(cls: type[RPCError]) type[RPCError][source]#
Decorator that registers a new RPC error derived from
RPCError. The purpose of error registration is to have a mapping of error codes/code ranges to error classes for faster lookups during error creation.@register_error class MyCustomRPCError(RPCError): code_range = (lower_bound, upper_bound) # both inclusive code = code_range[0] # default code when used as is title = "My custom error"
- Parameters:
cls – The error class to register.
- Returns:
The error class.
- Raises:
TypeError – When cls is not a subclass of
RPCErroror has an invalid code range.ValueError – When the code is not within the code range, or the code range overlaps with that of an already registered error class.
- get_error(code: int) type[RPCError][source]#
Returns the RPC error class that was previously registered to code. A
ValueErroris raised if no error class was found for code.- Parameters:
code – The error code to look up.
- Returns:
The error class.
- Raises:
ValueError – When no error class was found for code.
RPC errors#
- exception RPCParseError(data: Any = None, *, code: int | None = None, title: str | None = None)[source]#
Bases:
RPCError
- exception RPCInvalidRequest(data: Any = None, *, code: int | None = None, title: str | None = None)[source]#
Bases:
RPCError
- exception RPCMethodNotFound(data: Any = None, *, code: int | None = None, title: str | None = None)[source]#
Bases:
RPCError
- exception RPCInvalidParams(data: Any = None, *, code: int | None = None, title: str | None = None)[source]#
Bases:
RPCError
Project Info#
Installation#
Install simply via pip.
pip install jsonrpyc
# or with optional dev dependencies
pip install jsonrpyc[dev]
Contributing#
If you like to contribute to jsonrpyc, I’m happy to receive pull requests.
The full testing pipeline is based on pre-commit. Run the following to install development dependencies and set it up:
# inside the cloned repository
git lfs install
pip install -e .[dev]
pre-commit install
Now, every time you make a commit, the pre-commit and pre-push hooks will automatically run linting, type checking and unit tests. To run them manually, use
# for linting, type checking and additional checks on all files
pre-commit run --all-files
# only for staged files
pre-commit run
# or run unit tests standalone
pytest
Development#
Source hosted at GitHub
Report issues, questions, feature requests on GitHub Issues