1.6. First own pattern

Patterns are used to provide technical, reusable resources to plugins. They are not related to the content of data, which they handle. And they provide no final interfaces to the users. Instead they inject an API into plugins, which inherit from them.

A pattern gets automatically loaded when the first plugin, which inherits from it, gets initiated and activated. And it gets deactivated when the last plugin, which inherit from it, gets deactivated.

For our code example this means that the csv watcher would be the perfect pattern. There are several use cases, where a specific plugin could need the help of a csv watcher pattern. For instance to monitor measurement results or to periodically import data from a tool which exports it’s data to a file.

1.6.1. Preparation

Before we can start, let’s create the needed python packages and files.

Create a folder csv_watcher_pattern inside the patterns folder of the csv_manager package. In the new folder create two additional files: __init__.py and csv_watcher_pattern.py.:

>>> tree
CSV-Manager
├── csv_manager
│   ├── applications
│   │   ├── configuration.py
│   │   ├── csv_manager_app.py
│   │   └── __init__.py
│   ├── patterns
│   │   ├── csv_watcher_pattern
│   │   │   ├── csv_watcher_pattern.py
│   │   │   └── __init__.py
│   │   └── __init__.py
│   └ ...
└ ...

1.6.2. Code the pattern base

Open the csv_watcher_pattern.py file and add the following:

 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
from groundwork.patterns import GwThreadsPattern


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()


class CsvWatcherPlugin:
    """
    Proxy to the CsvWatcherApplication class.
    Responsible for adding the correct plugin context,
    if a csv_watcher functions gets called inside a plugin
    """
    def __init__(self, plugin):
        self._plugin = plugin
        self._app = plugin.app
        self._watchers = {}

    def register(self, csv_file, interval):
        self._app.csv_watcher.register(csv_file, interval, self._plugin)

    def unregister(self, csv_file):
        self._app.csv_watcher.unregister(csv_file, self._plugin)

    def get(self, csv_file=None):
        self._app.csv_watcher.get(csv_file, self._plugin)


class CsvWatcherApplication:
    """
    Main class for handling watchers of csv files.
    """
    def __init__(self):
        self._watchers = {}

    def register(self, csv_file, interval, plugin):
        pass

    def unregister(self, csv_file, plugin):
        pass

    def get(self, csv_file=None, plugin=None):
        pass

Wow, that’s a lot of code for one single step. But to make our goal clear, we want to be able to register new watchers inside a plugin by using something like self.csv_watcher.register(). We also want to be able to unregister or get watchers, which were registered by the current plugin only.

Or in others words: A plugin shall only have access to stuff, which it has registered by itself (called plugin context). That’s already the case for groundwork patterns like commands, documents, signals and more.

On the other hand, there must be a way to get access to all watchers. This should be possible by accessing the data via the application context. Or more detailed: by accessing it via the app object, which is available in each plugin via self.app. In our case we would get all watchers by using self.app.csv_watchers.get()

The above code has already implemented this. We have one class CsvWatcherPlugin [19], which cares about functions called in the plugin context. And another class CsvWatcherApplication [39], that handles the calls on application level.

To save some code, functions of CsvWatcherPlugin are mostly calling the related function of CsvWatcherApplication. But they add the current plugin as parameter, so that the results are filtered for the given plugin.

Example: Lets imagine we are working inside a plugin called “AwesomePlugin” and we register a new watcher via self.csv_watcher.register("test.csv") [29]. This will call the register function of CsvWatcherPlugin and inside this function app.csv_watcher.register("test.csv", plugin="AwesomePlugin") [46] is called, which is part of CsvWatcherApplication.

The pattern is responsible for creating these contexts and handling the correct initialisation [10-16].

As you can see, we will have 3 new functions inside a plugin which inherits from CsvWatcherPattern: register(), unregister and get()

register() and unregister are used to create new watchers or to delete existing ones. get() provides acccess to existing watchers.

1.6.3. Moving the csv logic

Let’s move our csv logic from the plugin into our pattern. We use a new class CsvWatcher for all functions and information round about a csv watcher. So it is very easy for a plugin to request a single watcher and get all needed stuff.

 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
 class CsvWatcher:
     def __init__(self, csv_file, interval, description, plugin):
         self.csv_file = csv_file
         self.interval = interval
         self.plugin = plugin
         self.description = description

         # Register thread
         self.csv_thread = plugin.threads.register("csv_thread_%s" % csv_file, self._csv_watcher_thread,
                                               "Thread for monitoring a csv file in background")

         self.running = self.csv_thread.running

     def run(self):
         self.csv_thread.run()

     def _csv_watcher_thread(self, plugin):
         csv_file = plugin.csv_file
         interval = plugin.csv_interval
         plugin.log.debug("watcher command called with csv_path: %s and interval: %s" % (csv_file, interval))

         # Check if the given csv_file really exists
         if not os.path.exists(csv_file):
             self.log.error("CSV file %s does not exist" % csv_file)

         # Start with an "empty csv file"
         old_content = []

         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")

                 # Check if there are new/changed rows
                 for row in new_content:
                     if row not in old_content:
                         plugin.log.debug("New row: %s" % row)

                 # Check if old rows are missing
                 for row in old_content:
                     if row not in new_content:
                         plugin.log.debug("Missing row: %s" % row)

                 # Store the current csv file content as old content
                 old_content = new_content

             csv_file_object.close()

             # Wait x seconds
             time.sleep(interval)


 class CsvWatcherExistsException(BaseException):
     pass

As you can see, we only have made some minor changes.

We added a description parameter, so that a plugin can describe the use case during registration of a watcher.

As we are no longer inside a plugin, self.log will not work. Instead we have to use plugin.log to create log messages.

We have also added an exception, which gets called, if the given csv file does not exist.

Now let’s take a look into our cleaned plugin class:

 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
 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])

     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 deactivate(self):
         pass

It has become much short and we have removed some import statements.

Instead of GwThreadPattern our plugin class inherits from our new CsvWatcherPattern [6]. Because CsvWatcherPattern inherits itself from GwThreadPattern the thread function calls are still be available in our plugin class (but we do not need them anymore).

Inside the csv_watcher_command() function we are using our new register() and run() functions to handle our csv watcher [40+43].

1.6.4. All changed files

Before we start testing, let’s take a look into all changed files.

1.6.4.1. csv_watcher_pattern.py

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import time
import csv

from groundwork.patterns import GwThreadsPattern
from groundwork.util import gw_get


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()


class CsvWatcherPlugin:
    """
    Proxy to the CsvWatcherApplication class.
    Responsible for adding the correct plugin context, if a csv_watcher functions gets called inside a plugin
    """

    def __init__(self, plugin):
        self._plugin = plugin
        self._app = plugin.app
        self._watchers = {}

    def register(self, csv_file, interval, description):
        return self._app.csv_watcher.register(csv_file, interval, description, self._plugin)

    def unregister(self, csv_file):
        self._app.csv_watcher.unregister(csv_file, self._plugin)

    def get(self, csv_file=None):
        self._app.csv_watcher.get(csv_file, self._plugin)


class CsvWatcherApplication:
    """
    Main class for handling watchers of csv files.
    """

    def __init__(self):
        self._watchers = {}

    def register(self, csv_file, interval, description, plugin):
        if csv_file in self._watchers.keys():
            raise CsvWatcherExistsException("csv file %s is already registered by %s." %
                                            (csv_file, self._watchers[csv_file].plugin.name))

        self._watchers[csv_file] = CsvWatcher(csv_file, interval, description, plugin)
        return self._watchers[csv_file]

    def unregister(self, csv_file, plugin):
        pass

    def get(self, csv_file=None, plugin=None):
        gw_get(self._watchers, csv_file, plugin)


class CsvWatcher:
    def __init__(self, csv_file, interval, description, plugin):
        self.csv_file = csv_file
        self.interval = interval
        self.plugin = plugin
        self.description = description

        # Register thread
        self.csv_thread = plugin.threads.register("csv_thread_%s" % csv_file, self._csv_watcher_thread,
                                                  "Thread for monitoring a csv file in background")

        self.running = self.csv_thread.running

    def run(self):
        self.csv_thread.run()

    def _csv_watcher_thread(self, plugin):
        csv_file = self.csv_file
        interval = self.interval
        plugin.log.info("watcher command called with csv_path: %s and interval: %s" % (csv_file, interval))

        # Check if the given csv_file really exists
        if not os.path.exists(csv_file):
            plugin.log.error("CSV file %s does not exist" % csv_file)

        # Start with an "empty csv file"
        old_content = []

        while True:
            csv_file_object = open(csv_file)
            new_content = list(csv.DictReader(csv_file_object))

            if new_content != old_content:
                plugin.log.info("Change detected")

                # Check if there are new/changed rows
                for row in new_content:
                    if row not in old_content:
                        plugin.log.info("New row: %s" % row)

                # Check if old rows are missing
                for row in old_content:
                    if row not in new_content:
                        plugin.log.info("Missing row: %s" % row)

                # Store the current csv file content as old content
                old_content = new_content

            csv_file_object.close()

            # Wait x seconds
            time.sleep(interval)


class CsvWatcherExistsException(BaseException):
    pass

1.6.4.2. csv_watcher_plugin.py

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
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])

    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 deactivate(self):
        pass

1.6.4.3. patterns/__init__.py

We have also imported our pattern inside the __init__.py file of the pattern-package. With this little change we are able to shorten the import-statement of our pattern to from csv_manager.patterns import csv_manager_pattern.

1
2
3
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from .csv_watcher_pattern.csv_watcher_pattern import CsvWatcherPattern

1.6.5. Testing

Let’s call our csv_manager and see if it works:

>>> csv_manager csv_watch -i 5 test.csv
2017-01-15 10:02:18,232 - INFO  - Application signals initialised
2017-01-15 10:02:18,417 - INFO  - Application commands initialised
2017-01-15 10:02:18,418 - INFO  - Plugins initialised: csv_manager_plugin
2017-01-15 10:02:18,418 - INFO  - Application documents initialised
2017-01-15 10:02:18,418 - INFO  - Plugins initialised: GwPluginsInfo
2017-01-15 10:02:18,419 - INFO  - Application threads initialised
2017-01-15 10:02:18,419 - INFO  - Plugins initialised: CsvWatcherPlugin
2017-01-15 10:02:18,420 - INFO  - Plugins activated: csv_manager_plugin, GwPluginsInfo, CsvWatcherPlugin
2017-01-15 10:02:18,420 - INFO  - watcher command called with csv_path: test.csv and interval: 5
2017-01-15 10:02:18,420 - INFO  - Change detected
2017-01-15 10:02:18,420 - INFO  - New row: {'city': 'Munich', 'phone': '123-456', 'name': 'Daniel'}
2017-01-15 10:02:18,420 - INFO  - New row: {'city': 'Cologne', 'phone': '111/222', 'name': 'Maria'}
2017-01-15 10:02:18,420 - INFO  - New row: {'city': 'Paris', 'phone': '0445-4545-45451', 'name': 'Richard'}
2017-01-15 10:02:18,421 - INFO  - New row: {'city': 'London', 'phone': '777-888888', 'name': 'Anabel'}

That’s it. You have created an awesome pattern, which every plugin can simply use to register watchers for csv files. And you have updated your plugin to use this pattern.

Sadly there is one ugly problem. Our pattern creates log messages and our plugin gets not informed about changes.

In most cases patterns should not create output to the user. This is the job for the plugin, because only the plugin is specific enough to really know what kind of information the user needs and how this should be presented (log, console, website, e-mail, …).

So in the next chapter Adding signals and receivers we will modify our pattern to send signals instead of writing log messages. And we create a receiver in our plugin, so that the plugin can present the changes to the user.