extensions.py 7.11 KB
Newer Older
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
1
2
"""
"""
3
import os
4
5
import fnmatch
import inspect
6
import sys
7
import imp
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
8
import logging
9
10
import gevent
import json
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
11
12

logger = logging.getLogger(__name__)
13

14
from .plugins import SenpyPlugin, SentimentPlugin, EmotionPlugin
15
from .models import Error
16

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
17
from .blueprints import nif_blueprint
18
from git import Repo, InvalidGitRepositoryError
19
from functools import partial
20

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
21

22
class Senpy(object):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
23
    """ Default Senpy extension for Flask """
24
25
26
27
28

    def __init__(self, app=None, plugin_folder="plugins"):
        self.app = app
        base_folder = os.path.join(os.path.dirname(__file__), "plugins")

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
29
30
31
32
33
        self._search_folders = set()
        self._outdated = True

        for folder in (base_folder, plugin_folder):
            self.add_folder(folder)
34
35
36
37

        if app is not None:
            self.init_app(app)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
38
39
40
    def init_app(self, app):
        """ Initialise a flask app to add plugins to its context """
        """
41
42
        Note: I'm not particularly fond of adding self.app and app.senpy, but
        I can't think of a better way to do it.
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
43
        """
44
45
46
47
48
49
50
51
52
        app.senpy = self
        # Use the newstyle teardown_appcontext if it's available,
        # otherwise fall back to the request context
        if hasattr(app, 'teardown_appcontext'):
            app.teardown_appcontext(self.teardown)
        else:
            app.teardown_request(self.teardown)
        app.register_blueprint(nif_blueprint)

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
53
    def add_folder(self, folder):
54
        logger.debug("Adding folder: %s", folder)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
55
56
57
58
59
        if os.path.isdir(folder):
            self._search_folders.add(folder)
            self._outdated = True
            return True
        else:
60
            logger.debug("Not a folder: %s", folder)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
61
62
            return False

63
    def analyse(self, **params):
64
        algo = None
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
65
        logger.debug("analysing with params: {}".format(params))
66
67
        if "algorithm" in params:
            algo = params["algorithm"]
68
69
        elif self.plugins:
            algo = self.default_plugin
70
71
72
73
        if algo in self.plugins:
            if self.plugins[algo].is_activated:
                plug = self.plugins[algo]
                resp = plug.analyse(**params)
74
                resp.analysis.append(plug)
75
                return resp
76
77
78
            else:
                logger.debug("Plugin not activated: {}".format(algo))
                return Error(status=400, message="The algorithm '{}' is not activated yet".format(algo))
79
        else:
80
            logger.debug("The algorithm '{}' is not valid\nValid algorithms: {}".format(algo, self.plugins.keys()))
81
            return Error(status=400, message="The algorithm '{}' is not valid".format(algo))
82
83
84

    @property
    def default_plugin(self):
85
        candidates = self.filter_plugins(is_activated=True)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
86
        if len(candidates) > 0:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
87
88
            candidate = candidates.keys()[0]
            logger.debug("Default: {}".format(candidate))
89
90
            return candidate
        else:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
91
            return None
92

93
    def parameters(self, algo):
94
95
        return getattr(self.plugins.get(algo or self.default_plugin), "params", {})

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
125
126
127
    def activate_all(self, sync=False):
        ps = []
        for plug in self.plugins.keys():
            ps.append(self.activate_plugin(plug, sync=sync))
        return ps

    def deactivate_all(self, sync=False):
        ps = []
        for plug in self.plugins.keys():
            ps.append(self.deactivate_plugin(plug, sync=sync))
        return ps

    def _set_active_plugin(self, plugin_name, active=True, *args, **kwargs):
        self.plugins[plugin_name].is_activated = active

    def activate_plugin(self, plugin_name, sync=False):
        plugin = self.plugins[plugin_name]
        th = gevent.spawn(plugin.activate)
        th.link_value(partial(self._set_active_plugin, plugin_name, True))
        if sync:
            th.join()
        else:
            return th

    def deactivate_plugin(self, plugin_name, sync=False):
        plugin = self.plugins[plugin_name]
        th = gevent.spawn(plugin.deactivate)
        th.link_value(partial(self._set_active_plugin, plugin_name, False))
        if sync:
            th.join()
        else:
            return th
128
129

    def reload_plugin(self, plugin):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
130
        logger.debug("Reloading {}".format(plugin))
131
132
133
134
        plug = self.plugins[plugin]
        nplug = self._load_plugin(plug.module, plug.path)
        del self.plugins[plugin]
        self.plugins[nplug.name] = nplug
135

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
136
    @staticmethod
137
138
139
140
141
142
143
144
145
146
    def _load_plugin(root, filename):
        logger.debug("Loading plugin: {}".format(filename))
        fpath = os.path.join(root, filename)
        with open(fpath,'r') as f:
            info = json.load(f)
        logger.debug("Info: {}".format(info))
        sys.path.append(root)
        module = info["module"]
        name = info["name"]
        (fp, pathname, desc) = imp.find_module(module, [root,])
147
        try:
148
149
150
151
152
153
154
155
156
157
158
159
            tmp = imp.load_module(module, fp, pathname, desc)
            sys.path.remove(root)
            candidate = None
            for _, obj in inspect.getmembers(tmp):
                if inspect.isclass(obj) and inspect.getmodule(obj) == tmp:
                    logger.debug("Found plugin class: {}@{}".format(obj, inspect.getmodule(obj)))
                    candidate = obj
                    break
            if not candidate:
                logger.debug("No valid plugin for: {}".format(filename))
                return
            module = candidate(info=info)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
160
            try:
161
                repo_path = root
162
                module._repo = Repo(repo_path)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
163
            except InvalidGitRepositoryError:
164
                module._repo = None
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
165
        except Exception as ex:
166
167
168
            logger.debug("Exception importing {}: {}".format(filename, ex))
            return None, None
        return name, module
169
170
171

    def _load_plugins(self):
        plugins = {}
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
172
        for search_folder in self._search_folders:
173
174
175
            for root, dirnames, filenames in os.walk(search_folder):
                for filename in fnmatch.filter(filenames, '*.senpy'):
                    name, plugin = self._load_plugin(root, filename)
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
176
                    if plugin:
177
                        plugins[name] = plugin
178

J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
179
        self._outdated = False
180
181
182
183
184
185
186
        return plugins

    def teardown(self, exception):
        pass

    @property
    def plugins(self):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
187
        """ Return the plugins registered for a given application.  """
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
188
189
190
        if not hasattr(self, 'senpy_plugins') or self._outdated:
            self.senpy_plugins = self._load_plugins()
        return self.senpy_plugins
191

192
    def filter_plugins(self, **kwargs):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
193
        """ Filter plugins by different criteria """
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
194

195
        def matches(plug):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
196
197
198
199
            res = all(getattr(plug, k, None) == v for (k, v) in kwargs.items())
            logger.debug("matching {} with {}: {}".format(plug.name,
                                                          kwargs,
                                                          res))
200
            return res
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
201

202
203
204
        if not kwargs:
            return self.plugins
        else:
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
205
            return {n: p for n, p in self.plugins.items() if matches(p)}
206
207

    def sentiment_plugins(self):
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
208
        """ Return only the sentiment plugins """
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
209
        return {p: plugin for p, plugin in self.plugins.items() if
J. Fernando Sánchez's avatar
J. Fernando Sánchez committed
210
                isinstance(plugin, SentimentPlugin)}