Source code for isomer.component

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

# Isomer - The distributed application framework
# ==============================================
# Copyright (C) 2011-2020 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 os
import traceback
from copy import deepcopy
from random import randint
from sys import exc_info
from uuid import uuid4

from circuits import Component
from circuits.web.controllers import Controller
from formal import model_factory

# TODO: Part of the event-clean up efforts.
#
# noinspection PyUnresolvedReferences
from isomer.events.system import isomer_ui_event, authorized_event, anonymous_event
from isomer.events.client import send
from isomer.logger import isolog, warn, critical, error, verbose
from isomer.schemata.component import ComponentBaseConfigSchema
from isomer.misc import nested_map_update
from jsonschema import ValidationError
from pymongo.errors import ServerSelectionTimeoutError


# 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 BaseMeta(object): """Isomer Base Component Class""" context = None
[docs]class LoggingMeta(BaseMeta): """Base class for all components that adds naming and logging functionality""" names: list = []
[docs] def __init__(self, uniquename=None, *args, **kwargs): """Check for configuration issues and instantiate a component""" def pick_unique_name(): 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 self.uniquename = "" if uniquename is not None: if uniquename not in self.names: self.uniquename = uniquename self.names.append(uniquename) else: isolog( "Unique component added twice: ", uniquename, lvl=critical, emitter="CORE", ) pick_unique_name() else: pick_unique_name()
[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: dict = {} configform: dict = []
[docs] def __init__(self, uniquename, no_db=False, *args, **kwargs): """Check for configuration issues and instantiate a component""" LoggingMeta.__init__(self, uniquename, *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) environment_identifier = 'ISOMER_COMPONENT_' + self.uniquename overrides = [key for key, item in os.environ.items() if key.startswith(environment_identifier)] if len(overrides) > 0: self.log('Environment overrides found:', overrides) for item in overrides: path = item.lstrip(environment_identifier).lower().split("_") nested_map_update(self.config._fields, os.environ[item], path) self.config.save() 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, *args): """Removes the unique name from the systems unique name list""" super(ConfigurableMeta, self).unregister(*args) self.names.remove(self.uniquename)
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, *args, **kwargs): Component.__init__(self, *args, **kwargs) LoggingMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
[docs]class ConfigurableController(ConfigurableMeta, Controller): """Configurable controller for direct web access"""
[docs] def __init__(self, uniquename, *args, **kwargs): Controller.__init__(self, *args, **kwargs) ConfigurableMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
[docs]class ConfigurableComponent(ConfigurableMeta, Component): """Configurable component for default Isomer modules"""
[docs] def __init__(self, uniquename, *args, **kwargs): Component.__init__(self, *args, **kwargs) ConfigurableMeta.__init__(self, uniquename=uniquename, *args, **kwargs)
# TODO: Move to its own meta somehow def _respond(self, event, data): self.log(event.source(), event.realname(), event.channels[0], pretty=True) response = {"component": event.source(), "action": event.action, "data": data} self.fireEvent(send(event.client.uuid, response), event.channels[0])
[docs]class FrontendMeta(LoggingMeta): """Meta component for frontend-only modules There is nothing to configure here. """
[docs] def register(self, *_): """Mock command, does not do anything except log invocation""" self.log("Frontend meta component loaded:", self.uniquename)
[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)