1.7. Adding signals and receivers¶
In this chapter we will learn how our pattern can send signals instead of logging text. And how to register a receiver for this signal inside our plugin.
Signals & receivers are used to provide an easy way of asynchronous communication between plugins. For instant: one plugin creates a new user and another plugins needs to send a welcome e-mail.
Another advantage is that the sending plugin does not need to know to which plugins it has to send the information. And the receiving plugins do not need to know, from which plugin they have to collect the information. Both of them only need to know the signal name.
1.7.1. Register and send signals¶
Let’s add a signal to our CsvWatcherPattern
and remove all log message (or set the log-level to DEBUG):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | class CsvWatcherPattern(GwThreadsPattern):
def __init__(self, app, **kwargs):
super().__init__(app, **kwargs)
# Adds csv_watcher to each plugin
# This may be done several times. Once for each plugin,
# which inherits from CsvWatcherPattern
self.csv_watcher = CsvWatcherPlugin(plugin=self)
# Adds csv_watcher on application level
# This is done only once for each application
if not hasattr(app, "csv_watcher"):
app.csv_watcher = CsvWatcherApplication()
# Registers a signal, which get s called every time a change
# is detected inside an watched csv file.
if self.signals.get("csv_watcher_change") is None:
self.signals.register(signal="csv_watcher_change",
description="indicates a change in a monitored csv file.")
# ... some not changed code ...
def _csv_watcher_thread(self, plugin):
# ... some not changed code ...
while True:
csv_file_object = open(csv_file)
new_content = list(csv.DictReader(csv_file_object))
if new_content != old_content:
plugin.log.debug("Change detected")
new_rows = []
missing_rows = []
# Check if there are new/changed rows
for row in new_content:
if row not in old_content:
new_rows.append(row)
# Check if old rows are missing
for row in old_content:
if row not in new_content:
missing_rows.append(row)
# Store the current csv file content as old content
old_content = new_content
plugin.signals.send("csv_watcher_change",
csv_file=csv_file,
new_rows=new_rows,
missing_rows=missing_rows)
csv_file_object.close()
# Wait x seconds
time.sleep(interval)
|
In lines 17-19 we have registered our signals. A signal with the same name can only be registered once.
Therefore we must be sure that we do not register it again, if a second plugin inherits from CsvWatcherPattern
.
Instead of logging new or missing rows, we add the changed rows to two lists, which we later send to all receivers of our signal [40+45].
The final signal is send in line 50. We add two keyword arguments to it: new_rows
and missing_rows
.
1.7.2. Register a receiver¶
Right now nothing gets printed, if our pattern detects a change.
Let’s change this by adding a receiver to our plugin CsvWatcherPlugin
and
create a function, which logs the changes again. (Changes are highlighted)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | from click import Argument, Option
from groundwork.patterns import GwCommandsPattern
from csv_manager.patterns import CsvWatcherPattern
class CsvWatcherPlugin(GwCommandsPattern, CsvWatcherPattern):
"""
A plugin for monitoring csv files.
"""
def __init__(self, app, **kwargs):
self.name = "CsvWatcherPlugin"
super().__init__(app, **kwargs)
self.csv_file = None
self.csv_interval = None
self.watcher_thread = None
def activate(self):
# Argument for our command, which stores the csv file path.
path_argument = Argument(("csv_file",),
required=True,
type=str)
interval_option = Option(("-i", "--interval"),
type=int,
default=10,
help="Sets the time between two checks in seconds")
self.commands.register("csv_watch",
"Monitors csv files",
self.csv_watcher_command,
params=[path_argument, interval_option])
self.signals.connect(receiver="csv_change_receiver",
signal="csv_watcher_change",
function=self.csv_change_monitor,
description="Gets called for each csv change")
def csv_watcher_command(self, csv_file, interval=10):
# Register thread
self.watcher_thread = self.csv_watcher.register(csv_file, interval, "Watcher for %s" % csv_file)
# Start thread
self.watcher_thread.run()
def csv_change_monitor(self, plugin, **kwargs):
new_rows = kwargs.get("new_rows", None)
missing_rows = kwargs.get("missing_rows", None)
csv_file = kwargs.get("csv_file", "unknown file")
for row in new_rows:
self.log.info("%s has new row: %s" % (csv_file, row))
for row in missing_rows:
self.log.info("%s is missing row: %s" % (csv_file, row))
def deactivate(self):
pass
|
All we need is a function, which shall be called, if the signal is received: csv_change_monitor
[50-59].
And it must be connected to the signal [35-38].
Our function csv_change_monitor
must have 2 parameters: plugin
and **kwargs
.
plugin
contains the plugin instance, which has connected this function to the signal.
And **kwargs
can contain everything or nothing. It’s up by the signal sender to fill data in here.
However, we know that there may be three entries in **kwargs
: csv_file, new_rows and missing_rows.
So we try to get them [51-53] and log them [55, 58].
1.7.3. Let it run¶
Again let’s make a test run:
>>> csv_manager csv_watch -i 5 test.csv
2017-01-15 15:31:46,425 - INFO - Application signals initialised
2017-01-15 15:31:46,624 - INFO - Application commands initialised
2017-01-15 15:31:46,624 - INFO - Plugins initialised: csv_manager_plugin
2017-01-15 15:31:46,625 - INFO - Application documents initialised
2017-01-15 15:31:46,625 - INFO - Plugins initialised: GwPluginsInfo
2017-01-15 15:31:46,626 - INFO - Application threads initialised
2017-01-15 15:31:46,626 - INFO - Plugins initialised: CsvWatcherPlugin
2017-01-15 15:31:46,627 - INFO - Plugins activated: csv_manager_plugin, GwPluginsInfo, CsvWatcherPlugin
2017-01-15 15:31:46,627 - INFO - test.csv has new row: {'phone': '123-4561', 'name': 'Daniel', 'city': 'Munich'}
2017-01-15 15:31:46,627 - INFO - test.csv has new row: {'phone': '111/2222', 'name': 'Maria', 'city': 'Cologne'}
2017-01-15 15:31:46,628 - INFO - test.csv has new row: {'phone': '0445-4545-45451', 'name': 'Richard', 'city': 'Paris'}
2017-01-15 15:31:46,628 - INFO - test.csv has new row: {'phone': '777-8888', 'name': 'Annabel', 'city': 'London'}
2017-01-15 15:32:11,652 - INFO - test.csv has new row: {'phone': '1111-222222', 'name': 'Annabel', 'city': 'London'}
2017-01-15 15:32:11,652 - INFO - test.csv is missing row: {'phone': '777-8888', 'name': 'Annabel', 'city': 'London'}
Nice, we are now able to add as many plugins to our signal as we like and no unwanted log messages are printed anymore.
On the next chapter Using configs and documents we will use our new signal to store the changes inside a groundwork document, which can be used like other groundwork documents for any kind of helpful documentation.