Compare commits

...
5 changed files with 90 additions and 33 deletions
@@ -1,13 +1,13 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw import BroadcastingEventHandler
from tfw import EventHandlerBase
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class FSMManagingEventHandler(BroadcastingEventHandler):
class FSMManagingEventHandler(EventHandlerBase):
def __init__(self, key, fsm_type):
super().__init__(key)
self.fsm = fsm_type()
@@ -22,28 +22,29 @@ class FSMManagingEventHandler(BroadcastingEventHandler):
try:
data = message['data']
message['data'] = self.command_handlers[data['command']](data)
self.server_connector.broadcast(self._fsm_updater.generate_fsm_update())
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def handle_trigger(self, data):
self.fsm.step(data['value'])
return self.with_fsm_update(data)
def with_fsm_update(self, data):
return {
**data,
**self._fsm_updater.get_fsm_state_and_transitions()
}
return data
def handle_update(self, data):
return self.with_fsm_update(data)
return data
class FSMUpdater:
def __init__(self, fsm):
self.fsm = fsm
def generate_fsm_update(self):
return {
'key': 'fsm_update',
'data': self.get_fsm_state_and_transitions()
}
def get_fsm_state_and_transitions(self):
state = self.fsm.state
valid_transitions = [
+16 -8
View File
@@ -31,10 +31,25 @@ class EventHandlerBase(ABC):
a response back in case the handler returned something.
This is subscribed in __init__().
"""
if not self.check_key(message):
return
response = self.dispatch_handling(message)
if response:
self.server_connector.send(response)
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.
"""
return self.key == message['key']
def dispatch_handling(self, message):
"""
Used to dispatch messages to their specific handlers.
@@ -131,16 +146,9 @@ class BroadcastingEventHandler(EventHandlerBase, ABC):
response = self.dispatch_handling(message)
if response:
self.own_message_hashes.append(self.hash_message(response))
self.server_connector.send(self.make_broadcast_message(response))
self.server_connector.broadcast(response)
@staticmethod
def hash_message(message):
message_bytes = dumps(message, sort_keys=True).encode()
return md5(message_bytes).hexdigest()
@staticmethod
def make_broadcast_message(message):
return {
'key': 'broadcast',
'data': message
}
@@ -40,26 +40,40 @@ class ServerUplinkConnector(ZMQConnectorBase):
def send_to_eventhandler(self, message):
"""
Send a message to an event handler.
Send a message to an event handler through the TFW server.
This envelopes the desired message in the 'data' field of the message to
TFWServer, which will mirror it to event handlers.
:param message: JSON message you want to send
:param message['key']: key of event handler you want to address
"""
nested_message = {'key': message['key'], 'data': message.pop('data')}
message['key'] = 'mirror'
message['data'] = nested_message
self.send(message)
self.send({
'key': 'mirror',
'data': message
})
def send(self, message):
"""
Send a message to the TFW server
Send a message to the frontend through the TFW server.
:param message: JSON message you want to send
"""
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
def broadcast(self, message):
"""
Broadast a message through the TFW server.
This envelopes the desired message in the 'data' field of the message to
TFWServer, which will broadast it.
:param message: JSON message you want to send
"""
self.send({
'key': 'broadcast',
'data': message
})
class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector):
pass
+41 -8
View File
@@ -1,25 +1,21 @@
from subprocess import Popen, run
from functools import partial
from functools import partial, singledispatch
from contextlib import suppress
import yaml
import jinja2
from transitions import State
from tfw import FSMBase
class YamlFSM(FSMBase):
def __init__(self, config_file):
self.config = self.parse_config(config_file)
def __init__(self, config_file, jinja2_variables=None):
self.config = ConfigParser(config_file, jinja2_variables).config
self.setup_states()
super().__init__() # FSMBase.__init__() requires states
self.setup_transitions()
@staticmethod
def parse_config(config_file):
with open(config_file, 'r') as ifile:
return yaml.safe_load(ifile)
def setup_states(self):
self.for_config_states_and_transitions_do(self.wrap_callbacks_with_subprocess_call)
self.states = [State(**state) for state in self.config['states']]
@@ -62,3 +58,40 @@ def run_command_async(command, event):
def command_statuscode_is_zero(command):
return run(command, shell=True).returncode == 0
class ConfigParser:
def __init__(self, config_file, jinja2_variables):
self.read_variables = singledispatch(self.read_variables)
self.read_variables.register(dict, self._read_variables_dict)
self.read_variables.register(str, self._read_variables_str)
self.config = self.parse_config(config_file, jinja2_variables)
def parse_config(self, config_file, jinja2_variables):
config_string = self.read_file(config_file)
if jinja2_variables is not None:
variables = self.read_variables(jinja2_variables)
template = jinja2.Environment(loader=jinja2.BaseLoader).from_string(config_string)
config_string = template.render(**variables)
return yaml.safe_load(config_string)
@staticmethod
def read_file(filename):
with open(filename, 'r') as ifile:
return ifile.read()
@staticmethod
def read_variables(variables):
raise TypeError(f'Invalid variables type {type(variables)}')
@staticmethod
def _read_variables_str(variables):
if isinstance(variables, str):
with open(variables, 'r') as ifile:
return yaml.safe_load(ifile)
@staticmethod
def _read_variables_dict(variables):
return variables
+1
View File
@@ -4,3 +4,4 @@ transitions==0.6.4
terminado==0.8.1
watchdog==0.8.3
PyYAML==3.12
Jinja2==2.10