quickrpc.remote_api module

A RemoteAPI is an interface-like class whose methods correspond to remote calls.

Let us start with an example:

from quickrpc.remote_api import RemoteAPI, incoming, outgoing

class MyAPI(RemoteAPI):
    @incoming
    def notify(self, sender, arg1=val1, arg2=val2):
        """notification that something happened"""

    @outgoing
    def helloworld(self, receivers, arg1=val1):
        """Tell everybody that I am here."""

    @incoming(has_reply=true)
    def echo(self, sender, text="test"):
        """returns the text that was sent."""

RemoteAPI is used by subclassing it. Remote methods are defined by the @incoming and @outgoing decorators.

Important

The method body of remote methods must be empty.

This is because by caling @outgoing methods, you actually issue a call over the Transport that is bound to the RemoteAPI at runtime. Since the API is meant to be used by both sides (by means of inverting it), @incoming methods should be empty, too. The side effect of this is that the class definition is more or less a printable specification of your interface.

@incoming methods have a .connect() method to attach an implementation to that message. The connected handler has the same signature as the @incoming method, except for the self argument.

By default, all defined calls are resultless (i.e. notifications). To define calls with return value, decorate with has_reply=True kwarg.

When handling such a call on the incoming side, your handler’s return value is returned to the sender. Exceptions are caught and sent as error reply.

On the outgoing side, the call immediately returns a Promise object. You then use result() to get at the actual result. This will block until the result arrived.

(TODO: make blocking call by default, add block=False param for Promises)

class quickrpc.remote_api.RemoteAPI(codec='jrpc', transport=None, security='null', invert=False, async_processing=False)[source]

Bases: object

Describes an API i.e. a set of allowed outgoing and incoming calls.

Subclass and add your calls.

codec holds the Codec for (de)serializing data. transport holds the underlying transport. security holds the security provider.

Both can also be strings, then Transport.fromstring() / Codec.fromstring() are used to acquire the respective objects. In this case, transport still needs to be started via myapi.transport.start().

Methods marked as @outgoing are automatically turned into messages when called. The method body is executed before sending. (use e.g. for validation of outgoing data). They must accept a special receivers argument, which is passed to the Transport.

Methods marked as @incoming are called by the transport when messages arrive. They work like signals - you can connect your own handler(s) to them. Connected handlers must have the same signature as the incoming call. All @incoming methods MUST support a senders argument.

Connect like this:

>>> def handler(self, foo=None): pass
>>> remote_api.some_method.connect(handler)
>>> # later
>>> remote_api.some_method.disconnect(handler)

Execution order: the method of remote_api is executed first, then the connected handlers in the order of registering.

Incoming messages with unknown method will not be processed. If the message has .id != 0, it will automatically be replied with an error.

Threading:

  • outgoing messages are sent on the calling thread.
  • If async_processing = False, incoming messages are handled on the thread which handles Transport receive events. I.e. the Transport implementation defines the behaviour.
  • If async_processing = True, an extra Thread is used to handle messages.

The latter allows the receive handler to run concurrently to message handling, allowing further requests to be sent out and to await the result. However it means one extra thread. In any case, only one incoming message is handled at a time.

Recommendation is to set async_processing=True if there are any outgoing calls that have a reply, False if not.

Inverting:

You can invert() the whole api, swapping incoming and outgoing methods. When inverted, the sender and receiver arguments of each method swap their roles. This is also possible upon initialization by giving invert=True kwarg.

invert()[source]

Swaps @incoming and @outgoing decoration on all methods of this INSTANCE.

I.e. generates the opposite-side API.

Do this before connecting any handlers to incoming calls.

You can achieve the same effect by instantiating with invert=True kwarg.

message_error(sender, exception, in_reply_to=None)[source]

Called each time that an incoming message causes problems.

By default, it logs the error as warning. in_reply_to is the message that triggered the error, None if decoding failed. If the requested method can be identified and has a reply, an error reply is returned to the sender.

transport

Gets/sets the transport used to send and receive messages.

You can change the transport at runtime.

unhandled_calls()[source]

Generator, returns the names of all incoming, unconnected methods.

If no results are returned, all incoming messages are connected. Use this to check for missed .connect calls.

quickrpc.remote_api.incoming(unbound_method=None, has_reply=False, allow_positional_args=False)[source]

Marks a method as possible incoming message.

@incoming(has_reply=False, allow_positional_args=False)

Incoming methods keep list of connected listeners, which are called with the signature of the incoming method (excluding self). The first argument will be passed positional and is a string describing the sender of the message. The remaining arguments can be chosen freely and will usually be passed as named args.

Optionally, you can receive security info (the secinfo dict extracted from the message). For this, call myapi.<method>.pass_secinfo(True). Listener calls then receive an additional kwarg called secinfo, containing the received dictionary. I.e. your handler(s) must add a secinfo= parameter in addition to the signature specified in the RemoteAPI.

Listeners can be added with myapi.<method>.connect(handler) and disconnected with .disconnect(handler). They are called in the order that they were added.

If has_reply=True, the handler should return a value that is sent back to the sender. If multiple handlers are connected, at most one of them must return something.

Notice:

Processing of incoming messages does not resume until all listeners returned. This means that if you issue a followup remote call in a listener, the result can not arrive while the listener is executing. If you want to do this, use promise.then() to resume when the result is there.

You can also spawn a new thread in your listener, to do the processing. However, be aware that this makes you vulnerable against DOS attacks, since an attacker can make you open arbitrary many threads this way.

If allow_positional_args=True, messages with positional (unnamed) arguments are accepted. Otherwise such arguments throw an error message without executing the handler(s). Note that the Codec must support positional and/or mixed args as well. It is strongly recommended to use named args only.

Lastly, the incoming method has a myapi.<method>.inverted() method, which will return the @outgoing variant of it.

quickrpc.remote_api.outgoing(unbound_method=None, has_reply=False, allow_positional_args=False)[source]

Marks a method as possible outgoing message.

@outgoing(has_reply=False, allow_position_args=False)

Invocation of outgoing methods leads to a message being sent over the Transport of the RemoteAPI.

The first argument must be the list of receivers of the message, as a list of strings. When calling the method, usually you will use the sender name(s) received via an incoming call. Set receivers=None to send to all connected peers.

The remaining arguments can be choosen freely. The argument values can be anything supported by the Codec that you use. The builtin Codecs support all the “atomic” builtin types, as well as dicts and lists.

If has_reply=True, the other side is expected to return a result value. In this case, calling the outgoing method returns a Promise immediately.

If allow_positional_args=True, calls with positional (unnamed) arguments are accepted. Otherwise such arguments raise ValueError. For sending, they will be converted into named arguments. It is strongly recommended to use named args only.

Lastly, the outgoing method has a myapi.<method>.inverted() method, which will return the @incoming variant of it.