Features

Subscriptions

The above example shows how we can create a trivial sender and then register a listener to one of its events. This mechanism is called subscription and it has several aspects related to it.

One of this aspects is that a listener can subscribe to a sender, before the sender is connected to pysig. A listener is not forced to wait for a sender to connect in order to subscribe to its events. Also, the listener can be informed when a sender is connected or disconnected via a special event, defined by sig.EVENT_CONNECT and sig.EVENT_DISCONNECT attributes.

Types of subscriptions

A listener can connect to multiple type of events, as follows:
  • a specific event, triggered by a specific sender
  • any event triggered by a specific sender, called sender broadcast event
  • any event triggered by any sender, called router broadcast event
  • a specific event, triggered by any sender, called channel
Type of Event Event Sender Syntax
normal specific specific router.addListener(callback, “sender”, “event”)
sender broadcast any specific router.addListener(callback, “sender”, None)
router broadcast any any router.addListener(callback, None, None)
channel specific any router.addListener(callback, None, “event”)

Each of these types of events will be described below, in this documentation.

Example

Let’s see how a listener can subscribe before a sender is connected, in the following example:

import sig

# this function will be called when the sender connects or disconnects
def listen_to_connect_disconnect(info, data):
    event = info.get("event")
    print "Sender connect/disconnect event, current state: ",
    if event == sig.EVENT_CONNECT:
        print "connected"
    else:    print "disconnected"

# this is your callback function
def listen_to_events(info, data):
    event = info.get("event")
    sender = info.get("sender")
    print "Received event '%s' from sender '%s' having data '%s'" % (event, sender, data)

def print_sender_state():
    print "Sender state: %s" % ("connected" if router.getSender("my_sender").isConnected() else "disconnected")

# create router
router = sig.Router()

# register listener, even thouhg no sender is yet connected
router.addListener(listen_to_events, "my_sender", "my_event_1")

# register to connect and disconnect events
router.addListener(listen_to_connect_disconnect, "my_sender", sig.EVENT_CONNECT)
router.addListener(listen_to_connect_disconnect, "my_sender", sig.EVENT_DISCONNECT)

print_sender_state()

# create sender (connect event will be triggered)
my_events = ["my_event_1", "my_event_2"]
sender = router.addSender("my_sender", my_events)

print_sender_state()

# disconnect sender (disconnect event will be triggered)
router.removeSender(sender)

print_sender_state()

# connect sender again (connect event will be triggered)
sender = router.addSender("my_sender", my_events)

print_sender_state()

# trigger first event
sender.getSignal(my_events[0]).trigger(None)

This example will have the following output:

Sender state: disconnected
Sender connect/disconnect event, current state:  connected
Sender state: connected
Sender connect/disconnect event, current state:  disconnected
Sender state: disconnected
Sender connect/disconnect event, current state:  connected
Sender state: connected
Received event 'my_event_1' from sender 'my_sender' having data 'None'

Broadcast events

We’ve talked about broadcast event, now it will be good to show an example of how they work.

Just before we start, we need to know that there are two broadcast events in pysig:

  1. the sender broadcast event
  2. the router broadcast event

Sender broadcast event

As the name suggests, the sender broadcast event, is that special event that is sent each time a specific sender triggers a signal.

A small example of how we can register a the broadcast event of a specific sender named my_sender:

# register to all events from this sender
router.addListener(listen_to_events, "my_sender")

This special event will be triggered upon any event sent by my_sender. When registering to a broadcast event, we can differentiate between different events fired by the same sender by using the info dictionary parameter passed to listeners callback.

Router broadcast event

This is a special event that is triggered before any event from any sender is triggered. When a listener is registered to this event, it will receive all events that are managed by the router object.

# register to all events from this router
router.addListener(listen_to_events)

Of course, you can differentiate between different senders, using the same info parameter. The info parameter is constructed by the Signal class and contains the following information:

{
  "sender" : sender_identifier
  "event"  : event_identifier
}

Example

In this example we will register a single listener, connected to all router events. The listener will print out when a sender is connected or disconnected and any other event triggered by any sender.

import sig

# this function will be called for any signal triggered
def listen_to_any(info, data):
    event = info.get("event")
    sender = info.get("sender")

    if event == sig.EVENT_CONNECT:
        print "Sender '%s' is now connected" % (sender)
    elif event == sig.EVENT_DISCONNECT:
        print "Sender '%s' is now disconnected" % (sender)
    else:
        print "Sender '%s' triggered event '%s' with data '%s'" % (sender, event, data)

# create router
router = sig.Router()

# register listener, even thouhg no sender is yet connected
router.addListener(listen_to_any)

# create sender (connect event will be triggered)
my_events = ["my_event_1", "my_event_2"]
sender = router.addSender("my_sender", my_events)

# disconnect sender (disconnect event will be triggered)
router.removeSender(sender)

# connect sender again (connect event will be triggered)
sender = router.addSender("my_sender", my_events)

# connect another sender
sender2 = router.addSender("my_second_sender", my_events)

# trigger some events
sender.getSignal(my_events[0]).trigger(None)
sender2.getSignal(my_events[1]).trigger(None)

And the output:

Sender 'my_sender' is now connected
Sender 'my_sender' is now disconnected
Sender 'my_sender' is now connected
Sender 'my_second_sender' is now connected
Sender 'my_sender' triggered event 'my_event_1' with data 'None'
Sender 'my_second_sender' triggered event 'my_event_2' with data 'None'

Needless to say, registering broadcast events can be a performance penalty if the listener is only interested in receiving only a couple of events and not all. It may still be good for debugging and tracing events when necessary.

Channel Events

A channel event is actually an event that shares a plural of senders. So far, the structure of events was limited to the scope of the senders that declared them upon registration.

For example, let’s say you have three posible sensors, each one destined to measure temperature, humidity and light intensity. These will play the role of senders, called temp, humidity and light. They all register the same event, called changed, that is fired when the value of their measurement changes significantly.

In order to listen for the changed event, you have to register three times, as follows:

router.addRemoteListener(callback, "temp"    , "changed")
router.addRemoteListener(callback, "humidity", "changed")
router.addRemoteListener(callback, "light"   , "changed")

If the data published to any of the changed event is generic enough to determine its type, registering for each sender in particular may not be so scalable if another set of sensors will be deployed later on. We will have to modify the code to keep adding subscription to senders that trigger the same event.

For this purpose pysig introces the notion of channel events. Using channel events, a listener can only subscribe to one generic event and listen for events from senders that share the same channel, transparently.

Like in the case of broadcast events, you can differentiate between different senders using the info parameter, passed to the callback.

Senders

The design of pysig intends that channel events to be declared explicit by the senders that want to share the same event. In order to achieve this easily, we only need to prefix the events with the “#” character.

For the temperature sensor, the code would look like:

sender = router.addRemoteSender("temp", ["#changed", "cold", "hot"])

This tells pysig that the changed event is a channel event, while the cold and hot events are events specific to this sensors.

To use the same channel, the humidity sensor will register like this:

sender = router.addRemoteSender("humidity", ["#changed", "dry", "wet"])

Now, the two senders share the same changed event. In comparison to a regular event, the main difference for the changed event is that allows listeners to register to this event without the need of knowing which senders are publishing it.

Listeners

This is where channel events are visibly different from any others.

We can now capture all changed events with only one subscription:

router.addRemoteListener(callback, event= "#changed")

or, equaly correct

router.addRemoteListener(callback, None, "#changed")

As it appears, we have registered to the broadcast sender for an event called changed. We will now receive the changed event from any sender that uses this channel.

Good to know

Channel events offers great flexibility for listeners, but they introduce some complexity that needs to be detailed.

  • The built-in events of pysig are by default channels. Therefore it may be possible to listen to any sig.EVENT_CONNECT event, for any sender that connects to the router, without having the need to subscribe to all published events and filter out connect messages.

    router.addRemoteListener(listen_for_connects, event= sig.EVENT_CONNECT)
    
  • If a sender does not explicitly declare an event as being a channel, it is considered a regular event. Therefore the following listener will receive nothing from the temp sender, even if the cold event is fired by it, because the temp sender didn’t declared the event as being a channel:

    router.addRemoteListener(callback, event= "cold")
    
  • The following subscription will register for the changed event from the temp sender and not to the changed channel, therefore will receive events only from temp sender:

    router.addRemoteListener(callback, "temp","#changed")
    

Requests

A nice feature of pysig is that is able to request data from a particular sender, without having the need of waiting a particular event to achieve that. This is called a request and can be made by anyone that has access to the Router object, to any registered sender.

The way you may issue a request, is as follows:

import sig
router = sig.Router()
[...]
response = router.request("my_sender", "getWeather")
print "Response for 'getWeather' is '%s'" % (response)

The limitation of the requests feature, is that it can only be issued to senders that:

  1. are connected to the router
  2. implements a special request handler

Example

The way you register a request handler for a sender:

import sig

class Weather:
        def getCurrentTemperature(self, params):
                return "29.5"
        def isHot(self, params):
                return True
        def isCold(self, params):
                return False

def default_request_handler(method, params):
        return "Unknown method %s (params: %s)" % (method, params)

# create router
router = sig.Router()

# create Weather object
weather = Weather()

# construct supported requests
requests = {
                "temperature" : weather.getCurrentWeather,
                "ishot"  : weather.isHot,
                "iscold" : weather.isCold,
                #
                # this handler is optional but when defined
                # all unknown requests will be passed to this handler
                #
                None : default_request_handler
        }

# create sender with support for requests
sender = router.addSender("my_sender", ["weather_change"], requests= requests)

# now we can request things from sender
response = sender.request("temperature")
print "temperature is now: %s" % (response)

response = router.request("my_sender","ishot")
print "Hot: %s" % (response)

# request something unknown
response = router.request("my_sender", "unknown_method", {"lorem" : "ipsum"})
print response

Notice how we can make requests directly using Sender instance or indirectly via the Router instance.

Also notice the default_request_handler method that is invoked when a particular method is not found as being implemented by the sender. This generic method is optional, but if not implemented a request sent with an unknown method for a particular sender, will otherwise raise an LookupError exception. This would have happened in the case of the last request to unknown_method request.

You can also choose just to implement this generic method instead of implementing separate methods for each method invoked by a request.

As a summary, the things you want to know about a request are:

  1. Works only for senders that are currently registered
  2. Any time the sender is reconnected it must pass its request handler object
  3. If the sender request handler doesn’t implement the generic None method, any request with an unknown method will raise a LookupError exception
  4. If the method implemented by the request handler raises an exception, the exception must be caught by caller
  5. The request returns the reponse received by the method implemented by the request handler

Logging support

Before using the library, it will be good to know that is supports logging. The default ‘logging’ library from python, can be connected to pysig, in the following manner:

import logging
import sig

# prepare logger object
logging.basicConfig()
logger = logging.getLogger('sig')

# setup sig logger
sig.setup_logger(logger)

If by any means, the logging library is not acceptable, pysig has its own logging utility called SimpleLogger. This utility will use print to output the logs, and inspect module in order to trace source line number and caller function. It will be described later in detail. (TBD)

import sig

# prepare logger
logger = sig.SimpleLogger(tag= "[sig]", level= sig.LOG_LEVELS.LEVEL_INFO)

# setup sig logger
sig.setup_logger(logger)

A glance of how the log will look like:

[sig][1713][             connect][    info]-[TCP_CL] Connecting to localhost:3000
[sig][1607][     _thread_receive][    info]-[TCP_CL] Started receive thread for localhost:3000
[sig][1653][     _thread_receive][    info]-[TCP_CL] Receive thread terminated