Package: sansio-jsonrpc
JSON-RPC v2.0 Sans I/OThis project providesa Sans I/Oimplementation ofJSON-RPC v 2.0. This means that the library
handles all of the encoding, decoding, framing, and logic required by the protocol
specification, but it does not implement any I/O (input or output). In order to use this
library, you need to either to use a downstream project that wraps I/O around this
project or else write your own I/O wrapper.Client ExampleThis example illustrates what the library does and how it needs to be integrated into
an I/O framework of your choosing.fromsansio_jsonrpcimportJsonRpcPeer,JsonRpcParseError,JsonRpcExceptionclient=JsonRpcPeer()request_id,bytes_to_send=client.request(method='open_vault_door',args={'employee':'Mark','pin':1234},)First, we instantiate the client. We call therequestmethod to create a new JSON-RPC
request and convert it into abytesrepresentation that is suitable for sending to the
server. The method also returns arequest_id, which is a hashable value that can be
used to correlate any future response back to the request that initiated it.connection.send(bytes_to_send)received_bytes=connection.recv()Remember, this is a Sans I/O library, so it does not do any networking itself. It is the
caller's responsibility to transfer data to and from the remote machine. This block
shows a hypothetical I/O framework being used to send the pending request and receive
the response asbytes, but this can be implemented with any I/O framework of your
choosing.try:messages=client.parse(received_bytes)exceptJsonRpcParseErroraspe:print('Exception while parsing response',pe)In this block, we feed the data received from the remote machine into the client
object'sparse()method. This method parses the incoming data and returns an iterable
containing messages sent by the server. (In the current implementation, the iterable
always contains exactly 1 message, but the API is designed this way to allow for future
enhancements such as a streaming JSON parser that can return 0-n messages after parsing
each chunk of data.)forresponseinmessages:assertisinstance(response,JsonRpcResponse)print('received a response:',response.id)ifresponse.is_success:print('success:',response.result)else:print('error:',response.error)exc=JsonRpcException.exc_from_error(response.error)Finally, we iterate over the messages. In the case of a client, these messages will
always beJsonRpcReponseobjects, which contain either result or error information.
Theresponse.idfield should match therequest_idobtained earlier.If you want an error response to raise an exception, you can callJsonRpcException.exc_from_error(...)and it will construct an exception for you. The
exception system is described in a separate section below. Of course, be careful where
you throw the exception, since throwing an exception while reading multiple responses
will result in the rest of the responses being ignored. In a concurrent system, you
probably want to raise the exception to the code path that calledrequest().In a concurrent system, you will also want to multiplex requests and responses on the
same connection. This library does not implement multiplexing itself because
multiplexing with appropriate flow control (see "Back Pressure" below) is going to
depend on the transport and I/O framework you are using.Server ExampleThis example shows how to receive a JSON-RPC request and dispatch a response.fromsansio_jsonrpcimport(JsonRpcPeer,JsonRpcException,JsonRpcParseError,JsonRpcMethodNotFoundError,)server=JsonRpcPeer()This example starts out the same way as the client: instantiating aJsonRpcPeerobject. Note that the client and server implementation are actually in the same class!This might be surprising at first but it is the most flexible way to implement JSON-RPC.
For example, the specification says that a notification is a special type of request,
and a request can only be sent from client to server. But there are scenarios where it
would be useful to have the server send notifications to the client, e.g. a
publish/subscribe system. The specification says that:One implementation of this specification could easily fill both of those roles, even
at the same time, to other different clients or the same client.This is why the library implements both roles in a single class. The choice to name the
objectserveris just a convention to help us remember what's going on.received_bytes=connection.recv()This block shows the hypothetical I/O framework again. As a server, we just want to wait
for a client to send us something.try:messages=server.parse(received_bytes)exceptJsonRpcParseErroraspe:bytes_to_send=server.respond_with_error(request=None,error=pe.get_error())connection.send(bytes_to_send)returnNext, we want to parse the incoming data. If a parse error is raised here, then we
should send back a response and we should not iterate over the messages, becausemessageswas not actually returned!. Because we were unable to parse a request, we setrequest=Nonein the response.for request in messages:
    assert isinstance(request, JsonRpcRequest)

    try:
        # Handle request here and send a response.
        result = handle_request(request)
        bytes_to_send = server.respond_with_result(request=request, result=result)
    except JsonRpcException as jre:
        bytes_to_send = server.respond_with_error(request=request, error=pe.get_error())

    connection.send(bytes_to_send)Next, we iterate over the received messages. For a server, each message should be aJsonRpcRequest. The implementation ofhandle_request(...)is up to you! You should
handle each request by returning a result. Therespond_with_result(...)method
produces an appropriate JSON-RPC response, which you should then send to the client.If an error occurs in your handler, you should raise one of the built-in exceptions or a
custom subclass ofJsonRpcApplicationError. For example, if you don't have a handler
for a given method, you should raiseJsonRpcMethodNotFoundError. Exceptions are
described more fully below.ExceptionsThe exception system in this library is designed to make error-handling as Pythonic as
possible, abstracting over the details of the JSON-RPC protocol as much as possible. All
exceptions in this module inherit fromJsonRpcException, so this is a good choice for
a catch-all exception handler. All exceptions have an error code and a message.
Exceptions can also optionally have adatafield which can be set to any valid JSON
data type.There are also specialized errors in the library that correspond to specific JSON-RPC
error codes in the specification. For example, if you receive data that cannot be
decoded into a valid JSON-RPC request, the specification calls for a -32700 error, and
this library raises aJsonRpcParseErrorto make it easy to catch this specific
condition. If the server sendsThe specification also allows you to define your own error codes. There areExceptions are used in two ways in this library. First of all, the library itself can
raise exceptions. For example inrecv(), it can raiseJsonRpcParseErroras described
above. The second usage is that the remote peer might send you a response that contains
an error object. In this case, you may want to convert that error into an exception so
you can raise it.The static methodJsonRpcException.exc_from_error(...)converts an error into an
exception. This method automatically selects the appropriate subclass ofJsonRpcException. For example, if the remote peer sent you error code -32700, this
method converts that into aJsonRpcParseError. Some error codes are reserved for
implementations to define their own error codes, e.g. -32099 is reserved in the
specification but not assigned any meaning. If the remote peer sends this error code,
then this method will raiseJsonRpcReservedError.The specification also has unreserved error codes that are available for defining
application-specific errors. You can use these in your own code in two different ways.
The first approach is to catch/raiseJsonRpcApplicationErrorand set/check the error
code to your application-defined value. The second approach is to create your own
subclass ofJsonRpcApplicationError. Here's an example:fromsansio_jsonrpcimportJsonRpcApplicationErrorclassMyApplicationError1(JsonRpcApplicationError):ERROR_CODE=1ERROR_MESSAGE="My application error type 1"classMyApplicationError2(JsonRpcApplicationError):ERROR_CODE=2ERROR_MESSAGE="My application error type 2"TheJsonRpcException.exc_from_error(...)method will automatically select the
appropriate subclass. For example if the server sends error code 1, then the method will
return aMyApplicationError1instance, even though that class is defined inyour
code! The library uses some metaclass black magic to make this work.Back PressureAs a SANS I/O library, this package does not implement any sort of flow control. If the
peer is sending you data faster than you can handle it, and you keep reading it, then
your process's memory usage will continually grow until it runs out of memory or the
kernel terminates it. Back pressure means signalling to the peer that it should stop
sending data for a bit until your process can catch up. The specifics of back pressure
really depend on what transport protocol and I/O framework you are using. For example,
TCP has flow control capabilities, and most implementations will automatically apply
back pressure if you simply stop reading from the socket. Therefore, if you use TCP with
this library, you should be careful to read from the socket only when you are ready to
process another message. If you eagerly read from the socket into an unbounded user
space buffer (such as a queue), then your code will not benefit from TCP's flow control,
because the kernel will see an empty buffer and it will keep filling it up.DevelopingThe project uses MyPy for type checking and Black for code formatting. Poetry is used to
manage dependencies and to build releases.