Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06472ad19 | ||
|
|
17b24377dd | ||
|
|
558e660268 | ||
|
|
381cd2b11e | ||
|
|
9f6ac6d27e |
@@ -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 = [
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ transitions==0.6.4
|
||||
terminado==0.8.1
|
||||
watchdog==0.8.3
|
||||
PyYAML==3.12
|
||||
Jinja2==2.10
|
||||
|
||||
Reference in New Issue
Block a user