Source code for isomer.component

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

# Isomer - The distributed application framework
# ==============================================
# Copyright (C) 2011-2019 Heiko 'riot' Weinen <riot@c-base.org> and others.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""

Configurable Component
======================

Contains
--------

Systemwide configurable component definition. Stores configuration either in
database or as json files.
Enables editing of configuration through frontend.

See also
--------

Provisions


"""
import inspect
import traceback
from sys import exc_info
from uuid import uuid4

from circuits import Component
from circuits.web.controllers import Controller
from copy import deepcopy
from jsonschema import ValidationError
from pymongo.errors import ServerSelectionTimeoutError
from random import randint
from formal import model_factory

from isomer.events.system import isomer_ui_event, authorized_event, anonymous_event
from isomer.logger import isolog, warn, critical, error, verbose
from isomer.schemata.component import ComponentBaseConfigSchema


# from pprint import pprint


[docs]def handler(*names, **kwargs): """Creates an Event Handler This decorator can be applied to methods of classes derived from :class:`circuits.core.components.BaseComponent`. It marks the method as a handler for the events passed as arguments to the ``@handler`` decorator. The events are specified by their name. The decorated method's arguments must match the arguments passed to the :class:`circuits.core.events.Event` on creation. Optionally, the method may have an additional first argument named *event*. If declared, the event object that caused the handler to be invoked is assigned to it. By default, the handler is invoked by the component's root :class:`~.manager.Manager` for events that are propagated on the channel determined by the BaseComponent's *channel* attribute. This may be overridden by specifying a different channel as a keyword parameter of the decorator (``channel=...``). Keyword argument ``priority`` influences the order in which handlers for a specific event are invoked. The higher the priority, the earlier the handler is executed. If you want to override a handler defined in a base class of your component, you must specify ``override=True``, else your method becomes an additional handler for the event. **Return value** Normally, the results returned by the handlers for an event are simply collected in the :class:`circuits.core.events.Event`'s :attr:`value` attribute. As a special case, a handler may return a :class:`types.GeneratorType`. This signals to the dispatcher that the handler isn't ready to deliver a result yet. Rather, it has interrupted it's execution with a ``yield None`` statement, thus preserving its current execution state. The dispatcher saves the returned generator object as a task. All tasks are reexamined (i.e. their :meth:`next()` method is invoked) when the pending events have been executed. This feature avoids an unnecessarily complicated chaining of event handlers. Imagine a handler A that needs the results from firing an event E in order to complete. Then without this feature, the final action of A would be to fire event E, and another handler for an event ``SuccessE`` would be required to complete handler A's operation, now having the result from invoking E available (actually it's even a bit more complicated). Using this "suspend" feature, the handler simply fires event E and then yields ``None`` until e.g. it finds a result in E's :attr:`value` attribute. For the simplest scenario, there even is a utility method :meth:`circuits.core.manager.Manager.callEvent` that combines firing and waiting. """ def wrapper(f): if names and isinstance(names[0], bool) and not names[0]: f.handler = False return f if ( len(names) > 0 and inspect.isclass(names[0]) and issubclass(names[0], isomer_ui_event) ): f.names = (str(names[0].realname()),) else: f.names = names f.handler = True f.priority = kwargs.get("priority", 0) f.channel = kwargs.get("channel", None) f.override = kwargs.get("override", False) args = inspect.getfullargspec(f).args if args and args[0] == "self": del args[0] f.event = getattr(f, "event", bool(args and args[0] == "event")) return f return wrapper
[docs]class LoggingMeta(object): """Baseclass for all components that adds naming and logging functionality""" names = []
[docs] def __init__(self, uniquename=None, *args, **kwargs): """Check for configuration issues and instantiate a component""" self.uniquename = "" if uniquename: if uniquename not in self.names: self.uniquename = uniquename self.names.append(uniquename) else: isolog( "Unique component added twice: ", uniquename, lvl=critical, emitter="CORE", ) else: while True: uniquename = "%s%s" % (self.__class__.__name__, randint(0, 32768)) if uniquename not in self.names: self.uniquename = uniquename self.names.append(uniquename) break
[docs] def log(self, *args, **kwargs): """Log a statement from this component""" func = inspect.currentframe().f_back.f_code # Dump the message + the name of this function to the log. if "exc" in kwargs and kwargs["exc"] is True: exc_type, exc_obj, exc_tb = exc_info() line_no = exc_tb.tb_lineno # print('EXCEPTION DATA:', line_no, exc_type, exc_obj, exc_tb) args += (traceback.extract_tb(exc_tb),) else: line_no = func.co_firstlineno sourceloc = "[%.10s@%s:%i]" % (func.co_name, func.co_filename, line_no) isolog(sourceloc=sourceloc, emitter=self.uniquename, *args, **kwargs)
[docs]class ConfigurableMeta(LoggingMeta): """Meta class to add configuration capabilities to circuits objects""" configprops = {} configform = []
[docs] def __init__(self, *args, no_db=False, **kwargs): """Check for configuration issues and instantiate a component""" super(ConfigurableMeta, self).__init__(*args, **kwargs) if no_db is True: self.no_db = True self.log("Not using database!") return else: self.no_db = False self.configschema = deepcopy(ComponentBaseConfigSchema) self.configschema["schema"]["properties"].update(self.configprops) if len(self.configform) > 0: self.configschema["form"] += self.configform else: self.configschema["form"] = ["*"] # self.log("[UNIQUECOMPONENT] Config Schema: ", self.configschema, # lvl=critical) # pprint(self.configschema) # self.configschema['name'] = self.uniquename # self.configschema['id'] = "#" + self.uniquename # schemastore[self.uniquename] = {'schema': self.configschema, # 'form': self.configform} self.componentmodel = model_factory(self.configschema["schema"]) # self.log("Component model: ", lvl=critical) # pprint(self.componentmodel._schema) self._read_config() if not self.config: self.log("Creating initial default configuration.") try: self._set_config() self._write_config() except ValidationError as e: self.log("Error during configuration reading: ", e, type(e), exc=True) if self.config.active is False: self.log("Component disabled.", lvl=warn)
# raise ComponentDisabled
[docs] def register(self, *args): """Register a configurable component in the configuration schema store""" if self.config.active: super(ConfigurableMeta, self).register(*args) if self.no_db: return from isomer.schemastore import configschemastore # self.log('ADDING SCHEMA:') # pprint(self.configschema) configschemastore[self.name] = self.configschema
[docs] def unregister(self): """Removes the unique name from the systems unique name list""" self.names.remove(self.uniquename) super(ConfigurableMeta, self).unregister()
def _read_config(self): """Read this component's configuration from the database""" try: self.config = self.componentmodel.find_one({"name": self.uniquename}) except ServerSelectionTimeoutError: # pragma: no cover self.log( "No database access! Check if mongodb is running " "correctly.", lvl=critical, ) if self.config: self.log("Configuration read.", lvl=verbose) else: self.log("No configuration found.", lvl=warn) # self.log(self.config) def _write_config(self): """Write this component's configuration back to the database""" if not self.config: self.log("Unable to write non existing configuration", lvl=error) return self.config.save() self.log("Configuration stored.") def _set_config(self, config=None): """Set this component's initial configuration""" if not config: config = {} try: # pprint(self.configschema) self.config = self.componentmodel(config) # self.log("Config schema:", lvl=critical) # pprint(self.config.__dict__) # pprint(self.config._fields) try: name = self.config.name self.log("Name set to: ", name, lvl=verbose) except (AttributeError, KeyError): # pragma: no cover self.log("Has no name.", lvl=verbose) try: self.config.name = self.uniquename except (AttributeError, KeyError) as e: # pragma: no cover self.log( "Cannot set component name for configuration: ", e, type(e), self.name, exc=True, lvl=critical, ) try: uuid = self.config.uuid self.log("UUID set to: ", uuid, lvl=verbose) except (AttributeError, KeyError): self.log("Has no UUID", lvl=verbose) self.config.uuid = str(uuid4()) try: notes = self.config.notes self.log("Notes set to: ", notes, lvl=verbose) except (AttributeError, KeyError): self.log("Has no notes, trying docstring", lvl=verbose) notes = self.__doc__ if notes is None: notes = "No notes." else: notes = notes.lstrip().rstrip() self.log(notes) self.config.notes = notes try: componentclass = self.config.componentclass self.log("Componentclass set to: ", componentclass, lvl=verbose) except (AttributeError, KeyError): self.log("Has no component class", lvl=verbose) self.config.componentclass = self.name except ValidationError as e: self.log( "Not setting invalid component configuration: ", e, type(e), exc=True, lvl=error, ) # self.log("Fields:", self.config._fields, lvl=verbose)
[docs] @handler("reload_configuration") def reload_configuration(self, event): """Event triggered configuration reload""" if event.target == self.uniquename: self.log("Reloading configuration") self._read_config()
[docs]class ComponentDisabled(Exception): pass
[docs]class LoggingComponent(LoggingMeta, Component): """Logging capable component for simple Isomer components"""
[docs] def __init__(self, uniquename=None, *args, **kwargs): LoggingMeta.__init__(self, uniquename) Component.__init__(self, *args, **kwargs)
[docs]class ConfigurableController(ConfigurableMeta, Controller): """Configurable controller for direct web access"""
[docs] def __init__(self, uniquename=None, *args, **kwargs): ConfigurableMeta.__init__(self, uniquename, **kwargs) Controller.__init__(self, *args, **kwargs)
[docs]class ConfigurableComponent(ConfigurableMeta, Component): """Configurable component for default Isomer modules"""
[docs] def __init__(self, uniquename=None, *args, **kwargs): ConfigurableMeta.__init__(self, uniquename) Component.__init__(self, *args, **kwargs)
[docs]class ExampleComponent(ConfigurableComponent): """Exemplary component to demonstrate basic component usage""" configprops = { "setting": { "type": "string", "title": "Some Setting", "description": "Some string setting.", "default": "Yay", } }
[docs] def __init__(self, *args, **kwargs): """Show how the component initialization works and test this by adding a log statement.""" super(ExampleComponent, self).__init__("EXAMPLE", *args, **kwargs) self.log("Example component started")
# self.log(self.config)