ivy.ivy module

Using an IvyServer

The following code is a typical example of use:

from ivy.ivy import IvyServer

class MyAgent(IvyServer):
  def __init__(self, agent_name):
    IvyServer.__init__(self,agent_name)
    self.start('127.255.255.255:2010')
    self.bind_msg(self.handle_hello, 'hello .*')
    self.bind_msg(self.handle_button, 'BTN ([a-fA-F0-9]+)')

  def handle_hello(self, agent):
    print('[Agent %s] GOT hello from %r'%(self.agent_name, agent))

  def handle_button(self, agent, btn_id):
    print(
      '[Agent %s] GOT BTN button_id=%s from %r'
      % (self.agent_name, btn_id, agent)
    )
    # let's answer!
    self.send_msg('BTN_ACK %s'%btn_id)

a=MyAgent('007')
Messages types:

BYE, ADD_REGEXP, MSG, ERROR, DEL_REGEXP, END_REGEXP, END_INIT, START_REGEXP, START_INIT, DIRECT_MSG, DIE

Separators:

ARG_START, ARG_END

Misc. constants:

DEFAULT_IVYBUS, PROTOCOL_VERSION, IVY_SHOULD_NOT_DIE, IvyApplicationConnected, IvyApplicationDisconnected, DEFAULT_TTL

Objects and functions related to logging:

ivylogger, debug, log, warn, error, ivy_loghdlr, ivy_logformatter

Implementation details

An Ivy agent is made of several threads:

  • an IvyServer instance

  • a UDP server, launched by the Ivy server, listening to incoming UDP broadcast messages

  • IvyTimer objects

Copyright (c) 2005-2023 Sebastien Bigaret <sebastien.bigaret@telecom-bretagne.eu>

class ivy.ivy.ClientsBinding(pattern: Pattern, clients: List[Tuple[IvyClient, int]])

Bases: object

Holds agents on the bus which are subscribed (bound) to a given regexp

clients: List[Tuple[IvyClient, int]]
pattern: Pattern
regexp() str
class ivy.ivy.IvyClient(ip: str, port: int, client_socket: socket, agent_id: str | None = None, agent_name: str | None = None)

Bases: object

Represents a client connected to the bus. Every callback methods registered by an agent receive an object of this type as their first parameter, so that they know which agent on the bus is the cause of the event which triggered the callback.

An IvyClient is responsible for:
  • performing the initialization required by the Ivy protocol,

  • sending the various types of messages specified in the protocol.

It is not responsible for receiving messages from the client: another object is in charge of that, namely an IvyHandler object.

The local IvyServer creates one IvyClient per agent on the bus.

Protocol-related methods:

start_init(), end_init(),

Announcing changes in our subscriptions:

send_new_subscription(), remove_subscription(),

Sending messages:

send_message(), send_direct_message(), send_die_message(), send_error(), send_ping(), send_pong(), wave_bye()

MT-safety

MT-safe.

end_init() None

Should be called when the initialization process ends.

Raises:

IvyIllegalStateError – if the method has already been called (and self.status has already been set to INITIALIZED)

get_next_ping_delta() float | None

Returns the time (in seconds) elapsed since the oldest ping request. See: get_next_ping_delta_ns() for details.

Returns:

the time (seconds) elapsed since the oldest ping request, or None if there is not such request.

get_next_ping_delta_ns() int | None

Returns the time elapsed since the oldest ping request, in nanoseconds. This oldest request is then discarded from the internal LIFO stack (see send_ping()).

Returns:

the time (in nanoseconds) elapsed since the oldest ping request. Returns None if there is no such request –i.e. either no ping was sent or the method has been called at least as many times as send_ping()).

remove_subscription(idx: int) None

Notifies the remote agent that we (the local agent) are not interested in a given subscription.

Parameters:
  • idx: the index/id of a subscription previously registered with send_new_subscription.

send_die_message(num_id: int = 0, msg: str = '') None

Sends a die message

send_direct_message(num_id: int, msg: str) None

Sends a direct message

Note: the message will be encoded by encode_message with numerical_id=num_id and params==msg; this means that if msg is not a string but a list or a tuple, the direct message will contain more than one parameter. This is an extension of the original Ivy design, supported by python, but if you want to inter-operate with applications using the standard Ivy API the message you send must be a string. See in particular in ivy.h:

typedef void (*MsgDirectCallback)( IvyClientPtr app,
                                   void *user_data, int id, char *msg ) ;
send_error(num_id: int, msg: str) None

Sends an error message

send_message(num_id: int, captures: Sequence) None

Sends a message to the client.

Parameters:
  • num_id: index of the client’s subscription matched by the message

  • captures: the capturing groups found when the regexp of that subscription match the message

send_new_subscription(idx: int, regexp: str) None

Notifies the remote agent that we (the local agent) subscribe to a new type of messages

Parameters:
  • idx: the index/id of the new subscription. It is the responsibility of the local agent to make sure that every subscription gets a unique id.

  • regexp: a regular expression. The subscription consists in receiving messages matching the regexp.

send_ping() None

Sends a PING request to the client. The time at which the request is sent is pushed into an internal LIFO stack, which is then used by get_next_ping_delta().

send_pong(num_id: int) None

Sends a PONG message to a client. This is intended to be sent after this client sent us a PING message.

start_init(agent_name: str) None

Finalizes the initialization process by setting the client’s agent_name. This is a Ivy protocol requirement that an application sends its agent-name only once during the initial handshake (beginning with message of type START_INIT and ending with a type END_INIT). After this method is called, we expect to receive the initial subscriptions for that client (or none); the initialization process completes after end_init is called.

Raises:

IvyIllegalStateError – if the method has already been called once

wave_bye(num_id: int = 0) None

Notifies the remote agent that we are about to quit

class ivy.ivy.IvyHandler(request, client_address, server)

Bases: StreamRequestHandler

An IvyHandler is associated to one IvyClient connected to our server.

It runs into a dedicate thread as long as the remote client is connected to us.

It is in charge of examining all messages that are received and to take any appropriate actions.

Implementation note: the IvyServer is accessible in self.server

handle() None
process_ivymessage(msg: str, client: IvyClient) bool

Examines the message (after passing it through the decode_msg() filter) and takes the appropriate actions depending on the message types. Please refer to the document The Ivy Architecture and Protocol and to the python code for further details.

Parameters:
  • msg: (should not include a newline at end)

  • client: the agent sending the message

Returns:

False if the connection should be terminated, True otherwise

server: IvyServer
exception ivy.ivy.IvyIllegalStateError

Bases: RuntimeError

Raised when a method is called in an incorrect context, for example when attemptin to send a message whilst the server isn’t initialised.

exception ivy.ivy.IvyMalformedMessage

Bases: Exception

Raised when a received message is incorrectly formed

exception ivy.ivy.IvyProtocolError

Bases: Exception

Raised when a received (well-formed) message is not recognized as a valid message

class ivy.ivy.IvyServer(agent_name: str, ready_msg: str | None = '', app_callback: ~typing.Callable = <function void_function>, die_callback: ~typing.Callable = <function void_function>, usesDaemons: bool = False)

Bases: ThreadingTCPServer

An Ivy server is responsible for receiving and handling the messages that other clients send on an Ivy bus to a given agent.

An IvyServer has two important attributes: usesDaemons and server_termination.

Communication on the ivybus:

start(), send_msg(), send_direct_message(), send_ready_message(), handle_msg(), stop()

Inspecting the ivybus:

get_clients(), _get_client(), get_client_with_name()

Our own subscriptions:

get_subscriptions(), bind_msg(), unbind_msg(), _add_subscription(), _remove_subscription(), _get_fct_for_subscription()

Other clients’ subscriptions:

add_client_binding(), get_client_bindings(), remove_client_binding()

Handling callbacks:

Callbacks can be registered either by assigning a Callable to a class attribute or by calling the corresponding method:

app_callback regexp_change_callback, bind_regexp_change() die_callback, bind_die_change() direct_callback, bind_direct_msg() pong_callback, bind_pong()

MT-safety

All public methods (not starting with an underscore _) are MT-safe

Builds a new IvyServer. A client only needs to call start() on the newly created instances to connect to the corresponding Ivy bus and to start communicating with other applications.

MT-safety: both functions app_callback() and die_callback() must be prepared to be called concurrently

Parameters:
  • agent_name: the client’s agent name

  • ready_msg: a message to send to clients when they connect

  • app_callback: a function called each time a client connects or disconnects. This function is called with a single parameter indicating which event occurred: IvyApplicationConnected or IvyApplicationDisconnected.

  • die_callback: called when the IvyServer receives a DIE message

  • usesDaemons: see above.

See also

bind_msg(), start()

add_client_binding(client: IvyClient, binding_id: int, regexp: str) None
bind_direct_msg(on_direct_msg_fct: Callable) None

Registers a callback to be called when a direct message is received.

bind_msg(on_msg_fct: Callable, regexp: str) int

Registers a new subscription, by binding a regexp to a function, so that this function is called whenever a message matching the regexp is received.

Parameters:
  • on_msg_fct: a function accepting as many parameters as there is groups in the regexp. For example:

    • the regexp '^hello .*' corresponds to a function called w/ no parameter,

    • '^hello (.*)': one parameter,

    • '^hello=([^ ]*) from=([^ ]*)': two parameters

  • regexp: (string) a regular expression

Returns:

the binding’s id, which can be used to unregister the binding with unbind_msg()

bind_pong(on_pong_callback: Callable) None

Registers a callback to be called when receiving a “pong” message.

bind_regexp_change(on_regexp_change_callback: Callable) None

Registers a callback to be called when a client on the bus adds or removes one of its subscriptions.

get_client(ip: str, port: int) IvyClient

Returns the corresponding client, and create a new one if needed.

If agent_id is not None, the method checks whether a client with the same id is already registered; if it exists, the method exits by returning None.

You should not need to call this, use get_client_with_name() instead

get_client_bindings(client: IvyClient) List[Tuple[int, str]]
get_client_with_name(name: str) List[IvyClient]

Returns the list of the clients registered with a given agent-name

See:

get_clients

get_clients() List[str]

Returns the list of the agent names of all connected clients

See:

get_client_with_name

get_subscriptions() List[Tuple[int, str]]
handle_die_message(msg_id: int, from_client: IvyClient) bool
handle_direct_msg(client: IvyClient, num_id: int, msg: str) None
Parameters:
  • client

  • num_id

  • msg

Returns:

handle_msg(client: IvyClient, idx: int, *params: Any) None

Simply call the function bound to the subscription id idx with the supplied parameters.

handle_new_client(client: IvyClient) None

Completes connection with the client

handle_pong(client: IvyClient, delta: float) None
handle_regexp_change(client: IvyClient, event: int, num_id: int, regexp: str) None
isAlive() bool
ivybus: Tuple[str, int] | None

Holds (broadcast_addr, port) of the Ivy bus after the server is started

port

The port on which the TCP server awaits connection

register_client(ip: str, port: int, client_socket: socket, agent_id: str | None = None, agent_name: str | None = None) IvyClient

Creates a new IvyClient.

If agent_id is not None, the method checks whether a client with the same id is already registered; if it exists, the method exits by returning None.

You should not need to call this, use get_client_with_name() instead

remove_client(ip: str, port: int, trigger_application_callback: bool = True) IvyClient | None

Removes a registered client

This method is responsible for calling server.app_callback

Returns:

the removed client, or None if no such client was found

Note

NO NETWORK CLEANUP IS DONE

remove_client_binding(client: IvyClient, binding_id: int) str | None
static run_callback(callback: Callable, callback_description: str, agent: IvyClient, *args: Any, **kw: Any) Any

Runs a callback, catching any exception it may raise.

Parameters:
  • callback: the function to be called

  • callback_description: the description to use in the error message, when an exception is raised.

  • agent: the IvyClient triggering the callback, which is passed as the first argument to the callback

  • on_exc: the returned value in case an exception was raised by the callback.

  • args: other positional arguments are passed as-is to the callback.

  • kw: other keyword arguments are passed as-is to the callback.

Returns:

the value returned by the callback, or on_exc if an exception was raised

send_direct_message(agent_name: str, num_id: int, msg: str, stop_on_first: bool = True) bool

Sends a direct message to the agent named agent_name. If there is more than one agent with that name on the bus, parameter stop_on_first determines the behaviour.

Parameters:
  • agent_name: the name of the agent(s) to which the direct message should be sent.

  • num_id: a numerical identifier attached to the direct message

  • msg: the direct message itself

  • stop_on_first: if True, the message to all agents having the same name will receive the message; if False the method exits after the first message has been sent.

Returns:

True if at least one direct message was sent

send_msg(message: str, *, to: IvyClient | None = None) int

Send a message to the clients which subscribed to such a message (or to a specific client if parameter to is set). Specifically, a message is sent to a given client each time the message matches one of its subscriptions ; as a consequence, this can result in more than one (ivy) message being sent to a client, depending on its subscriptions.

Parameters:
  • message – the message to send

  • to – the client to which the message is to be sent. When None (the default) the message is sent to all connected clients.

Returns:

the number of times a subscription matched the message

send_ready_message(client: IvyClient) None

Sends the ready message.

serve_forever(poll_interval: float = 0.5) None

Handle requests (calling handle_request()) until doomsday… or until stop() is called.

This method is registered as the target method for the thread. It is also responsible for launching the UDP server in a separate thread, see UDP_init_and_listen() for details.

You should not need to call this method, use start() instead.

server_termination

A threading.Event object that is set on server shutdown. It can be used either to test whether the server has been stopped (server_termination.isSet()) or to wait until it is stopped (server_termination.wait()). Application code should not try to set the Event directly, rather it will call stop() to terminate the server.

start(ivybus: str | None = None) None

Binds the server to the ivybus. The server remains connected until stop() is called, or until it receives and accepts a ‘die’ message.

Raises:

IvyIllegalStateError – if the server has already been started

stop() None

Disconnects the server from the ivybus. It also sets the server_termination event.

Raises:

IvyIllegalStateError – if the server is not running

unbind_msg(num_id: int) str

Unbinds a subscription

Parameters:

num_id – the binding’s id, as returned by bind_msg()

Returns:

the regexp corresponding to the unsubscribed binding

Raises:

KeyError – if no such subscription can be found

usesDaemons

Whether the threads are daemonic or not. Daemonic threads do not prevent python from exiting when the main thread stop, while non-daemonic ones do. Default is False. This attribute should be set through __init__() time and should not be modified afterwards.

class ivy.ivy.IvyTimer(server: IvyServer, nbticks: int, delay_ms: int, callback: Callable)

Bases: Thread

An IvyTimer object is responsible for calling a function regularly. It is bound to an IvyServer and stops when its server stops.

Interacting with a timer object:
  • Each timer gets a unique id, stored in the attribute id. Note that a dead timer’s id can be reassigned to another one (a dead timer is a timer that has been stopped)

  • To start a timer, simply call its method start()

  • To modify a timer’s delay: simply assign delay, the modification will be taken into account after the next tick. The delay should be given in milliseconds.

  • to stop a timer, assign abort to True, the timer will stop at the next tick (the callback won’t be called)

MT-safety:

Please note: start() starts a new thread; if the same function is given as the callback to different timers, that function should be prepared to be called concurrently. Specifically, if the callback accesses shared variables, they should be protected against concurrency problems (using locks e.g.).

Creates and activates a new timer.

Parameters:
  • server: the IvyServer related to this timer –when the server stops, so does the timer.

  • nbticks: the number of repetition to make. 0 (zero) means: endless loop

  • delay: the delay, in milliseconds, between two ticks

  • callback: a function called at each tick. This function is called with one parameter, the timer itself

callback

The function to be called at each tick.

delay

The delay between two ticks, in milliseconds

id

The timer unique id

nbticks

The number of ticks after which the timer stops. Zero (0) means infinity.

run() None

Calls the callback every delay ms. See IvyTimer for details on termination.

ivy.ivy.UDP_init_and_listen(broadcast_addr: str, port: int, socket_server: IvyServer) None

Called by an IvyServer at startup; the method is responsible for:

  • sending the initial UDP broadcast message,

  • waiting for incoming UDP broadcast messages being sent by new clients connecting on the bus. When it receives such a message, a connection is established to that client and that connection (a socket) is then passed to the IvyServer instance.

Parameters:
  • broadcast_addr: the broadcast address used on the Ivy bus

  • port: the port dedicated to the Ivy bus

  • socket_server: instance of an IvyServer handling communications for our client.

ivy.ivy.decode_MSG_params(params: str) Sequence[str]

Implements the special treatment of parameters in text messages (message type: MSG). The purpose here is to make sure that, when their last parameter is not ETX-terminated, they are processed the same way as in the reference ivy-c library.

ivy.ivy.decode_ivybus(ivybus: str | None = None) Tuple[str, int]

Transforms the supplied string into the corresponding broadcast address and port

Parameters:

ivybus – if None or empty, defaults to environment variable IVYBUS, or DEFAULT_IVYBUS if no such env.var. exists

Returns:

a tuple made of (broadcast address, port number). For example:

>>> print decode_ivybus('192.168.12:2010')
('192.168.12.255', 2010)

ivy.ivy.decode_msg(message: str) Tuple[int, int, Sequence[str]]

Extracts from an ivybus message its message type, the conveyed numerical identifier and its parameters.

Returns:

msg_type, numerical_id, parameters

Raises:

IvyMalformedMessage – if the message’s type or numerical identifier are not integers

ivy.ivy.encode_message(msg_type: int, numerical_id: int, params: str | Sequence = '') bytes

params is string -> added as-is params is list -> concatenated, separated by ARG_END

ivy.ivy.is_multicast(ip: str) bool

Tells whether the specified ip is a multicast address or not

Parameters:

ip – an IPv4 address in dotted-quad string format, for example 192.168.2.3

ivy.ivy.trace(*args, **kw)
ivy.ivy.void_function(*arg: Any, **kw: Any) Any

A function that accepts any number of parameters and does nothing