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 'getWether' 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 is: :: import sig class SenderRequestHandler: def getWeather(self, params): if params.get("when","") == "now": return 20.2 else return "unknown" def get(self, method, params): return "Unknown method %s" % (method) # create router router = sig.Router() # create sender with support for requests sender = router.addSender("my_sender", ["weather_change"], request_handler= SenderRequestHandler()) # now we can request things from sender response = sender.request("getWeather", {"when":"now"}) print "Weather now is: %s" % (response) response = router.request("my_sender","getWether",{"when":"tomorrow"}) print "Weather tomorrow is: %s" % (response) # request something unknown response = router.request("my_sender", "getWeatherInformation", None) print response Notice how we can make requests directly using **Sender** instance or indirectly via the **Router** instance. Also notice the **get** method from **SenderRequestHandler** that is invoked when a particular method is not found as being implemented by the request handler. 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 *getWeatherInformation*. You can also choose just to implement this generic **get** 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 **get** method, any request with an unknown method will raise an **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