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 viamyapi.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, thesender
andreceiver
arguments of each method swap their roles. This is also possible upon initialization by givinginvert=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.
-
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, callmyapi.<method>.pass_secinfo(True)
. Listener calls then receive an additional kwarg calledsecinfo
, containing the received dictionary. I.e. your handler(s) must add asecinfo=
parameter in addition to the signature specified in theRemoteAPI
.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 theCodec
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 theRemoteAPI
.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 aPromise
immediately.If
allow_positional_args=True
, calls with positional (unnamed) arguments are accepted. Otherwise such arguments raiseValueError
. 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.