mirror of
				https://github.com/avatao-content/baseimage-tutorial-framework
				synced 2025-11-04 15:32:56 +00:00 
			
		
		
		
	Adjust the whole framework to event handler dependency inversion
This commit is contained in:
		@@ -1 +1,3 @@
 | 
			
		||||
from .event_handler_base import EventHandlerBase
 | 
			
		||||
from .event_handler_factory import EventHandlerFactoryBase
 | 
			
		||||
from .event_handler import EventHandler
 | 
			
		||||
from .fsm_aware_event_handler import FSMAwareEventHandler
 | 
			
		||||
 
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
import logging
 | 
			
		||||
from abc import ABC, abstractmethod
 | 
			
		||||
from typing import Iterable
 | 
			
		||||
 | 
			
		||||
from tfw.networking import Scope
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventHandlerBase(ABC):
 | 
			
		||||
    """
 | 
			
		||||
    Abstract base class for all Python based EventHandlers. Useful implementation template
 | 
			
		||||
    for other languages.
 | 
			
		||||
 | 
			
		||||
    Derived classes must implement the handle_event() method
 | 
			
		||||
    """
 | 
			
		||||
    _instances = set()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, key, scope=Scope.ZMQ):
 | 
			
		||||
        type(self)._instances.add(self)
 | 
			
		||||
        self.server_connector = self._build_server_connector()
 | 
			
		||||
        self.scope = scope
 | 
			
		||||
        self.keys = []
 | 
			
		||||
        if isinstance(key, str):
 | 
			
		||||
            self.keys.append(key)
 | 
			
		||||
        elif isinstance(key, Iterable):
 | 
			
		||||
            self.keys = list(key)
 | 
			
		||||
 | 
			
		||||
        self.subscribe(*self.keys)
 | 
			
		||||
        self.server_connector.register_callback(self.event_handler_callback)
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
    def _build_server_connector(self):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def subscribe(self, *keys):
 | 
			
		||||
        """
 | 
			
		||||
        Subscribe this EventHandler to receive events for given keys.
 | 
			
		||||
        Note that you can subscribe to the same key several times in which
 | 
			
		||||
        case you will need to unsubscribe multiple times in order to stop
 | 
			
		||||
        receiving events.
 | 
			
		||||
 | 
			
		||||
        :param keys: list of keys to subscribe to
 | 
			
		||||
        """
 | 
			
		||||
        for key in keys:
 | 
			
		||||
            self.server_connector.subscribe(key)
 | 
			
		||||
            self.keys.append(key)
 | 
			
		||||
 | 
			
		||||
    def event_handler_callback(self, message):
 | 
			
		||||
        """
 | 
			
		||||
        Callback that is invoked when receiving a message.
 | 
			
		||||
        Dispatches messages to handler methods and sends
 | 
			
		||||
        a response back in case the handler returned something.
 | 
			
		||||
        This is subscribed in __init__().
 | 
			
		||||
        """
 | 
			
		||||
        if self.check_key(message):
 | 
			
		||||
            self.dispatch_handling(message)
 | 
			
		||||
 | 
			
		||||
    def check_key(self, message):
 | 
			
		||||
        """
 | 
			
		||||
        Checks whether the message is intended for this
 | 
			
		||||
        EventHandler.
 | 
			
		||||
 | 
			
		||||
        This is necessary because ZMQ handles PUB - SUB
 | 
			
		||||
        connetions with pattern matching (e.g. someone
 | 
			
		||||
        subscribed to 'fsm' will receive 'fsm_update'
 | 
			
		||||
        messages as well.
 | 
			
		||||
        """
 | 
			
		||||
        if '' in self.keys:
 | 
			
		||||
            return True
 | 
			
		||||
        return message['key'] in self.keys
 | 
			
		||||
 | 
			
		||||
    def dispatch_handling(self, message):
 | 
			
		||||
        """
 | 
			
		||||
        Used to dispatch messages to their specific handlers.
 | 
			
		||||
 | 
			
		||||
        :param message: the message received
 | 
			
		||||
        :returns: the message to send back
 | 
			
		||||
        """
 | 
			
		||||
        self.handle_event(message)
 | 
			
		||||
 | 
			
		||||
    def handle_event(self, message):
 | 
			
		||||
        """
 | 
			
		||||
        Abstract method that implements the handling of messages.
 | 
			
		||||
 | 
			
		||||
        :param message: the message received
 | 
			
		||||
        :returns: the message to send back
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
    def send_message(self, message):
 | 
			
		||||
        self.server_connector.send_message(message, self.scope)
 | 
			
		||||
 | 
			
		||||
    def unsubscribe(self, *keys):
 | 
			
		||||
        """
 | 
			
		||||
        Unsubscribe this eventhandler from the given keys.
 | 
			
		||||
 | 
			
		||||
        :param keys: list of keys to unsubscribe from
 | 
			
		||||
        """
 | 
			
		||||
        for key in keys:
 | 
			
		||||
            self.server_connector.unsubscribe(key)
 | 
			
		||||
            self.keys.remove(key)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def stop_all_instances(cls):
 | 
			
		||||
        for instance in cls._instances:
 | 
			
		||||
            instance.stop()
 | 
			
		||||
 | 
			
		||||
    def stop(self):
 | 
			
		||||
        self.server_connector.close()
 | 
			
		||||
        self.cleanup()
 | 
			
		||||
 | 
			
		||||
    def cleanup(self):
 | 
			
		||||
        """
 | 
			
		||||
        Perform cleanup actions such as releasing database
 | 
			
		||||
        connections and stuff like that.
 | 
			
		||||
        """
 | 
			
		||||
@@ -24,13 +24,21 @@ class EventHandlerBuilder:
 | 
			
		||||
        self._event_handler_type = event_handler_type
 | 
			
		||||
 | 
			
		||||
    def build(self, server_connector):
 | 
			
		||||
        server_connector.subscribe(*self._analyzer.keys)
 | 
			
		||||
        event_handler = self._event_handler_type(server_connector)
 | 
			
		||||
        server_connector.subscribe(*self._try_get_keys(event_handler))
 | 
			
		||||
        event_handler.handle_event = self._analyzer.handle_event
 | 
			
		||||
        with suppress(AttributeError):
 | 
			
		||||
            event_handler.cleanup = self._analyzer.cleanup
 | 
			
		||||
        return event_handler
 | 
			
		||||
 | 
			
		||||
    def _try_get_keys(self, event_handler):
 | 
			
		||||
        try:
 | 
			
		||||
            return self._analyzer.keys
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            with suppress(AttributeError):
 | 
			
		||||
                return event_handler.keys
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventHandlerAnalyzer:
 | 
			
		||||
    def __init__(self, event_handler, supplied_keys):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								lib/tfw/event_handlers/fsm_aware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/tfw/event_handlers/fsm_aware.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from tfw.crypto import KeyManager, verify_message
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FSMAware:
 | 
			
		||||
    keys = ['fsm_update']
 | 
			
		||||
    """
 | 
			
		||||
    Base class for stuff that has to be aware of the framework FSM.
 | 
			
		||||
    This is done by processing 'fsm_update' messages.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.fsm_state = None
 | 
			
		||||
        self.fsm_in_accepted_state = False
 | 
			
		||||
        self.fsm_event_log = []
 | 
			
		||||
        self._auth_key = KeyManager().auth_key
 | 
			
		||||
 | 
			
		||||
    def process_message(self, message):
 | 
			
		||||
        if message['key'] == 'fsm_update':
 | 
			
		||||
            if verify_message(self._auth_key, message):
 | 
			
		||||
                self._handle_fsm_update(message)
 | 
			
		||||
 | 
			
		||||
    def _handle_fsm_update(self, message):
 | 
			
		||||
        try:
 | 
			
		||||
            new_state = message['current_state']
 | 
			
		||||
            if self.fsm_state != new_state:
 | 
			
		||||
                self.handle_fsm_step(message)
 | 
			
		||||
            self.fsm_state = new_state
 | 
			
		||||
            self.fsm_in_accepted_state = message['in_accepted_state']
 | 
			
		||||
            self.fsm_event_log.append(message)
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            LOG.error('Invalid fsm_update message received!')
 | 
			
		||||
 | 
			
		||||
    def handle_fsm_step(self, message):
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										19
									
								
								lib/tfw/event_handlers/fsm_aware_event_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/tfw/event_handlers/fsm_aware_event_handler.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
from .event_handler import EventHandler
 | 
			
		||||
from .fsm_aware import FSMAware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FSMAwareEventHandler(EventHandler, FSMAware):
 | 
			
		||||
    # pylint: disable=abstract-method
 | 
			
		||||
    """
 | 
			
		||||
    Abstract base class for EventHandlers which automatically
 | 
			
		||||
    keep track of the state of the TFW FSM.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, server_connector):
 | 
			
		||||
        EventHandler.__init__(self, server_connector)
 | 
			
		||||
        FSMAware.__init__(self)
 | 
			
		||||
 | 
			
		||||
    def _event_callback(self, message):
 | 
			
		||||
        self.process_message(message)
 | 
			
		||||
 | 
			
		||||
    def handle_fsm_step(self, message):
 | 
			
		||||
        self.handle_event(message, self.server_connector)
 | 
			
		||||
@@ -174,13 +174,17 @@ def test_build_raises_if_no_key(test_keys):
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        MockEventHandlerFactory().build(eh)
 | 
			
		||||
 | 
			
		||||
    def test_handle_event(*_):
 | 
			
		||||
    def handle_event(*_):
 | 
			
		||||
        pass
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        MockEventHandlerFactory().build(test_handle_event)
 | 
			
		||||
        MockEventHandlerFactory().build(handle_event)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        MockEventHandlerFactory().build(lambda msg, sc: None)
 | 
			
		||||
 | 
			
		||||
    WithKeysEventHandler = EventHandler
 | 
			
		||||
    WithKeysEventHandler.keys = test_keys
 | 
			
		||||
    MockEventHandlerFactory().build(eh, event_handler_type=WithKeysEventHandler)
 | 
			
		||||
 | 
			
		||||
    eh.keys = test_keys
 | 
			
		||||
    MockEventHandlerFactory().build(eh)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user