Passed
Push — master ( 9e42ed...b2d36d )
by Jaisen
02:09
created

elodie.plugins.plugins   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 133
dl 0
loc 221
rs 9.36
c 0
b 0
f 0
wmc 38

16 Methods

Rating   Name   Duplication   Size   Complexity  
A PluginDb.set() 0 9 3
A PluginDb.__init__() 0 14 4
A PluginBase.display() 0 4 1
A Plugins.__init__() 0 4 1
A PluginDb.delete() 0 9 3
A PluginBase.log() 0 4 1
A PluginDb.get() 0 8 3
A PluginBase.batch() 0 2 1
A Plugins.run_all_after() 0 23 4
A PluginDb.get_all() 0 4 2
A Plugins.run_batch() 0 20 4
A Plugins.run_all_before() 0 22 4
A PluginBase.after() 0 2 1
A PluginBase.__init__() 0 4 1
A PluginBase.before() 0 2 1
A Plugins.load() 0 26 4
1
"""
2
Plugin object.
3
4
.. moduleauthor:: Jaisen Mathai <[email protected]>
5
"""
6
from __future__ import print_function
7
from builtins import object
8
9
import io
10
11
from json import dumps, loads
12
from importlib import import_module
13
from os.path import dirname, dirname, isdir, isfile
14
from os import mkdir
15
from sys import exc_info
16
from traceback import format_exc
17
18
from elodie.compatability import _bytes
19
from elodie.config import load_config_for_plugin, load_plugin_config
20
from elodie.constants import application_directory
21
from elodie import log
22
23
24
class ElodiePluginError(Exception):
25
    """Exception which can be thrown by plugins to return failures.
26
    """
27
    pass
28
29
30
class PluginBase(object):
31
    """Base class which all plugins should inherit from.
32
       Defines stubs for all methods and exposes logging and database functionality
33
    """
34
    __name__ = 'PluginBase'
35
36
    def __init__(self):
37
        # Loads the config for the plugin from config.ini
38
        self.config_for_plugin = load_config_for_plugin(self.__name__)
39
        self.db = PluginDb(self.__name__)
40
41
    def after(self, file_path, destination_folder, final_file_path, metadata):
42
        pass
43
44
    def batch(self):
45
        pass
46
47
    def before(self, file_path, destination_folder):
48
        pass
49
50
    def log(self, msg):
51
        # Writes an info log not shown unless being run in --debug mode.
52
        log.info(dumps(
53
            {self.__name__: msg}
54
        ))
55
56
    def display(self, msg):
57
        # Writes a log for all modes and will be displayed.
58
        log.all(dumps(
59
            {self.__name__: msg}
60
        ))
61
62
class PluginDb(object):
63
    """A database module which provides a simple key/value database.
64
       The database is a JSON file located at %application_directory%/plugins/%pluginname.lower()%.json
65
    """
66
    def __init__(self, plugin_name):
67
        self.db_file = '{}/plugins/{}.json'.format(
68
            application_directory,
69
            plugin_name.lower()
70
        )
71
72
        # If the plugin db directory does not exist, create it
73
        if(not isdir(dirname(self.db_file))):
74
            mkdir(dirname(self.db_file))
75
76
        # If the db file does not exist we initialize it
77
        if(not isfile(self.db_file)):
78
            with io.open(self.db_file, 'wb') as f:
79
                f.write(_bytes(dumps({})))
80
81
82
    def get(self, key):
83
        with io.open(self.db_file, 'r') as f:
84
            db = loads(f.read())
85
86
        if(key not in db):
87
            return None
88
89
        return db[key]
90
91
    def set(self, key, value):
92
        with io.open(self.db_file, 'r') as f:
93
            data = f.read()
94
            db = loads(data)
95
96
        db[key] = value
97
        new_content = dumps(db, ensure_ascii=False).encode('utf8')
98
        with io.open(self.db_file, 'wb') as f:
99
            f.write(new_content)
100
101
    def get_all(self):
102
        with io.open(self.db_file, 'r') as f:
103
            db = loads(f.read())
104
        return db
105
106
    def delete(self, key):
107
        with io.open(self.db_file, 'r') as f:
108
            db = loads(f.read())
109
110
        # delete key without throwing an exception
111
        db.pop(key, None)
112
        new_content = dumps(db, ensure_ascii=False).encode('utf8')
113
        with io.open(self.db_file, 'wb') as f:
114
            f.write(new_content)
115
116
117
class Plugins(object):
118
    """Plugin object which manages all interaction with plugins.
119
       Exposes methods to load plugins and execute their methods.
120
    """
121
122
    def __init__(self):
123
        self.plugins = []
124
        self.classes = {}
125
        self.loaded = False
126
127
    def load(self):
128
        """Load plugins from config file.
129
        """
130
        # If plugins have been loaded then return
131
        if self.loaded == True:
132
            return
133
134
        plugin_list = load_plugin_config()
135
        for plugin in plugin_list:
136
            plugin_lower = plugin.lower()
137
            try:
138
                # We attempt to do the following.
139
                #  1. Load the module of the plugin.
140
                #  2. Instantiate an object of the plugin's class.
141
                #  3. Add the plugin to the list of plugins.
142
                #  
143
                #  #3 should only happen if #2 doesn't throw an error
144
                this_module = import_module('elodie.plugins.{}.{}'.format(plugin_lower, plugin_lower))
145
                self.classes[plugin] = getattr(this_module, plugin)()
146
                # We only append to self.plugins if we're able to load the class
147
                self.plugins.append(plugin)
148
            except:
149
                log.error('An error occurred initiating plugin {}'.format(plugin))
150
                log.error(format_exc())
151
152
        self.loaded = True
153
154
    def run_all_after(self, file_path, destination_folder, final_file_path, metadata):
155
        """Process `before` methods of each plugin that was loaded.
156
        """
157
        self.load()
158
        pass_status = True
159
        for cls in self.classes:
160
            this_method = getattr(self.classes[cls], 'after')
161
            # We try to call the plugin's `before()` method.
162
            # If the method explicitly raises an ElodiePluginError we'll fail the import
163
            #  by setting pass_status to False.
164
            # If any other error occurs we log the message and proceed as usual.
165
            # By default, plugins don't change behavior.
166
            try:
167
                this_method(file_path, destination_folder, final_file_path, metadata)
168
                log.info('Called after() for {}'.format(cls))
169
            except ElodiePluginError as err:
170
                log.warn('Plugin {} raised an exception in run_all_before: {}'.format(cls, err))
171
                log.error(format_exc())
172
                log.error('false')
173
                pass_status = False
174
            except:
175
                log.error(format_exc())
176
        return pass_status
177
178
    def run_batch(self):
179
        self.load()
180
        pass_status = True
181
        for cls in self.classes:
182
            this_method = getattr(self.classes[cls], 'batch')
183
            # We try to call the plugin's `before()` method.
184
            # If the method explicitly raises an ElodiePluginError we'll fail the import
185
            #  by setting pass_status to False.
186
            # If any other error occurs we log the message and proceed as usual.
187
            # By default, plugins don't change behavior.
188
            try:
189
                this_method()
190
                log.info('Called batch() for {}'.format(cls))
191
            except ElodiePluginError as err:
192
                log.warn('Plugin {} raised an exception in run_batch: {}'.format(cls, err))
193
                log.error(format_exc())
194
                pass_status = False
195
            except:
196
                log.error(format_exc())
197
        return pass_status
198
199
    def run_all_before(self, file_path, destination_folder):
200
        """Process `before` methods of each plugin that was loaded.
201
        """
202
        self.load()
203
        pass_status = True
204
        for cls in self.classes:
205
            this_method = getattr(self.classes[cls], 'before')
206
            # We try to call the plugin's `before()` method.
207
            # If the method explicitly raises an ElodiePluginError we'll fail the import
208
            #  by setting pass_status to False.
209
            # If any other error occurs we log the message and proceed as usual.
210
            # By default, plugins don't change behavior.
211
            try:
212
                this_method(file_path, destination_folder)
213
                log.info('Called before() for {}'.format(cls))
214
            except ElodiePluginError as err:
215
                log.warn('Plugin {} raised an exception in run_all_after: {}'.format(cls, err))
216
                log.error(format_exc())
217
                pass_status = False
218
            except:
219
                log.error(format_exc())
220
        return pass_status
221