component.ConfigurableMeta._read_config()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nop 1
dl 0
loc 14
rs 9.9
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# Isomer - The distributed application framework
5
# ==============================================
6
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU Affero General Public License for more details.
17
#
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
"""
22
23
Configurable Component
24
======================
25
26
Contains
27
--------
28
29
Systemwide configurable component definition. Stores configuration either in
30
database or as json files.
31
Enables editing of configuration through frontend.
32
33
See also
34
--------
35
36
Provisions
37
38
39
"""
40
41
import inspect
42
import os
43
import traceback
44
from copy import deepcopy
45
from random import randint
46
from sys import exc_info
47
from uuid import uuid4
48
49
from circuits import Component
50
from circuits.web.controllers import Controller
51
from formal import model_factory
52
53
# TODO: Part of the event-clean up efforts.
54
#
55
# noinspection PyUnresolvedReferences
56
from isomer.events.system import isomer_ui_event, authorized_event, anonymous_event
57
from isomer.events.client import send
58
from isomer.logger import isolog, warn, critical, error, verbose
59
from isomer.schemata.component import ComponentBaseConfigSchema
60
from isomer.misc import nested_map_update
61
from jsonschema import ValidationError
62
from pymongo.errors import ServerSelectionTimeoutError
63
64
65
# from pprint import pprint
66
67
68
def handler(*names, **kwargs):
69
    """Creates an Event Handler
70
71
    This decorator can be applied to methods of classes derived from
72
    :class:`circuits.core.components.BaseComponent`. It marks the method as a
73
    handler for the events passed as arguments to the ``@handler`` decorator.
74
    The events are specified by their name.
75
76
    The decorated method's arguments must match the arguments passed to the
77
    :class:`circuits.core.events.Event` on creation. Optionally, the
78
    method may have an additional first argument named *event*. If declared,
79
    the event object that caused the handler to be invoked is assigned to it.
80
81
    By default, the handler is invoked by the component's root
82
    :class:`~.manager.Manager` for events that are propagated on the channel
83
    determined by the BaseComponent's *channel* attribute.
84
    This may be overridden by specifying a different channel as a keyword
85
    parameter of the decorator (``channel=...``).
86
87
    Keyword argument ``priority`` influences the order in which handlers
88
    for a specific event are invoked. The higher the priority, the earlier
89
    the handler is executed.
90
91
    If you want to override a handler defined in a base class of your
92
    component, you must specify ``override=True``, else your method becomes
93
    an additional handler for the event.
94
95
    **Return value**
96
97
    Normally, the results returned by the handlers for an event are simply
98
    collected in the :class:`circuits.core.events.Event`'s :attr:`value`
99
    attribute. As a special case, a handler may return a
100
    :class:`types.GeneratorType`. This signals to the dispatcher that the
101
    handler isn't ready to deliver a result yet.
102
    Rather, it has interrupted it's execution with a ``yield None``
103
    statement, thus preserving its current execution state.
104
105
    The dispatcher saves the returned generator object as a task.
106
    All tasks are reexamined (i.e. their :meth:`next()` method is invoked)
107
    when the pending events have been executed.
108
109
    This feature avoids an unnecessarily complicated chaining of event
110
    handlers. Imagine a handler A that needs the results from firing an
111
    event E in order to complete. Then without this feature, the final
112
    action of A would be to fire event E, and another handler for
113
    an event ``SuccessE`` would be required to complete handler A's
114
    operation, now having the result from invoking E available
115
    (actually it's even a bit more complicated).
116
117
    Using this "suspend" feature, the handler simply fires event E and
118
    then yields ``None`` until e.g. it finds a result in E's :attr:`value`
119
    attribute. For the simplest scenario, there even is a utility
120
    method :meth:`circuits.core.manager.Manager.callEvent` that combines
121
    firing and waiting.
122
    """
123
124
    def wrapper(f):
125
        if names and isinstance(names[0], bool) and not names[0]:
126
            f.handler = False
127
            return f
128
129
        if (
130
            len(names) > 0
131
            and inspect.isclass(names[0])
132
            and issubclass(names[0], isomer_ui_event)
133
        ):
134
            f.names = (str(names[0].realname()),)
135
        else:
136
            f.names = names
137
138
        f.handler = True
139
140
        f.priority = kwargs.get("priority", 0)
141
        f.channel = kwargs.get("channel", None)
142
        f.override = kwargs.get("override", False)
143
144
        args = inspect.getfullargspec(f).args
145
146
        if args and args[0] == "self":
147
            del args[0]
148
        f.event = getattr(f, "event", bool(args and args[0] == "event"))
149
150
        return f
151
152
    return wrapper
153
154
155
class BaseMeta(object):
156
    """Isomer Base Component Class"""
157
158
    context = None
159
160
161
class LoggingMeta(BaseMeta):
162
    """Base class for all components that adds naming and logging
163
    functionality"""
164
165
    names: list = []
166
167
    def __init__(self, uniquename=None, *args, **kwargs):
168
        """Check for configuration issues and instantiate a component"""
169
170
        def pick_unique_name():
171
            while True:
172
                uniquename = "%s%s" % (self.__class__.__name__, randint(0, 32768))
173
                if uniquename not in self.names:
174
                    self.uniquename = uniquename
175
                    self.names.append(uniquename)
176
177
                    break
178
179
        self.uniquename = ""
180
181
        if uniquename is not None:
182
            if uniquename not in self.names:
183
                self.uniquename = uniquename
184
                self.names.append(uniquename)
185
            else:
186
                isolog(
187
                    "Unique component added twice: ",
188
                    uniquename,
189
                    lvl=critical,
190
                    emitter="CORE",
191
                )
192
                pick_unique_name()
193
        else:
194
            pick_unique_name()
195
196
    def log(self, *args, **kwargs):
197
        """Log a statement from this component"""
198
199
        func = inspect.currentframe().f_back.f_code
200
        # Dump the message + the name of this function to the log.
201
202
        if "exc" in kwargs and kwargs["exc"] is True:
203
            exc_type, exc_obj, exc_tb = exc_info()
204
            line_no = exc_tb.tb_lineno
205
            # print('EXCEPTION DATA:', line_no, exc_type, exc_obj, exc_tb)
206
            args += (traceback.extract_tb(exc_tb),)
207
        else:
208
            line_no = func.co_firstlineno
209
210
        sourceloc = "[%.10s@%s:%i]" % (func.co_name, func.co_filename, line_no)
211
        isolog(sourceloc=sourceloc, emitter=self.uniquename, *args, **kwargs)
212
213
214
class ConfigurableMeta(LoggingMeta):
215
    """Meta class to add configuration capabilities to circuits objects"""
216
217
    configprops: dict = {}
218
    configform: dict = []
219
220
    def __init__(self, uniquename, no_db=False, *args, **kwargs):
221
        """Check for configuration issues and instantiate a component"""
222
223
        LoggingMeta.__init__(self, uniquename, *args, **kwargs)
224
225
        if no_db is True:
226
            self.no_db = True
227
            self.log("Not using database!")
228
            return
229
        else:
230
            self.no_db = False
231
232
        self.configschema = deepcopy(ComponentBaseConfigSchema)
233
234
        self.configschema["schema"]["properties"].update(self.configprops)
235
        if len(self.configform) > 0:
236
            self.configschema["form"] += self.configform
237
        else:
238
            self.configschema["form"] = ["*"]
239
240
        # self.log("[UNIQUECOMPONENT] Config Schema: ", self.configschema,
241
        #         lvl=critical)
242
        # pprint(self.configschema)
243
244
        # self.configschema['name'] = self.uniquename
245
        # self.configschema['id'] = "#" + self.uniquename
246
247
        # schemastore[self.uniquename] = {'schema': self.configschema,
248
        # 'form': self.configform}
249
250
        self.componentmodel = model_factory(self.configschema["schema"])
251
        # self.log("Component model: ", lvl=critical)
252
        # pprint(self.componentmodel._schema)
253
254
        self._read_config()
255
        if not self.config:
256
            self.log("Creating initial default configuration.")
257
            try:
258
                self._set_config()
259
                self._write_config()
260
            except ValidationError as e:
261
                self.log("Error during configuration reading: ", e, type(e), exc=True)
262
263
        environment_identifier = 'ISOMER_COMPONENT_' + self.uniquename
264
265
        overrides = [key for key, item in
266
                     os.environ.items() if key.startswith(environment_identifier)]
267
268
        if len(overrides) > 0:
269
            self.log('Environment overrides found:', overrides)
270
            for item in overrides:
271
                path = item.lstrip(environment_identifier).lower().split("_")
272
                nested_map_update(self.config._fields, os.environ[item], path)
273
274
            self.config.save()
275
276
        if self.config.active is False:
277
            self.log("Component disabled.", lvl=warn)
278
            # raise ComponentDisabled
279
280
    def register(self, *args):
281
        """Register a configurable component in the configuration schema
282
        store"""
283
284
        if self.config.active:
285
            super(ConfigurableMeta, self).register(*args)
286
287
        if self.no_db:
288
            return
289
290
        from isomer.schemastore import configschemastore
291
292
        # self.log('ADDING SCHEMA:')
293
        # pprint(self.configschema)
294
        configschemastore[self.name] = self.configschema
295
296
    def unregister(self, *args):
297
        """Removes the unique name from the systems unique name list"""
298
        super(ConfigurableMeta, self).unregister(*args)
299
300
        self.names.remove(self.uniquename)
301
302
    def _read_config(self):
303
        """Read this component's configuration from the database"""
304
305
        try:
306
            self.config = self.componentmodel.find_one({"name": self.uniquename})
307
        except ServerSelectionTimeoutError:  # pragma: no cover
308
            self.log(
309
                "No database access! Check if mongodb is running " "correctly.",
310
                lvl=critical,
311
            )
312
        if self.config:
313
            self.log("Configuration read.", lvl=verbose)
314
        else:
315
            self.log("No configuration found.", lvl=warn)
316
            # self.log(self.config)
317
318
    def _write_config(self):
319
        """Write this component's configuration back to the database"""
320
321
        if not self.config:
322
            self.log("Unable to write non existing configuration", lvl=error)
323
            return
324
325
        self.config.save()
326
        self.log("Configuration stored.")
327
328
    def _set_config(self, config=None):
329
        """Set this component's initial configuration"""
330
        if not config:
331
            config = {}
332
333
        try:
334
            # pprint(self.configschema)
335
            self.config = self.componentmodel(config)
336
            # self.log("Config schema:", lvl=critical)
337
            # pprint(self.config.__dict__)
338
339
            # pprint(self.config._fields)
340
341
            try:
342
                name = self.config.name
343
                self.log("Name set to: ", name, lvl=verbose)
344
            except (AttributeError, KeyError):  # pragma: no cover
345
                self.log("Has no name.", lvl=verbose)
346
347
            try:
348
                self.config.name = self.uniquename
349
            except (AttributeError, KeyError) as e:  # pragma: no cover
350
                self.log(
351
                    "Cannot set component name for configuration: ",
352
                    e,
353
                    type(e),
354
                    self.name,
355
                    exc=True,
356
                    lvl=critical,
357
                )
358
359
            try:
360
                uuid = self.config.uuid
361
                self.log("UUID set to: ", uuid, lvl=verbose)
362
            except (AttributeError, KeyError):
363
                self.log("Has no UUID", lvl=verbose)
364
                self.config.uuid = str(uuid4())
365
366
            try:
367
                notes = self.config.notes
368
                self.log("Notes set to: ", notes, lvl=verbose)
369
            except (AttributeError, KeyError):
370
                self.log("Has no notes, trying docstring", lvl=verbose)
371
372
                notes = self.__doc__
373
                if notes is None:
374
                    notes = "No notes."
375
                else:
376
                    notes = notes.lstrip().rstrip()
377
                    self.log(notes)
378
                self.config.notes = notes
379
380
            try:
381
                componentclass = self.config.componentclass
382
                self.log("Componentclass set to: ", componentclass, lvl=verbose)
383
            except (AttributeError, KeyError):
384
                self.log("Has no component class", lvl=verbose)
385
                self.config.componentclass = self.name
386
387
        except ValidationError as e:
388
            self.log(
389
                "Not setting invalid component configuration: ",
390
                e,
391
                type(e),
392
                exc=True,
393
                lvl=error,
394
            )
395
396
            # self.log("Fields:", self.config._fields, lvl=verbose)
397
398
    @handler("reload_configuration")
399
    def reload_configuration(self, event):
400
        """Event triggered configuration reload"""
401
402
        if event.target == self.uniquename:
403
            self.log("Reloading configuration")
404
            self._read_config()
405
406
407
class ComponentDisabled(Exception):
408
    pass
409
410
411
class LoggingComponent(LoggingMeta, Component):
412
    """Logging capable component for simple Isomer components"""
413
414
    def __init__(self, uniquename=None, *args, **kwargs):
415
        Component.__init__(self, *args, **kwargs)
416
        LoggingMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
417
418
419
class ConfigurableController(ConfigurableMeta, Controller):
420
    """Configurable controller for direct web access"""
421
422
    def __init__(self, uniquename=None, *args, **kwargs):
423
        Controller.__init__(self, *args, **kwargs)
424
        ConfigurableMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
425
426
427
class ConfigurableComponent(ConfigurableMeta, Component):
428
    """Configurable component for default Isomer modules"""
429
430
    def __init__(self, uniquename, *args, **kwargs):
431
        Component.__init__(self, *args, **kwargs)
432
        ConfigurableMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
433
434
    # TODO: Move to its own meta somehow
435
    def _respond(self, event, data):
436
        self.log(event.source(), event.realname(), event.channels[0], pretty=True)
437
        response = {"component": event.source(), "action": event.action, "data": data}
438
439
        self.fireEvent(send(event.client.uuid, response), event.channels[0])
440
441
442
class FrontendMeta(LoggingMeta):
443
    """Meta component for frontend-only modules
444
445
    There is nothing to configure here.
446
    """
447
448
    def register(self, *_):
449
        """Mock command, does not do anything except log invocation"""
450
451
        self.log("Frontend meta component loaded:", self.uniquename)
452
453
454
class ExampleComponent(ConfigurableComponent):
455
    """Exemplary component to demonstrate basic component usage"""
456
457
    configprops = {
458
        "setting": {
459
            "type": "string",
460
            "title": "Some Setting",
461
            "description": "Some string setting.",
462
            "default": "Yay",
463
        }
464
    }
465
466
    def __init__(self, *args, **kwargs):
467
        """Show how the component initialization works and test this by
468
        adding a log statement."""
469
        super(ExampleComponent, self).__init__("EXAMPLE", *args, **kwargs)
470
471
        self.log("Example component started")
472
        # self.log(self.config)
473