class Baroque:
    """The Baroque event broker class.

    Note:
        When no configuration file is specified, the default configuration
        is loaded.

    Args:
        configfile (str, optional): Path to the configuration YML file.

    Raises:
        :obj:`baroque.exceptions.configuration.ConfigurationNotFoundError`: when the supplied filepath is not a regular file
        :obj:`baroque.exceptions.configuration.ConfigurationParseError`: when the supplied file cannot be parsed

    """

    def __init__(self, configfile=None):
        self.config = configreader.read_config_or_default(configfile)
        self.reactreg = registries.ReactorsRegistry()
        self.topicsreg = registries.TopicsRegistry()
        self.evtcounter = counters.EventCounter()
        self._load_preregistered_eventtypes()
        self._load_persistence_backend()

    @property
    def configuration(self):
        """:obj:`dict`: the configuration for this broker instance"""
        return self.config

    @property
    def version(self):
        """:obj:`tuple`: version tuple for this broker instance"""
        return BAROQUE_VERSION

    @property
    def reactors(self):
        """:obj:`baroque.datastructures.registries.ReactorRegistry`:
        registry of reactors subscribed to this broker instance"""
        return self.reactreg

    @property
    def eventtypes(self):
        """:obj:`baroque.datastructures.registries.EventTypesRegistry`:
        registry of event types registered on this broker instance"""
        return self.reactreg.get_event_types_registry()

    @property
    def events(self):
        """:obj:`baroque.datastructures.counters.EventCounter`:
        counter of events published on this broker instance so far"""
        return self.evtcounter

    @property
    def topics(self):
        """:obj:`baroque.datastructures.registries.TopicsRegistry`:
        registry of topics registered on this broker instance"""
        return self.topicsreg

    def reset(self):
        """Resets the reactors register and the published events counter of
        this broker instance.

        """
        self.reactreg = registries.ReactorsRegistry()
        self.evtcounter = counters.EventCounter()

    # -------- reactor-related methods --------
    def on(self, eventtype):
        """Registers an event type on the broker.

        Args:
            eventtype (:obj:`baroque.entities.eventtype.EventType`): the event type to be registered

        Returns:
            :obj:`baroque.datastructures.bags.ReactorsBag`

        """
        return self.reactreg.get_or_create_bag(eventtype)

    def on_any_event_run(self, reactor):
        """Subscribes a reactor on the broker to be run upon any event firing.

        Args:
            reactor (:obj:`baroque.entities.reactor.Reactor`): the reactor to be subscribed

        Returns:
            :obj:`baroque.datastructures.reactor.Reactor`

        """
        return self.reactreg.get_jolly_bag().run(reactor)

    # -------- event-related methods --------
    def publish(self, event):
        """Publishes an event on the broker.

        Note:
            This is a template-method

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be published

        """
        assert isinstance(event, Event)
        if self.config['events']['validate_schema']:
            self._validate_event_schema(event)
        self._count_event(event)
        self._execute_reactors(event)
        self._update_event_status(event)
        if self.config['events']['persist']:
            self._persist_event(event)

    # "private" methods
    def _count_event(self, event):
        """Increments the events counter of the broker

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be counted

        """
        self.evtcounter.increment_counting(event)

    def _execute_reactors(self, event):
        """Execute all reactors that subscribed to the event type of the input
        event.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event
        Raises
            :obj:`baroque.exceptions.eventtypes.UnregisteredEventTypeError`: when the type of the event is not registered on the broker

        """
        propagate = self.config['reactors']['propagate_exceptions']
        ignore = self.config['eventtypes']['ignore_unregistered']
        if not ignore:
            if event.type not in self.eventtypes:
                raise UnregisteredEventTypeError(event.type)
        for r in self.reactreg.get_jolly_bag():
            try:
                r.react_conditionally(event)
            except:
                if propagate:
                    raise
        for r in self.reactreg.get_bag(event.type):
            try:
                r.react_conditionally(event)
            except:
                if propagate:
                    raise

    def _update_event_status(self, event):
        """Turns the event status to published.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event whose status needs to be set to published.

        """
        event.set_published()

    def _persist_event(self, event):
        """Persists the event information to a datastore.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be persisted

        """
        self._persistance_backend.create(event)

    def _load_preregistered_eventtypes(self):
        """Register predefined event types (as per configuration) on this
        broker instance"""
        for path in self.config['eventtypes']['pre_registered']:
            class_ = importer.class_from_dotted_path(path)
            eventtype = class_()
            self.eventtypes.register(eventtype)

    def _load_persistence_backend(self):
        """Loads on this broker instance the persistence backend defined in
        configuration"""
        path = self.config['events']['persistence_backend']
        if path is not None:
            class_ = importer.class_from_dotted_path(path)
            self._persistance_backend = class_()

    def _validate_event_schema(self, event):
        """Validate the JSON Schema of the input event.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event whose JSON schema needs to be validated.
        Raises
            :obj:`baroque.exceptions.eventtypes.InvalidEventSchema`:
            when the type of the event is not registered on the broker

        """
        if not EventType.validate(event, event.type):
            raise InvalidEventSchemaError(event)

    # -------- event-related methods --------
    def publish_on_topic(self, event, topic):
        """Publishes an event on a specified topic registered on the broker.

        Note:
            This is a template-method

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be published on the topic
            topic (:obj:`baroque.entities.topic.Topic`): the topic on which the event must be published

        Raises
            :obj:`baroque.exceptions.topics.UnregisteredTopicError`:
            when trying to publish events on a topic that is not registered
            on the broker

        """
        if topic not in self.topics:
            raise UnregisteredTopicError(topic)
        self._count_event(event)
        self.topics.publish_on_topic(event, topic)
        self._update_event_status(event)
        if self.config['events']['persist']:
            self._persist_event(event)

    def on_topic_run(self, topic, reactor):
        """Attaches a reactor on a topic registered on the broker.

        Args:
            topic (:obj:`baroque.entities.topic.Topic`): the topic to which the reactor must be attached
            reactor (:obj:`baroque.entities.reactor.Reactor`): the reactor to be attached to the topic
        """
        if topic not in self.topics:
            if self.config['topics']['register_on_binding']:
                self.topics.register(topic)
            else:
                raise UnregisteredTopicError(topic)
        self.topics.on_topic_run(topic, reactor)

    # -------- aliases --------

    def fire(self, event):
        """Alias for `baroque.baroque.Baroque.publish()` method"""
        self.publish(event)

    def on_any_event_trigger(self, reactor):
        """Alias for `baroque.baroque.Baroque.on_any_event_run()` method"""
        return self.on_any_event_run(reactor)

    # -------- magic methods -------
    def __repr__(self):
        return '<{}.{}'.format(__name__, self.__class__.__name__) class EventType:
    """The type of an event, describing its semantics and content.

    Args:
        jsonschema (str): the JSON schema string describing the content of the
         events having this type
        description (str, optional): the description of this event type
        owner (str, optional): the owner of this event type

    """

    def __init__(self, jsonschema, description=None, owner=None):
        assert jsonschema is not None
        assert isinstance(jsonschema, str)
        self.jsonschema = jsonschema
        self.owner = owner
        self.description = description
        self.tags = set()

    @staticmethod
    def validate(evt, evttype):
        """Validates the content of an event against the JSON schema of its type.

        Args:
            evt (:obj:`baroque.entities.event.Event`): the event to be validated
            evttype (:obj:`baroque.entities.eventtype.EventType`): the type of
            the event that needs to be validated

        Returns:
            ``True`` if validation is OK, ``False`` otherwise

        """
        try:
            assert evt.type == evttype
            check(loads(evt.json()), loads(evttype.jsonschema))
            return True
        except (AssertionError, ValidationError):
            return False

    def json(self):
        """Dumps this object to a JSON string.

        Returns:
            str

        """
        data = dict(jsonschema=self.jsonschema, owner=self.owner,
                    description=self.description, tags=list(self.tags))
        return dumps(data)

    def md5(self):
        """Returns the MD5 hash of this object.

        Returns:
            str

        """
        m = hashlib.md5()
        m.update(self.json().encode('utf-8'))
        return m.hexdigest()

    def __repr__(self):
        return '<{}.{} - description: {}>'.format(
            __name__,
            self.__class__.__name__,
            self.description or 'None'
        ) DEFAULT_CONFIG = {
    'eventtypes': {
        'ignore_unregistered': True,
        'pre_registered': [
            'baroque.defaults.eventtypes.GenericEventType',
            'baroque.defaults.eventtypes.StateTransitionEventType',
            'baroque.defaults.eventtypes.DataOperationEventType',
            'baroque.defaults.eventtypes.MetricEventType'
        ]
    },
    'events': {
        'validate_schema': True,
        'persist': False,
        'persistence_backend': 'baroque.persistence.inmemory.DictBackend'
    },
    'reactors': {
        'propagate_exceptions': True
    },
    'topics': {
        'register_on_binding': True
    }
} class UnregisteredEventTypeError(Exception):
    """Raised when attempting to publish on the broker events of an
    unregistered type"""
    pass class InvalidEventSchemaError(Exception):
    """Raised when the event validation against its eventtype JSON
    schema fails"""
    pass class UnregisteredTopicError(Exception):
    """Raised when attempting to publish events on a topic that is not registered
    on the broker"""
    pass class Reactor:
    """An action to be executed whenever some type of events are published,
    with an optional condition to be satisfied satisfied. If a condition is
    set, this is checked out and if the outcome is ``True`` then the action
    is executed. If no condition is set, then the action is always executed.

    Args:
        reaction (function): the action to be executed
        condition (function, optional): the boolean condition to be satisfied

    Raises:
        `AssertionError`: when the supplied reaction is `None` or is not a callable, or (when supplied) when the condition is not a callable

    """
    def __init__(self, reaction, condition=None):
        assert reaction is not None
        assert callable(reaction)
        self.reaction_function = reaction
        if condition is not None:
            assert callable(condition)
        self.condition_function = condition
        self.last_reaction_timestamp = None
        self.id_last_event_reacted = None
        self.reactions_count = 0

    def react(self, event):
        """Execute the action of this reactor.

        Note:
            the condition of this reactor is out of the scope of this method
            (please see method :obj:``react_conditionally()``)

        Args:
            reaction (function): the action to be executed
            condition (function, optional): the boolean condition to be satisfied

        """
        self.reaction_function(event)
        self.reactions_count += 1
        self.last_reaction_timestamp = ts.utc_now()
        self.id_last_event_reacted = event.id

    def only_if(self, condition):
        """Sets the boolean condition for this reactor.

        Args:
            condition (function): the boolean condition to be satisfied

        """
        assert callable(condition)
        self.condition_function = condition

    def _condition_met(self, event):
        """Checks if the condition is satisfied when fed with an event.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be fed to
            the condition function

        Returns:
            ``True`` if no condition function is set or if the condition is
            satisfied, ``False`` otherwise

        """
        if self.condition_function is None:
            return True
        return self.condition_function(event)

    def react_conditionally(self, event):
        """First checks if the condition is satisfied, then based on the outcome
        executes the action.

        Args:
            event (:obj:`baroque.entities.event.Event`): the triggering event

        """
        if self._condition_met(event):
            self.react(event)

    def count_reactions(self):
        """Gives the number of times this reactor's action has been executed

        Returns:
            int

        """
        return self.reactions_count

    def last_reacted_on(self):
        """Gives the timestamp of the last time when the reactor's action has
        been executed

        Returns:
            str if reactor reacted at least once, ``None`` otherwise

        """
        if self.last_reaction_timestamp is None:
            return None
        return ts.stringify(self.last_reaction_timestamp)

    def last_event_reacted(self):
        """Gives the ID of the last event this reactor reacted on

        Returns:
            str

        """
        return self.id_last_event_reacted

    def __repr__(self):
        return '<{}.{} - reaction function: {}> - ' \
               'condition function: {} - ' \
               'last reaction on: {}'.format(
                    __name__,
                    self.__class__.__name__,
                    self.reaction_function,
                    self.condition_function or 'None',
                    self.last_reacted_on() or 'Never'
                ) class Event:
    """An event that can be published.

    Args:
        eventtype (:obj:`baroque.entities.eventtype.EventType` instance or `type` object): the type of the event
        payload (dict, optional): the content of this event
        description (str, optional): the description of this event
        owner (str, optional): the owner of this event

    """
    def __init__(self, eventtype, payload=None, description=None, owner=None):
        assert eventtype is not None
        if type(eventtype) == type:
            eventtype = eventtype()
        assert isinstance(eventtype, EventType)
        if payload is not None:
            assert isinstance(payload, dict)
        self.id = str(uuid.uuid4())
        self.type = eventtype
        self.owner = owner
        self.status = EventStatus.UNPUBLISHED
        self.description = description
        self.payload = payload
        self.tags = set()
        self.timestamp = None
        self.touch()

    def set_published(self):
        """Sets the status of this event to published."""
        self.status = EventStatus.PUBLISHED

    def set_unpublished(self):
        """Sets the status of this event to unpublished."""
        self.status = EventStatus.UNPUBLISHED

    def touch(self):
        """Sets the current time as timestamp of this event"""
        self.timestamp = ts.utc_now()

    def json(self):
        """Dumps this object to a JSON string.

        Returns:
            str

        """
        data = dict(id=self.id, type=self.type.json(),
                    owner=self.owner, status=self.status,
                    description=self.description, payload=self.payload,
                    tags=list(self.tags), timestamp=ts.stringify(self.timestamp))
        return dumps(data)

    def md5(self):
        """Returns the MD5 hash of this object.

        Returns:
            str

        """
        m = hashlib.md5()
        m.update(self.json().encode('utf-8'))
        return m.hexdigest()

    def __repr__(self):
        return '<{}.{} - type: {} - id: {}>'.format(
            __name__,
            self.__class__.__name__,
            self.type.__class__.__name__,
            self.id
        ) class Topic:
    """A distribution channel where events of specific types can be published
    and can be seen by subscribers of the topic. Topic subscribers will attach
    a reactor to the topic, which will be fired whenever any event of the types
    that are supported by the topic is published on the topic itself.

    Args:
        name (str): the name of this topic
        eventtypes (collection): the :obj:`baroque.entities.eventtype.EventType` objects that characterize this topic
        description (str, optional): a description of this topic
        owner (str, optional): the owner of this topic
        tags (set, optional): the `set` of tags that describe this topic

    Raises:
        `AssertionError`: name or tags are `None` or have a wrong type

    """

    def __init__(self, name, eventtypes, description=None, owner=None,
                 tags=None):
        assert name is not None
        assert isinstance(name, str)
        self.name = name
        assert eventtypes is not None
        if len(eventtypes) != 0:
            self.types = EventTypesBag(eventtypes)
        else:
            self.types = EventTypesBag()
        self.description = description
        self.owner = owner
        if tags is not None:
            assert not isinstance(tags, str)
            assert isinstance(tags, collections.Iterable)
            self.tags = set(tags)
        else:
            self.tags = set()
        self.id = str(uuid.uuid4())
        self.timestamp = None
        self.touch()

    @property
    def eventtypes(self):
        """:obj:`baroque.datastructures.bags.EventTypesBag`: bag containing the
         event types of this topic"""
        return self.types

    def touch(self):
        """Sets the current time as timestamp of this topic"""
        self.timestamp = ts.utc_now()

    def json(self):
        """Dumps this object to a JSON string.

        Returns:
            str

        """
        data = dict(id=self.id, owner=self.owner, description=self.description,
                    eventtypes=[str(et) for et in self.types],
                    tags=list(self.tags), timestamp=ts.stringify(self.timestamp))
        return dumps(data)

    def md5(self):
        """Returns the MD5 hash of this object.

        Returns:
            str

        """
        m = hashlib.md5()
        m.update(self.json().encode('utf-8'))
        return m.hexdigest()

    def __repr__(self):
        return '<{}.{} - name: {} - owner: {} - eventtypes: {}>'.format(
            __name__,
            self.__class__.__name__,
            self.name,
            self.owner or 'None',
            ', '.join([str(et) for et in self.types]) or 'None'
        ) class GenericEventType(EventType):
    """Describes generic events with a free-form content.

    Args:
        owner (str, optional): ID of the owner of this event type.

    """
    def __init__(self, owner=None):
        EventType.__init__(self,
                           '''{
                             "$schema": "http://json-schema.org/draft-04/schema#"
                           }''',
                           description='Generic event',
                           owner=owner) class MetricEventType(EventType):
    """Describes events carrying metric data.
    Suitable i.e. to track values about measured physical quantities.
    The metric name and value are conveyed in the event payload.

    Args:
        owner (str, optional): ID of the owner of this event type.

    """
    def __init__(self, owner=None):
        EventType.__init__(
            self,
            '''
            {
              "$schema": "http://json-schema.org/draft-04/schema#",
              "type": "object",
              "properties": {
                "payload": {
                  "type": "object",
                  "properties": {
                    "metric": {
                      "type": ["string", "number"]
                    },
                    "value": {
                      "type": ["string", "number"]
                    },
                    "timestamp": {
                      "type": ["string", "number"]
                    },
                    "meta": {
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "metric",
                    "value",
                    "timestamp"
                  ]
                }
              },
              "required": [
                "payload"
              ]
            }
            ''',
            description='Metric events',
            owner=owner) class ReactorFactory:
    """A factory class that exposes methods to quickly create useful
    :obj:`baroque.entities.reactor.Reactor` instances"""

    @classmethod
    def stdout(cls):
        """Factory method returning a reactor that prints events to stdout.

        Returns:
            :obj:`baroque.entities.reactor.Reactor`

        """
        def action(event):
            print(event)
        return Reactor(action)

    @classmethod
    def call_function(cls, obj, function_name, *args, **kwargs):
        """Factory method returning a reactor that calls a method on an object.

        Args:
            obj (object): the target object
            function_name (function): the function to be invoked on the object

        Returns:
            :obj:`baroque.entities.reactor.Reactor`

        """
        def action(event):
            getattr(obj, function_name)(*args, **kwargs)
        return Reactor(action)

    @classmethod
    def log_event(cls, logger, loglevel):
        """Factory method returning a reactor that logs on a logger at a
        specified loglevel.

        Args:
            logger (:obj:`logging.Logger`): the logger object
            loglevel (int): the logging level

        Returns:
            :obj:`baroque.entities.reactor.Reactor`

        """
        def action(event):
            logger.log(loglevel, str(event))
        return Reactor(action)

    @classmethod
    def json_webhook(cls, url, payload, query_params=None, headers=None):
        """Factory method returning a reactor that POSTs arbitrary
        JSON data to a webhook, along with the specified HTTP headers.

        Args:
            url (str): the webhook URL
            payload (dict): payload data dict to be dumped to JSON and sent
            query_params (dict): dict of query parameters
            headers (dict): dict of headers

        Returns:
            A dict containing the response HTTP status code (int) and payload
            (str), ie: ``{'status': 200, 'payload': None}``

        """
        import requests

        def action(event):
            resp = requests.post(url, params=query_params or dict(),
                                 headers=headers or dict(), json=payload)
            return dict(status=resp.status_code, payload=resp.json())
        return Reactor(action) class PersistenceBackend:

    def create(self, event):
        """Persists an event.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be persisted

        """
        pass

    def read(self, event_id):
        """Loads an event.

        Args:
            event_id (str): the identifier of the event to be loaded

        Returns:
            :obj:`baroque.entities.event.Event`

        """
        pass

    def update(self, event):
        """Updates the event.

        Args:
            event (:obj:`baroque.entities.event.Event`): the event to be updated

        """
        pass

    def delete(self, event_id):
        """Deletes an event.

        Args:
            event_id (str): the identifier of the event to be deleted

        """
        pass
import pytest
from baroque import Baroque, EventType
from baroque.defaults.config import DEFAULT_CONFIG as cfg
from baroque.exceptions.eventtypes import UnregisteredEventTypeError, \
    InvalidEventSchemaError
from baroque.exceptions.topics import UnregisteredTopicError
from baroque.entities.reactor import Reactor
from baroque.entities.event import Event
from baroque.entities.topic import Topic
from baroque.defaults.eventtypes import GenericEventType, MetricEventType
from baroque.defaults.reactors import ReactorFactory
from baroque.persistence.backend import PersistenceBackend


class FakeEventType(EventType):
    def __init__(self, owner=None):
        EventType.__init__(self,
                           '{}',
                           description='fake',
                           owner=owner)


class FakePersistenceBackend(PersistenceBackend):
    def __init__(self):
        self._events = list()

    def create(self, event):
        self._events.append(event)

    def __len__(self):
        return len(self._events)


def test_exception_bubbling():
    # do propagate
    cfg['reactors']['propagate_exceptions'] = True

    brq = Baroque()