Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06472ad19 | ||
|
|
17b24377dd | ||
|
|
558e660268 | ||
|
|
381cd2b11e | ||
|
|
9f6ac6d27e |
@@ -1,13 +1,13 @@
|
|||||||
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||||
# All Rights Reserved. See LICENSE file for details.
|
# All Rights Reserved. See LICENSE file for details.
|
||||||
|
|
||||||
from tfw import BroadcastingEventHandler
|
from tfw import EventHandlerBase
|
||||||
from tfw.config.logs import logging
|
from tfw.config.logs import logging
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FSMManagingEventHandler(BroadcastingEventHandler):
|
class FSMManagingEventHandler(EventHandlerBase):
|
||||||
def __init__(self, key, fsm_type):
|
def __init__(self, key, fsm_type):
|
||||||
super().__init__(key)
|
super().__init__(key)
|
||||||
self.fsm = fsm_type()
|
self.fsm = fsm_type()
|
||||||
@@ -22,28 +22,29 @@ class FSMManagingEventHandler(BroadcastingEventHandler):
|
|||||||
try:
|
try:
|
||||||
data = message['data']
|
data = message['data']
|
||||||
message['data'] = self.command_handlers[data['command']](data)
|
message['data'] = self.command_handlers[data['command']](data)
|
||||||
|
self.server_connector.broadcast(self._fsm_updater.generate_fsm_update())
|
||||||
return message
|
return message
|
||||||
except KeyError:
|
except KeyError:
|
||||||
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
||||||
|
|
||||||
def handle_trigger(self, data):
|
def handle_trigger(self, data):
|
||||||
self.fsm.step(data['value'])
|
self.fsm.step(data['value'])
|
||||||
return self.with_fsm_update(data)
|
return data
|
||||||
|
|
||||||
def with_fsm_update(self, data):
|
|
||||||
return {
|
|
||||||
**data,
|
|
||||||
**self._fsm_updater.get_fsm_state_and_transitions()
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_update(self, data):
|
def handle_update(self, data):
|
||||||
return self.with_fsm_update(data)
|
return data
|
||||||
|
|
||||||
|
|
||||||
class FSMUpdater:
|
class FSMUpdater:
|
||||||
def __init__(self, fsm):
|
def __init__(self, fsm):
|
||||||
self.fsm = 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):
|
def get_fsm_state_and_transitions(self):
|
||||||
state = self.fsm.state
|
state = self.fsm.state
|
||||||
valid_transitions = [
|
valid_transitions = [
|
||||||
|
|||||||
@@ -31,10 +31,25 @@ class EventHandlerBase(ABC):
|
|||||||
a response back in case the handler returned something.
|
a response back in case the handler returned something.
|
||||||
This is subscribed in __init__().
|
This is subscribed in __init__().
|
||||||
"""
|
"""
|
||||||
|
if not self.check_key(message):
|
||||||
|
return
|
||||||
|
|
||||||
response = self.dispatch_handling(message)
|
response = self.dispatch_handling(message)
|
||||||
if response:
|
if response:
|
||||||
self.server_connector.send(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):
|
def dispatch_handling(self, message):
|
||||||
"""
|
"""
|
||||||
Used to dispatch messages to their specific handlers.
|
Used to dispatch messages to their specific handlers.
|
||||||
@@ -131,16 +146,9 @@ class BroadcastingEventHandler(EventHandlerBase, ABC):
|
|||||||
response = self.dispatch_handling(message)
|
response = self.dispatch_handling(message)
|
||||||
if response:
|
if response:
|
||||||
self.own_message_hashes.append(self.hash_message(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
|
@staticmethod
|
||||||
def hash_message(message):
|
def hash_message(message):
|
||||||
message_bytes = dumps(message, sort_keys=True).encode()
|
message_bytes = dumps(message, sort_keys=True).encode()
|
||||||
return md5(message_bytes).hexdigest()
|
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):
|
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
|
This envelopes the desired message in the 'data' field of the message to
|
||||||
TFWServer, which will mirror it to event handlers.
|
TFWServer, which will mirror it to event handlers.
|
||||||
|
|
||||||
:param message: JSON message you want to send
|
: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')}
|
self.send({
|
||||||
message['key'] = 'mirror'
|
'key': 'mirror',
|
||||||
message['data'] = nested_message
|
'data': message
|
||||||
self.send(message)
|
})
|
||||||
|
|
||||||
def send(self, 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
|
:param message: JSON message you want to send
|
||||||
"""
|
"""
|
||||||
self._zmq_push_socket.send_multipart(serialize_tfw_msg(message))
|
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):
|
class ServerConnector(ServerUplinkConnector, ServerDownlinkConnector):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+41
-8
@@ -1,25 +1,21 @@
|
|||||||
from subprocess import Popen, run
|
from subprocess import Popen, run
|
||||||
from functools import partial
|
from functools import partial, singledispatch
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
import jinja2
|
||||||
from transitions import State
|
from transitions import State
|
||||||
|
|
||||||
from tfw import FSMBase
|
from tfw import FSMBase
|
||||||
|
|
||||||
|
|
||||||
class YamlFSM(FSMBase):
|
class YamlFSM(FSMBase):
|
||||||
def __init__(self, config_file):
|
def __init__(self, config_file, jinja2_variables=None):
|
||||||
self.config = self.parse_config(config_file)
|
self.config = ConfigParser(config_file, jinja2_variables).config
|
||||||
self.setup_states()
|
self.setup_states()
|
||||||
super().__init__() # FSMBase.__init__() requires states
|
super().__init__() # FSMBase.__init__() requires states
|
||||||
self.setup_transitions()
|
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):
|
def setup_states(self):
|
||||||
self.for_config_states_and_transitions_do(self.wrap_callbacks_with_subprocess_call)
|
self.for_config_states_and_transitions_do(self.wrap_callbacks_with_subprocess_call)
|
||||||
self.states = [State(**state) for state in self.config['states']]
|
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):
|
def command_statuscode_is_zero(command):
|
||||||
return run(command, shell=True).returncode == 0
|
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
|
terminado==0.8.1
|
||||||
watchdog==0.8.3
|
watchdog==0.8.3
|
||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
|
Jinja2==2.10
|
||||||
|
|||||||
Reference in New Issue
Block a user