naeural-client 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- naeural_client/__init__.py +13 -0
- naeural_client/_ver.py +13 -0
- naeural_client/base/__init__.py +6 -0
- naeural_client/base/distributed_custom_code_presets.py +44 -0
- naeural_client/base/generic_session.py +1763 -0
- naeural_client/base/instance.py +616 -0
- naeural_client/base/payload/__init__.py +1 -0
- naeural_client/base/payload/payload.py +66 -0
- naeural_client/base/pipeline.py +1499 -0
- naeural_client/base/plugin_template.py +5209 -0
- naeural_client/base/responses.py +209 -0
- naeural_client/base/transaction.py +157 -0
- naeural_client/base_decentra_object.py +143 -0
- naeural_client/bc/__init__.py +3 -0
- naeural_client/bc/base.py +1046 -0
- naeural_client/bc/chain.py +0 -0
- naeural_client/bc/ec.py +324 -0
- naeural_client/certs/__init__.py +0 -0
- naeural_client/certs/r9092118.ala.eu-central-1.emqxsl.com.crt +22 -0
- naeural_client/code_cheker/__init__.py +1 -0
- naeural_client/code_cheker/base.py +520 -0
- naeural_client/code_cheker/checker.py +294 -0
- naeural_client/comm/__init__.py +2 -0
- naeural_client/comm/amqp_wrapper.py +338 -0
- naeural_client/comm/mqtt_wrapper.py +539 -0
- naeural_client/const/README.md +3 -0
- naeural_client/const/__init__.py +9 -0
- naeural_client/const/base.py +101 -0
- naeural_client/const/comms.py +80 -0
- naeural_client/const/environment.py +26 -0
- naeural_client/const/formatter.py +7 -0
- naeural_client/const/heartbeat.py +111 -0
- naeural_client/const/misc.py +20 -0
- naeural_client/const/payload.py +190 -0
- naeural_client/default/__init__.py +1 -0
- naeural_client/default/instance/__init__.py +4 -0
- naeural_client/default/instance/chain_dist_custom_job_01_plugin.py +54 -0
- naeural_client/default/instance/custom_web_app_01_plugin.py +118 -0
- naeural_client/default/instance/net_mon_01_plugin.py +45 -0
- naeural_client/default/instance/view_scene_01_plugin.py +28 -0
- naeural_client/default/session/mqtt_session.py +72 -0
- naeural_client/io_formatter/__init__.py +2 -0
- naeural_client/io_formatter/base/__init__.py +1 -0
- naeural_client/io_formatter/base/base_formatter.py +80 -0
- naeural_client/io_formatter/default/__init__.py +3 -0
- naeural_client/io_formatter/default/a_dummy.py +51 -0
- naeural_client/io_formatter/default/aixp1.py +113 -0
- naeural_client/io_formatter/default/default.py +22 -0
- naeural_client/io_formatter/io_formatter_manager.py +96 -0
- naeural_client/logging/__init__.py +1 -0
- naeural_client/logging/base_logger.py +2056 -0
- naeural_client/logging/logger_mixins/__init__.py +12 -0
- naeural_client/logging/logger_mixins/class_instance_mixin.py +92 -0
- naeural_client/logging/logger_mixins/computer_vision_mixin.py +443 -0
- naeural_client/logging/logger_mixins/datetime_mixin.py +344 -0
- naeural_client/logging/logger_mixins/download_mixin.py +421 -0
- naeural_client/logging/logger_mixins/general_serialization_mixin.py +242 -0
- naeural_client/logging/logger_mixins/json_serialization_mixin.py +481 -0
- naeural_client/logging/logger_mixins/pickle_serialization_mixin.py +301 -0
- naeural_client/logging/logger_mixins/process_mixin.py +63 -0
- naeural_client/logging/logger_mixins/resource_size_mixin.py +81 -0
- naeural_client/logging/logger_mixins/timers_mixin.py +501 -0
- naeural_client/logging/logger_mixins/upload_mixin.py +260 -0
- naeural_client/logging/logger_mixins/utils_mixin.py +675 -0
- naeural_client/logging/small_logger.py +93 -0
- naeural_client/logging/tzlocal/__init__.py +20 -0
- naeural_client/logging/tzlocal/unix.py +231 -0
- naeural_client/logging/tzlocal/utils.py +113 -0
- naeural_client/logging/tzlocal/win32.py +151 -0
- naeural_client/logging/tzlocal/windows_tz.py +718 -0
- naeural_client/plugins_manager_mixin.py +273 -0
- naeural_client/utils/__init__.py +2 -0
- naeural_client/utils/comm_utils.py +44 -0
- naeural_client/utils/dotenv.py +75 -0
- naeural_client-2.0.0.dist-info/METADATA +365 -0
- naeural_client-2.0.0.dist-info/RECORD +78 -0
- naeural_client-2.0.0.dist-info/WHEEL +4 -0
- naeural_client-2.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1763 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import traceback
|
4
|
+
from collections import deque
|
5
|
+
from datetime import datetime as dt
|
6
|
+
from threading import Lock, Thread
|
7
|
+
from time import sleep
|
8
|
+
from time import time as tm
|
9
|
+
|
10
|
+
from ..base_decentra_object import BaseDecentrAIObject
|
11
|
+
from ..bc import DefaultBlockEngine
|
12
|
+
from ..const import COMMANDS, ENVIRONMENT, HB, PAYLOAD_DATA, STATUS_TYPE
|
13
|
+
from ..const import comms as comm_ct
|
14
|
+
from ..io_formatter import IOFormatterWrapper
|
15
|
+
from ..logging import Logger
|
16
|
+
from ..utils import load_dotenv
|
17
|
+
from .payload import Payload
|
18
|
+
from .pipeline import Pipeline
|
19
|
+
from .transaction import Transaction
|
20
|
+
|
21
|
+
# TODO: add support for remaining commands from EE
|
22
|
+
|
23
|
+
|
24
|
+
class GenericSession(BaseDecentrAIObject):
|
25
|
+
"""
|
26
|
+
A Session is a connection to a communication server which provides the channel to interact with nodes from the Naeural network.
|
27
|
+
A Session manages `Pipelines` and handles all messages received from the communication server.
|
28
|
+
The Session handles all callbacks that are user-defined and passed as arguments in the API calls.
|
29
|
+
"""
|
30
|
+
default_config = {
|
31
|
+
"CONFIG_CHANNEL": {
|
32
|
+
"TOPIC": "{}/{}/config"
|
33
|
+
},
|
34
|
+
"CTRL_CHANNEL": {
|
35
|
+
"TOPIC": "{}/ctrl"
|
36
|
+
},
|
37
|
+
"NOTIF_CHANNEL": {
|
38
|
+
"TOPIC": "{}/notif"
|
39
|
+
},
|
40
|
+
"PAYLOADS_CHANNEL": {
|
41
|
+
"TOPIC": "{}/payloads"
|
42
|
+
},
|
43
|
+
"QOS": 0,
|
44
|
+
"CERT_PATH": None,
|
45
|
+
}
|
46
|
+
|
47
|
+
BLOCKCHAIN_CONFIG = {
|
48
|
+
"PEM_FILE": "_pk_sdk.pem",
|
49
|
+
"PASSWORD": None,
|
50
|
+
"PEM_LOCATION": "data"
|
51
|
+
}
|
52
|
+
|
53
|
+
def __init__(self, *,
|
54
|
+
host=None,
|
55
|
+
port=None,
|
56
|
+
user=None,
|
57
|
+
pwd=None,
|
58
|
+
secured=None,
|
59
|
+
name='pySDK',
|
60
|
+
encrypt_comms=True,
|
61
|
+
config={},
|
62
|
+
filter_workers=None,
|
63
|
+
log: Logger = None,
|
64
|
+
on_payload=None,
|
65
|
+
on_notification=None,
|
66
|
+
on_heartbeat=None,
|
67
|
+
silent=True,
|
68
|
+
verbosity=1,
|
69
|
+
dotenv_path=None,
|
70
|
+
show_commands=False,
|
71
|
+
blockchain_config=BLOCKCHAIN_CONFIG,
|
72
|
+
bc_engine=None,
|
73
|
+
formatter_plugins_locations=['plugins.io_formatters'],
|
74
|
+
root_topic="naeural",
|
75
|
+
**kwargs) -> None:
|
76
|
+
"""
|
77
|
+
A Session is a connection to a communication server which provides the channel to interact with nodes from the Naeural network.
|
78
|
+
A Session manages `Pipelines` and handles all messages received from the communication server.
|
79
|
+
The Session handles all callbacks that are user-defined and passed as arguments in the API calls.
|
80
|
+
|
81
|
+
Parameters
|
82
|
+
----------
|
83
|
+
host : str, optional
|
84
|
+
The hostname of the server. If None, it will be retrieved from the environment variable AIXP_HOSTNAME
|
85
|
+
port : int, optional
|
86
|
+
The port. If None, it will be retrieved from the environment variable AIXP_PORT
|
87
|
+
user : str, optional
|
88
|
+
The user name. If None, it will be retrieved from the environment variable AIXP_USERNAME
|
89
|
+
pwd : str, optional
|
90
|
+
The password. If None, it will be retrieved from the environment variable AIXP_PASSWORD
|
91
|
+
secured: bool, optional
|
92
|
+
True if connection is secured, by default None
|
93
|
+
name : str, optional
|
94
|
+
The name of this connection, used to identify owned pipelines on a specific Naeural edge node.
|
95
|
+
The name will be used as `INITIATOR_ID` and `SESSION_ID` when communicating with Naeural edge nodes, by default 'pySDK'
|
96
|
+
config : dict, optional
|
97
|
+
Configures the names of the channels this session will connect to.
|
98
|
+
If using a Mqtt server, these channels are in fact topics.
|
99
|
+
Modify this if you are absolutely certain of what you are doing.
|
100
|
+
By default {}
|
101
|
+
filter_workers: list, optional
|
102
|
+
If set, process the messages that come only from the nodes from this list.
|
103
|
+
Defaults to None
|
104
|
+
show_commands : bool
|
105
|
+
If True, will print the commands that are being sent to the Naeural edge nodes.
|
106
|
+
Defaults to False
|
107
|
+
log : Logger, optional
|
108
|
+
A logger object which implements basic logging functionality and some other utils stuff. Can be ignored for now.
|
109
|
+
In the future, the documentation for the Logger base class will be available and developers will be able to use
|
110
|
+
custom-made Loggers.
|
111
|
+
on_payload : Callable[[Session, str, str, str, str, dict], None], optional
|
112
|
+
Callback that handles all payloads received from this network.
|
113
|
+
As arguments, it has a reference to this Session object, the node name, the pipeline, signature and instance, and the payload.
|
114
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
115
|
+
the user has defined a specific callback.
|
116
|
+
on_notification : Callable[[Session, str, dict], None], optional
|
117
|
+
Callback that handles notifications received from this network.
|
118
|
+
As arguments, it has a reference to this Session object, the node name and the notification payload.
|
119
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
120
|
+
the user has defined a specific callback.
|
121
|
+
This callback will be called when there are notifications related to the node itself, e.g. when the node runs
|
122
|
+
low on memory.
|
123
|
+
Defaults to None.
|
124
|
+
on_heartbeat : Callable[[Session, str, dict], None], optional
|
125
|
+
Callback that handles heartbeats received from this network.
|
126
|
+
As arguments, it has a reference to this Session object, the node name and the heartbeat payload.
|
127
|
+
Defaults to None.
|
128
|
+
silent : bool, optional
|
129
|
+
This flag will disable debug logs, set to 'False` for a more verbose log, by default True
|
130
|
+
dotenv_path : str, optional
|
131
|
+
Path to the .env file, by default None. If None, the path will be searched in the current working directory and in the directories of the files from the call stack.
|
132
|
+
root_topic : str, optional
|
133
|
+
This is the root of the topics used by the SDK. It is used to create the topics for the communication channels.
|
134
|
+
Defaults to "naeural"
|
135
|
+
"""
|
136
|
+
|
137
|
+
# TODO: maybe read config from file?
|
138
|
+
self._config = {**self.default_config, **config}
|
139
|
+
|
140
|
+
if root_topic is not None:
|
141
|
+
for key in self._config.keys():
|
142
|
+
if isinstance(self._config[key], dict) and 'TOPIC' in self._config[key]:
|
143
|
+
if isinstance(self._config[key]["TOPIC"], str) and self._config[key]["TOPIC"].startswith("{}"):
|
144
|
+
nr_empty = self._config[key]["TOPIC"].count("{}")
|
145
|
+
self._config[key]["TOPIC"] = self._config[key]["TOPIC"].format(root_topic, *(["{}"] * (nr_empty - 1)))
|
146
|
+
# end if root_topic
|
147
|
+
|
148
|
+
self.log = log
|
149
|
+
self.name = name
|
150
|
+
|
151
|
+
self._verbosity = verbosity
|
152
|
+
self.encrypt_comms = encrypt_comms
|
153
|
+
|
154
|
+
self._dct_online_nodes_pipelines: dict[str, Pipeline] = {}
|
155
|
+
self._dct_online_nodes_last_heartbeat: dict[str, dict] = {}
|
156
|
+
self._dct_can_send_to_node: dict[str, bool] = {}
|
157
|
+
self._dct_node_last_seen_time = {}
|
158
|
+
self._dct_node_addr_name = {}
|
159
|
+
self.online_timeout = 60
|
160
|
+
self.filter_workers = filter_workers
|
161
|
+
self.__show_commands = show_commands
|
162
|
+
|
163
|
+
pwd = pwd or kwargs.get('password', kwargs.get('pass', None))
|
164
|
+
user = user or kwargs.get('username', None)
|
165
|
+
host = host or kwargs.get('hostname', None)
|
166
|
+
self.__fill_config(host, port, user, pwd, secured, dotenv_path)
|
167
|
+
|
168
|
+
self.custom_on_payload = on_payload
|
169
|
+
self.custom_on_heartbeat = on_heartbeat
|
170
|
+
self.custom_on_notification = on_notification
|
171
|
+
|
172
|
+
self.own_pipelines = []
|
173
|
+
|
174
|
+
self.__running_callback_threads = False
|
175
|
+
self.__running_main_loop_thread = False
|
176
|
+
self.__closed_everything = False
|
177
|
+
|
178
|
+
self.sdk_main_loop_thread = Thread(target=self.__main_loop, daemon=True)
|
179
|
+
self.__formatter_plugins_locations = formatter_plugins_locations
|
180
|
+
|
181
|
+
self.__bc_engine = bc_engine
|
182
|
+
self.__blockchain_config = blockchain_config
|
183
|
+
|
184
|
+
self.__open_transactions: list[Transaction] = []
|
185
|
+
self.__open_transactions_lock = Lock()
|
186
|
+
|
187
|
+
self.__create_user_callback_threads()
|
188
|
+
super(GenericSession, self).__init__(log=log, DEBUG=not silent, create_logger=True)
|
189
|
+
return
|
190
|
+
|
191
|
+
def startup(self):
|
192
|
+
self.__start_blockchain(self.__bc_engine, self.__blockchain_config)
|
193
|
+
self.formatter_wrapper = IOFormatterWrapper(self.log, plugin_search_locations=self.__formatter_plugins_locations)
|
194
|
+
|
195
|
+
self._connect()
|
196
|
+
|
197
|
+
if not self.encrypt_comms:
|
198
|
+
self.P(
|
199
|
+
"Warning: Emitted messages will not be encrypted.\n"
|
200
|
+
"This is not recommended for production environments.\n"
|
201
|
+
"\n"
|
202
|
+
"Please set `encrypt_comms` to `True` when creating the `Session` object.",
|
203
|
+
color='r',
|
204
|
+
verbosity=1,
|
205
|
+
boxed=True,
|
206
|
+
box_char='*',
|
207
|
+
)
|
208
|
+
|
209
|
+
self.__start_main_loop_thread()
|
210
|
+
super(GenericSession, self).startup()
|
211
|
+
|
212
|
+
# Message callbacks
|
213
|
+
if True:
|
214
|
+
def __create_user_callback_threads(self):
|
215
|
+
self._payload_messages = deque()
|
216
|
+
self._payload_thread = Thread(
|
217
|
+
target=self.__handle_messages,
|
218
|
+
args=(self._payload_messages, self.__on_payload),
|
219
|
+
daemon=True
|
220
|
+
)
|
221
|
+
|
222
|
+
self._notif_messages = deque()
|
223
|
+
self._notif_thread = Thread(
|
224
|
+
target=self.__handle_messages,
|
225
|
+
args=(self._notif_messages, self.__on_notification),
|
226
|
+
daemon=True
|
227
|
+
)
|
228
|
+
|
229
|
+
self._hb_messages = deque()
|
230
|
+
self._hb_thread = Thread(
|
231
|
+
target=self.__handle_messages,
|
232
|
+
args=(self._hb_messages, self.__on_heartbeat),
|
233
|
+
daemon=True
|
234
|
+
)
|
235
|
+
|
236
|
+
self.__running_callback_threads = True
|
237
|
+
self._hb_thread.start()
|
238
|
+
self._notif_thread.start()
|
239
|
+
self._payload_thread.start()
|
240
|
+
return
|
241
|
+
|
242
|
+
def __parse_message(self, dict_msg: dict):
|
243
|
+
"""
|
244
|
+
Get the formatter from the payload and decode the message
|
245
|
+
"""
|
246
|
+
# check if payload is encrypted
|
247
|
+
if dict_msg.get(PAYLOAD_DATA.EE_IS_ENCRYPTED, False):
|
248
|
+
encrypted_data = dict_msg.get(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
|
249
|
+
sender_addr = dict_msg.get(comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR, None)
|
250
|
+
|
251
|
+
str_data = self.bc_engine.decrypt(encrypted_data, sender_addr)
|
252
|
+
|
253
|
+
if str_data is None:
|
254
|
+
self.D("Cannot decrypt message, dropping..\n{}".format(str_data), verbosity=2)
|
255
|
+
return None
|
256
|
+
|
257
|
+
try:
|
258
|
+
dict_data = json.loads(str_data)
|
259
|
+
except Exception as e:
|
260
|
+
self.P("Error while decrypting message: {}".format(e), color='r', verbosity=1)
|
261
|
+
self.D("Message: {}".format(str_data), verbosity=2)
|
262
|
+
return None
|
263
|
+
|
264
|
+
dict_msg = {**dict_data, **dict_msg}
|
265
|
+
dict_msg.pop(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
|
266
|
+
# end if encrypted
|
267
|
+
|
268
|
+
formatter = self.formatter_wrapper \
|
269
|
+
.get_required_formatter_from_payload(dict_msg)
|
270
|
+
if formatter is not None:
|
271
|
+
return formatter.decode_output(dict_msg)
|
272
|
+
else:
|
273
|
+
return None
|
274
|
+
|
275
|
+
def __on_message_default_callback(self, message, message_callback) -> None:
|
276
|
+
"""
|
277
|
+
Default callback for all messages received from the communication server.
|
278
|
+
|
279
|
+
Parameters
|
280
|
+
----------
|
281
|
+
message : str
|
282
|
+
The message received from the communication server
|
283
|
+
message_callback : Callable[[dict, str, str, str, str], None]
|
284
|
+
The callback that will handle the message.
|
285
|
+
"""
|
286
|
+
dict_msg = json.loads(message)
|
287
|
+
# parse the message
|
288
|
+
dict_msg_parsed = self.__parse_message(dict_msg)
|
289
|
+
if dict_msg_parsed is None:
|
290
|
+
return
|
291
|
+
|
292
|
+
try:
|
293
|
+
msg_path = dict_msg.get(PAYLOAD_DATA.EE_PAYLOAD_PATH, [None] * 4)
|
294
|
+
# TODO: in the future, the EE_PAYLOAD_PATH will have the address, not the id
|
295
|
+
msg_node_id, msg_pipeline, msg_signature, msg_instance = msg_path
|
296
|
+
msg_node_addr = dict_msg.get(PAYLOAD_DATA.EE_SENDER, None)
|
297
|
+
except:
|
298
|
+
self.D("Message does not respect standard: {}".format(dict_msg), verbosity=2)
|
299
|
+
return
|
300
|
+
|
301
|
+
message_callback(dict_msg_parsed, msg_node_addr, msg_pipeline, msg_signature, msg_instance)
|
302
|
+
return
|
303
|
+
|
304
|
+
def __handle_messages(self, message_queue, message_callback):
|
305
|
+
"""
|
306
|
+
Handle messages from the communication server.
|
307
|
+
This method is called in a separate thread.
|
308
|
+
|
309
|
+
Parameters
|
310
|
+
----------
|
311
|
+
message_queue : deque
|
312
|
+
The queue of messages received from the communication server
|
313
|
+
message_callback : Callable[[dict, str, str, str, str], None]
|
314
|
+
The callback that will handle the message.
|
315
|
+
"""
|
316
|
+
while self.__running_callback_threads:
|
317
|
+
if len(message_queue) == 0:
|
318
|
+
sleep(0.01)
|
319
|
+
continue
|
320
|
+
current_msg = message_queue.popleft()
|
321
|
+
self.__on_message_default_callback(current_msg, message_callback)
|
322
|
+
# end while self.running
|
323
|
+
|
324
|
+
# process the remaining messages before exiting
|
325
|
+
while len(message_queue) > 0:
|
326
|
+
current_msg = message_queue.popleft()
|
327
|
+
self.__on_message_default_callback(current_msg, message_callback)
|
328
|
+
return
|
329
|
+
|
330
|
+
def __maybe_ignore_message(self, node_addr):
|
331
|
+
"""
|
332
|
+
Check if the message should be ignored.
|
333
|
+
A message should be ignored if the `filter_workers` attribute is set and the message comes from a node that is not in the list.
|
334
|
+
|
335
|
+
Parameters
|
336
|
+
----------
|
337
|
+
node_addr : str
|
338
|
+
The address of the Naeural edge node that sent the message.
|
339
|
+
|
340
|
+
Returns
|
341
|
+
-------
|
342
|
+
bool
|
343
|
+
True if the message should be ignored, False otherwise.
|
344
|
+
"""
|
345
|
+
return self.filter_workers is not None and node_addr not in self.filter_workers
|
346
|
+
|
347
|
+
def __track_online_node(self, node_addr, node_id):
|
348
|
+
"""
|
349
|
+
Track the last time a node was seen online.
|
350
|
+
|
351
|
+
Parameters
|
352
|
+
----------
|
353
|
+
node_addr : str
|
354
|
+
The address of the Naeural edge node that sent the message.
|
355
|
+
"""
|
356
|
+
self._dct_node_last_seen_time[node_addr] = tm()
|
357
|
+
self._dct_node_addr_name[node_addr] = node_id
|
358
|
+
return
|
359
|
+
|
360
|
+
def __track_allowed_node(self, node_addr, dict_msg):
|
361
|
+
"""
|
362
|
+
Track if this session is allowed to send messages to node.
|
363
|
+
|
364
|
+
Parameters
|
365
|
+
----------
|
366
|
+
node_addr : str
|
367
|
+
The address of the Naeural edge node that sent the message.
|
368
|
+
dict_msg : dict
|
369
|
+
The message received from the communication server.
|
370
|
+
"""
|
371
|
+
node_whitelist = dict_msg.get(HB.EE_WHITELIST, [])
|
372
|
+
node_secured = dict_msg.get(HB.SECURED, False)
|
373
|
+
|
374
|
+
self._dct_can_send_to_node[node_addr] = not node_secured or self.bc_engine.address_no_prefix in node_whitelist or self.bc_engine.address == node_addr
|
375
|
+
return
|
376
|
+
|
377
|
+
def __on_heartbeat(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance):
|
378
|
+
"""
|
379
|
+
Handle a heartbeat message received from the communication server.
|
380
|
+
|
381
|
+
Parameters
|
382
|
+
----------
|
383
|
+
dict_msg : dict
|
384
|
+
The message received from the communication server
|
385
|
+
msg_node_addr : str
|
386
|
+
The address of the Naeural edge node that sent the message.
|
387
|
+
msg_pipeline : str
|
388
|
+
The name of the pipeline that sent the message.
|
389
|
+
msg_signature : str
|
390
|
+
The signature of the plugin that sent the message.
|
391
|
+
msg_instance : str
|
392
|
+
The name of the instance that sent the message.
|
393
|
+
"""
|
394
|
+
# extract relevant data from the message
|
395
|
+
|
396
|
+
if dict_msg.get(HB.HEARTBEAT_VERSION) == HB.V2:
|
397
|
+
str_data = self.log.decompress_text(dict_msg[HB.ENCODED_DATA])
|
398
|
+
data = json.loads(str_data)
|
399
|
+
dict_msg = {**dict_msg, **data}
|
400
|
+
|
401
|
+
self._dct_online_nodes_last_heartbeat[msg_node_addr] = dict_msg
|
402
|
+
|
403
|
+
msg_node_id = dict_msg[PAYLOAD_DATA.EE_ID]
|
404
|
+
self.__track_online_node(msg_node_addr, msg_node_id)
|
405
|
+
|
406
|
+
msg_active_configs = dict_msg.get(HB.CONFIG_STREAMS)
|
407
|
+
if msg_active_configs is None:
|
408
|
+
return
|
409
|
+
|
410
|
+
# default action
|
411
|
+
if msg_node_addr not in self._dct_online_nodes_pipelines:
|
412
|
+
self._dct_online_nodes_pipelines[msg_node_addr] = {}
|
413
|
+
for config in msg_active_configs:
|
414
|
+
pipeline_name = config[PAYLOAD_DATA.NAME]
|
415
|
+
pipeline: Pipeline = self._dct_online_nodes_pipelines[msg_node_addr].get(pipeline_name, None)
|
416
|
+
if pipeline is not None:
|
417
|
+
pipeline._sync_configuration_with_remote({k.upper(): v for k, v in config.items()})
|
418
|
+
else:
|
419
|
+
self._dct_online_nodes_pipelines[msg_node_addr][pipeline_name] = self.__create_pipeline_from_config(
|
420
|
+
msg_node_addr, config)
|
421
|
+
|
422
|
+
# TODO: move this call in `__on_message_default_callback`
|
423
|
+
if self.__maybe_ignore_message(msg_node_addr):
|
424
|
+
return
|
425
|
+
|
426
|
+
# pass the heartbeat message to open transactions
|
427
|
+
with self.__open_transactions_lock:
|
428
|
+
open_transactions_copy = self.__open_transactions.copy()
|
429
|
+
# end with
|
430
|
+
for transaction in open_transactions_copy:
|
431
|
+
transaction.handle_heartbeat(dict_msg)
|
432
|
+
|
433
|
+
self.D("Received hb from: {}".format(msg_node_addr), verbosity=2)
|
434
|
+
|
435
|
+
self.__track_allowed_node(msg_node_addr, dict_msg)
|
436
|
+
|
437
|
+
# call the custom callback, if defined
|
438
|
+
if self.custom_on_heartbeat is not None:
|
439
|
+
self.custom_on_heartbeat(self, msg_node_addr, dict_msg)
|
440
|
+
|
441
|
+
return
|
442
|
+
|
443
|
+
def __on_notification(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance):
|
444
|
+
"""
|
445
|
+
Handle a notification message received from the communication server.
|
446
|
+
|
447
|
+
Parameters
|
448
|
+
----------
|
449
|
+
dict_msg : dict
|
450
|
+
The message received from the communication server
|
451
|
+
msg_node_addr : str
|
452
|
+
The address of the Naeural edge node that sent the message.
|
453
|
+
msg_pipeline : str
|
454
|
+
The name of the pipeline that sent the message.
|
455
|
+
msg_signature : str
|
456
|
+
The signature of the plugin that sent the message.
|
457
|
+
msg_instance : str
|
458
|
+
The name of the instance that sent the message.
|
459
|
+
"""
|
460
|
+
# extract relevant data from the message
|
461
|
+
notification_type = dict_msg.get(STATUS_TYPE.NOTIFICATION_TYPE)
|
462
|
+
notification = dict_msg.get(PAYLOAD_DATA.NOTIFICATION)
|
463
|
+
|
464
|
+
if self.__maybe_ignore_message(msg_node_addr):
|
465
|
+
return
|
466
|
+
|
467
|
+
color = None
|
468
|
+
if notification_type != STATUS_TYPE.STATUS_NORMAL:
|
469
|
+
color = 'r'
|
470
|
+
self.D("Received notification {} from <{}/{}>: {}"
|
471
|
+
.format(
|
472
|
+
notification_type,
|
473
|
+
msg_node_addr,
|
474
|
+
msg_pipeline,
|
475
|
+
notification),
|
476
|
+
color=color,
|
477
|
+
verbosity=2,
|
478
|
+
)
|
479
|
+
|
480
|
+
# call the pipeline and instance defined callbacks
|
481
|
+
for pipeline in self.own_pipelines:
|
482
|
+
if msg_node_addr == pipeline.node_addr and msg_pipeline == pipeline.name:
|
483
|
+
pipeline._on_notification(msg_signature, msg_instance, Payload(dict_msg))
|
484
|
+
# since we found the pipeline, we can stop searching
|
485
|
+
# because the pipelines have unique names
|
486
|
+
break
|
487
|
+
|
488
|
+
# pass the notification message to open transactions
|
489
|
+
with self.__open_transactions_lock:
|
490
|
+
open_transactions_copy = self.__open_transactions.copy()
|
491
|
+
# end with
|
492
|
+
for transaction in open_transactions_copy:
|
493
|
+
transaction.handle_notification(dict_msg)
|
494
|
+
# call the custom callback, if defined
|
495
|
+
if self.custom_on_notification is not None:
|
496
|
+
self.custom_on_notification(self, msg_node_addr, Payload(dict_msg))
|
497
|
+
|
498
|
+
return
|
499
|
+
|
500
|
+
# TODO: maybe convert dict_msg to Payload object
|
501
|
+
# also maybe strip the dict from useless info for the user of the sdk
|
502
|
+
# Add try-except + sleep
|
503
|
+
def __on_payload(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance) -> None:
|
504
|
+
"""
|
505
|
+
Handle a payload message received from the communication server.
|
506
|
+
|
507
|
+
Parameters
|
508
|
+
----------
|
509
|
+
dict_msg : dict
|
510
|
+
The message received from the communication server
|
511
|
+
msg_node_addr : str
|
512
|
+
The address of the Naeural edge node that sent the message.
|
513
|
+
msg_pipeline : str
|
514
|
+
The name of the pipeline that sent the message.
|
515
|
+
msg_signature : str
|
516
|
+
The signature of the plugin that sent the message.
|
517
|
+
msg_instance : str
|
518
|
+
The name of the instance that sent the message.
|
519
|
+
"""
|
520
|
+
# extract relevant data from the message
|
521
|
+
msg_data = dict_msg
|
522
|
+
|
523
|
+
if self.__maybe_ignore_message(msg_node_addr):
|
524
|
+
return
|
525
|
+
|
526
|
+
# call the pipeline and instance defined callbacks
|
527
|
+
for pipeline in self.own_pipelines:
|
528
|
+
if msg_node_addr == pipeline.node_addr and msg_pipeline == pipeline.name:
|
529
|
+
pipeline._on_data(msg_signature, msg_instance, Payload(dict_msg))
|
530
|
+
# since we found the pipeline, we can stop searching
|
531
|
+
# because the pipelines have unique names
|
532
|
+
break
|
533
|
+
|
534
|
+
# pass the payload message to open transactions
|
535
|
+
with self.__open_transactions_lock:
|
536
|
+
open_transactions_copy = self.__open_transactions.copy()
|
537
|
+
# end with
|
538
|
+
for transaction in open_transactions_copy:
|
539
|
+
transaction.handle_payload(dict_msg)
|
540
|
+
if self.custom_on_payload is not None:
|
541
|
+
self.custom_on_payload(self, msg_node_addr, msg_pipeline, msg_signature, msg_instance, Payload(msg_data))
|
542
|
+
|
543
|
+
return
|
544
|
+
|
545
|
+
# Main loop
|
546
|
+
if True:
|
547
|
+
def __start_blockchain(self, bc_engine, blockchain_config):
|
548
|
+
if bc_engine is not None:
|
549
|
+
self.bc_engine = bc_engine
|
550
|
+
return
|
551
|
+
|
552
|
+
try:
|
553
|
+
self.bc_engine = DefaultBlockEngine(
|
554
|
+
log=self.log,
|
555
|
+
name=self.name,
|
556
|
+
config=blockchain_config,
|
557
|
+
verbosity=self._verbosity,
|
558
|
+
)
|
559
|
+
except:
|
560
|
+
raise ValueError("Failure in private blockchain setup:\n{}".format(traceback.format_exc()))
|
561
|
+
return
|
562
|
+
|
563
|
+
def __start_main_loop_thread(self):
|
564
|
+
self._main_loop_thread = Thread(target=self.__main_loop, daemon=True)
|
565
|
+
|
566
|
+
self.__running_main_loop_thread = True
|
567
|
+
self._main_loop_thread.start()
|
568
|
+
return
|
569
|
+
|
570
|
+
def __handle_open_transactions(self):
|
571
|
+
with self.__open_transactions_lock:
|
572
|
+
solved_transactions = [i for i, transaction in enumerate(self.__open_transactions) if transaction.is_solved()]
|
573
|
+
solved_transactions.reverse()
|
574
|
+
|
575
|
+
for idx in solved_transactions:
|
576
|
+
self.__open_transactions[idx].callback()
|
577
|
+
self.__open_transactions.pop(idx)
|
578
|
+
return
|
579
|
+
|
580
|
+
@property
|
581
|
+
def _connected(self):
|
582
|
+
"""
|
583
|
+
Check if the session is connected to the communication server.
|
584
|
+
"""
|
585
|
+
raise NotImplementedError
|
586
|
+
|
587
|
+
def __maybe_reconnect(self) -> None:
|
588
|
+
"""
|
589
|
+
Attempt reconnecting to the communication server if an unexpected disconnection ocurred,
|
590
|
+
using the credentials provided when creating this instance.
|
591
|
+
|
592
|
+
This method should be called in a user-defined main loop.
|
593
|
+
This method is called in `run` method, in the main loop.
|
594
|
+
"""
|
595
|
+
if self._connected == False:
|
596
|
+
self._connect()
|
597
|
+
return
|
598
|
+
|
599
|
+
def __close_own_pipelines(self, wait=True):
|
600
|
+
"""
|
601
|
+
Close all pipelines that were created by or attached to this session.
|
602
|
+
|
603
|
+
Parameters
|
604
|
+
----------
|
605
|
+
wait : bool, optional
|
606
|
+
If `True`, will wait for the transactions to finish. Defaults to `True`
|
607
|
+
"""
|
608
|
+
# iterate through all CREATED pipelines from this session and close them
|
609
|
+
transactions = []
|
610
|
+
|
611
|
+
for pipeline in self.own_pipelines:
|
612
|
+
transactions.extend(pipeline._close())
|
613
|
+
|
614
|
+
self.P("Closing own pipelines: {}".format([p.name for p in self.own_pipelines]))
|
615
|
+
|
616
|
+
if wait:
|
617
|
+
self.wait_for_transactions(transactions)
|
618
|
+
self.P("Closed own pipelines.")
|
619
|
+
return
|
620
|
+
|
621
|
+
def _communication_close(self):
|
622
|
+
"""
|
623
|
+
Close the communication server connection.
|
624
|
+
"""
|
625
|
+
raise NotImplementedError
|
626
|
+
|
627
|
+
def close(self, close_pipelines=False, wait_close=False, **kwargs):
|
628
|
+
"""
|
629
|
+
Close the session, releasing all resources and closing all threads
|
630
|
+
Resources are released in the main loop thread, so this method will block until the main loop thread exits.
|
631
|
+
This method is blocking.
|
632
|
+
|
633
|
+
Parameters
|
634
|
+
----------
|
635
|
+
close_pipelines : bool, optional
|
636
|
+
close all the pipelines created by or attached to this session (basically calling `.close_own_pipelines()` for you), by default False
|
637
|
+
wait_close : bool, optional
|
638
|
+
If `True`, will wait for the main loop thread to exit. Defaults to `False`
|
639
|
+
"""
|
640
|
+
|
641
|
+
if close_pipelines:
|
642
|
+
self.__close_own_pipelines(wait=wait_close)
|
643
|
+
|
644
|
+
self.__running_main_loop_thread = False
|
645
|
+
|
646
|
+
# wait for the main loop thread to exit
|
647
|
+
while not self.__closed_everything and wait_close:
|
648
|
+
sleep(0.1)
|
649
|
+
|
650
|
+
return
|
651
|
+
|
652
|
+
def _connect(self) -> None:
|
653
|
+
"""
|
654
|
+
Connect to the communication server using the credentials provided when creating this instance.
|
655
|
+
"""
|
656
|
+
raise NotImplementedError
|
657
|
+
|
658
|
+
def _send_payload(self, to, payload):
|
659
|
+
"""
|
660
|
+
Send a payload to a node.
|
661
|
+
|
662
|
+
Parameters
|
663
|
+
----------
|
664
|
+
to : str
|
665
|
+
The name of the Naeural edge node that will receive the payload.
|
666
|
+
payload : dict
|
667
|
+
The payload to send.
|
668
|
+
"""
|
669
|
+
raise NotImplementedError
|
670
|
+
|
671
|
+
def __release_callback_threads(self):
|
672
|
+
"""
|
673
|
+
Release all resources and close all threads
|
674
|
+
"""
|
675
|
+
self.__running_callback_threads = False
|
676
|
+
|
677
|
+
self._payload_thread.join()
|
678
|
+
self._notif_thread.join()
|
679
|
+
self._hb_thread.join()
|
680
|
+
return
|
681
|
+
|
682
|
+
def __main_loop(self):
|
683
|
+
"""
|
684
|
+
The main loop of this session. This method is called in a separate thread.
|
685
|
+
This method runs on a separate thread from the main thread, and it is responsible for handling all messages received from the communication server.
|
686
|
+
We use it like this to avoid blocking the main thread, which is used by the user.
|
687
|
+
"""
|
688
|
+
while self.__running_main_loop_thread:
|
689
|
+
self.__maybe_reconnect()
|
690
|
+
self.__handle_open_transactions()
|
691
|
+
sleep(0.1)
|
692
|
+
# end while self.running
|
693
|
+
|
694
|
+
self.P("Main loop thread exiting...", verbosity=2)
|
695
|
+
self.__release_callback_threads()
|
696
|
+
|
697
|
+
self.P("Comms closing...", verbosity=2)
|
698
|
+
self._communication_close()
|
699
|
+
self.__closed_everything = True
|
700
|
+
return
|
701
|
+
|
702
|
+
def run(self, wait=True, close_session=True, close_pipelines=False):
|
703
|
+
"""
|
704
|
+
This simple method will lock the main thread in a loop.
|
705
|
+
|
706
|
+
Parameters
|
707
|
+
----------
|
708
|
+
wait : bool, float, callable
|
709
|
+
If `True`, will wait forever.
|
710
|
+
If `False`, will not wait at all
|
711
|
+
If type `float` and > 0, will wait said amount of seconds
|
712
|
+
If type `float` and == 0, will wait forever
|
713
|
+
If type `callable`, will call the function until it returns `False`
|
714
|
+
Defaults to `True`
|
715
|
+
close_session : bool, optional
|
716
|
+
If `True` will close the session when the loop is exited.
|
717
|
+
Defaults to `True`
|
718
|
+
close_pipelines : bool, optional
|
719
|
+
If `True` will close all pipelines initiated by this session when the loop is exited.
|
720
|
+
This flag is ignored if `close_session` is `False`.
|
721
|
+
Defaults to `False`
|
722
|
+
"""
|
723
|
+
_start_timer = tm()
|
724
|
+
try:
|
725
|
+
bool_loop_condition = isinstance(wait, bool) and wait
|
726
|
+
number_loop_condition = isinstance(wait, (int, float)) and (wait == 0 or (tm() - _start_timer) < wait)
|
727
|
+
callable_loop_condition = callable(wait) and wait()
|
728
|
+
while (bool_loop_condition or number_loop_condition or callable_loop_condition) and not self.__closed_everything:
|
729
|
+
sleep(0.1)
|
730
|
+
bool_loop_condition = isinstance(wait, bool) and wait
|
731
|
+
number_loop_condition = isinstance(wait, (int, float)) and (wait == 0 or (tm() - _start_timer) < wait)
|
732
|
+
callable_loop_condition = callable(wait) and wait()
|
733
|
+
except KeyboardInterrupt:
|
734
|
+
self.P("CTRL+C detected. Stopping loop.", color='r', verbosity=1)
|
735
|
+
|
736
|
+
if close_session:
|
737
|
+
self.close(close_pipelines, wait_close=True)
|
738
|
+
|
739
|
+
return
|
740
|
+
|
741
|
+
def sleep(self, wait=True):
|
742
|
+
"""
|
743
|
+
Sleep for a given amount of time.
|
744
|
+
|
745
|
+
Parameters
|
746
|
+
----------
|
747
|
+
wait : bool, float, callable
|
748
|
+
If `True`, will wait forever.
|
749
|
+
If `False`, will not wait at all
|
750
|
+
If type `float` and > 0, will wait said amount of seconds
|
751
|
+
If type `float` and == 0, will wait forever
|
752
|
+
If type `callable`, will call the function until it returns `False`
|
753
|
+
Defaults to `True`
|
754
|
+
"""
|
755
|
+
_start_timer = tm()
|
756
|
+
try:
|
757
|
+
bool_loop_condition = isinstance(wait, bool) and wait
|
758
|
+
number_loop_condition = isinstance(wait, (int, float)) and (wait == 0 or (tm() - _start_timer) < wait)
|
759
|
+
callable_loop_condition = callable(wait) and wait()
|
760
|
+
while (bool_loop_condition or number_loop_condition or callable_loop_condition):
|
761
|
+
sleep(0.1)
|
762
|
+
bool_loop_condition = isinstance(wait, bool) and wait
|
763
|
+
number_loop_condition = isinstance(wait, (int, float)) and (wait == 0 or (tm() - _start_timer) < wait)
|
764
|
+
callable_loop_condition = callable(wait) and wait()
|
765
|
+
except KeyboardInterrupt:
|
766
|
+
self.P("CTRL+C detected. Stopping loop.", color='r', verbosity=1)
|
767
|
+
return
|
768
|
+
|
769
|
+
# Utils
|
770
|
+
if True:
|
771
|
+
def __fill_config(self, host, port, user, pwd, secured, dotenv_path):
|
772
|
+
"""
|
773
|
+
Fill the configuration dictionary with the credentials provided when creating this instance.
|
774
|
+
|
775
|
+
|
776
|
+
Parameters
|
777
|
+
----------
|
778
|
+
host : str
|
779
|
+
The hostname of the server.
|
780
|
+
Can be retrieved from the environment variables AIXP_HOSTNAME, AIXP_HOST
|
781
|
+
port : int
|
782
|
+
The port.
|
783
|
+
Can be retrieved from the environment variable AIXP_PORT
|
784
|
+
user : str
|
785
|
+
The user name.
|
786
|
+
Can be retrieved from the environment variables AIXP_USERNAME, AIXP_USER
|
787
|
+
pwd : str
|
788
|
+
The password.
|
789
|
+
Can be retrieved from the environment variables AIXP_PASSWORD, AIXP_PASS, AIXP_PWD
|
790
|
+
dotenv_path : str, optional
|
791
|
+
Path to the .env file, by default None. If None, the path will be searched in the current working directory and in the directories of the files from the call stack.
|
792
|
+
|
793
|
+
Raises
|
794
|
+
------
|
795
|
+
ValueError
|
796
|
+
Missing credentials
|
797
|
+
"""
|
798
|
+
|
799
|
+
# this method will search for the credentials in the environment variables
|
800
|
+
# the path to env file, if not specified, will be search in the following order:
|
801
|
+
# 1. current working directory
|
802
|
+
# 2-N. directories of the files from the call stack
|
803
|
+
load_dotenv(dotenv_path=dotenv_path, verbose=False)
|
804
|
+
|
805
|
+
possible_user_values = [
|
806
|
+
user,
|
807
|
+
os.getenv(ENVIRONMENT.AIXP_USERNAME),
|
808
|
+
os.getenv(ENVIRONMENT.AIXP_USER),
|
809
|
+
os.getenv(ENVIRONMENT.EE_USERNAME),
|
810
|
+
os.getenv(ENVIRONMENT.EE_USER),
|
811
|
+
self._config.get(comm_ct.USER),
|
812
|
+
]
|
813
|
+
|
814
|
+
user = next((x for x in possible_user_values if x is not None), None)
|
815
|
+
|
816
|
+
if user is None:
|
817
|
+
env_error = "Error: No user specified for Naeural network connection. Please make sure you have the correct credentials in the environment variables within the .env file or provide them as params in code (not recommended due to potential security issue)."
|
818
|
+
raise ValueError(env_error)
|
819
|
+
if self._config.get(comm_ct.USER, None) is None:
|
820
|
+
self._config[comm_ct.USER] = user
|
821
|
+
|
822
|
+
possible_password_values = [
|
823
|
+
pwd,
|
824
|
+
os.getenv(ENVIRONMENT.AIXP_PASSWORD),
|
825
|
+
os.getenv(ENVIRONMENT.AIXP_PASS),
|
826
|
+
os.getenv(ENVIRONMENT.AIXP_PWD),
|
827
|
+
os.getenv(ENVIRONMENT.EE_PASSWORD),
|
828
|
+
os.getenv(ENVIRONMENT.EE_PASS),
|
829
|
+
os.getenv(ENVIRONMENT.EE_PWD),
|
830
|
+
self._config.get(comm_ct.PASS),
|
831
|
+
]
|
832
|
+
|
833
|
+
pwd = next((x for x in possible_password_values if x is not None), None)
|
834
|
+
|
835
|
+
if pwd is None:
|
836
|
+
raise ValueError("Error: No password specified for Naeural network connection")
|
837
|
+
if self._config.get(comm_ct.PASS, None) is None:
|
838
|
+
self._config[comm_ct.PASS] = pwd
|
839
|
+
|
840
|
+
possible_host_values = [
|
841
|
+
host,
|
842
|
+
os.getenv(ENVIRONMENT.AIXP_HOSTNAME),
|
843
|
+
os.getenv(ENVIRONMENT.AIXP_HOST),
|
844
|
+
os.getenv(ENVIRONMENT.EE_HOSTNAME),
|
845
|
+
os.getenv(ENVIRONMENT.EE_HOST),
|
846
|
+
self._config.get(comm_ct.HOST),
|
847
|
+
"r9092118.ala.eu-central-1.emqxsl.com",
|
848
|
+
]
|
849
|
+
|
850
|
+
host = next((x for x in possible_host_values if x is not None), None)
|
851
|
+
|
852
|
+
if host is None:
|
853
|
+
raise ValueError("Error: No host specified for Naeural network connection")
|
854
|
+
if self._config.get(comm_ct.HOST, None) is None:
|
855
|
+
self._config[comm_ct.HOST] = host
|
856
|
+
|
857
|
+
possible_port_values = [
|
858
|
+
port,
|
859
|
+
os.getenv(ENVIRONMENT.AIXP_PORT),
|
860
|
+
os.getenv(ENVIRONMENT.EE_PORT),
|
861
|
+
self._config.get(comm_ct.PORT),
|
862
|
+
8883,
|
863
|
+
]
|
864
|
+
|
865
|
+
port = next((x for x in possible_port_values if x is not None), None)
|
866
|
+
|
867
|
+
if port is None:
|
868
|
+
raise ValueError("Error: No port specified for Naeural network connection")
|
869
|
+
if self._config.get(comm_ct.PORT, None) is None:
|
870
|
+
self._config[comm_ct.PORT] = int(port)
|
871
|
+
|
872
|
+
possible_cert_path_values = [
|
873
|
+
os.getenv(ENVIRONMENT.AIXP_CERT_PATH),
|
874
|
+
os.getenv(ENVIRONMENT.EE_CERT_PATH),
|
875
|
+
self._config.get(comm_ct.CERT_PATH),
|
876
|
+
]
|
877
|
+
|
878
|
+
cert_path = next((x for x in possible_cert_path_values if x is not None), None)
|
879
|
+
if cert_path is not None and self._config.get(comm_ct.CERT_PATH, None) is None:
|
880
|
+
self._config[comm_ct.CERT_PATH] = cert_path
|
881
|
+
|
882
|
+
possible_secured_values = [
|
883
|
+
secured,
|
884
|
+
os.getenv(ENVIRONMENT.AIXP_SECURED),
|
885
|
+
os.getenv(ENVIRONMENT.EE_SECURED),
|
886
|
+
self._config.get(comm_ct.SECURED),
|
887
|
+
False,
|
888
|
+
]
|
889
|
+
|
890
|
+
secured = next((x for x in possible_secured_values if x is not None), None)
|
891
|
+
if secured is not None and self._config.get(comm_ct.SECURED, None) is None:
|
892
|
+
secured = str(secured).strip().upper() in ['TRUE', '1']
|
893
|
+
self._config[comm_ct.SECURED] = secured
|
894
|
+
return
|
895
|
+
|
896
|
+
def __get_node_address(self, node):
|
897
|
+
"""
|
898
|
+
Get the address of a node. If node is an address, return it. Else, return the address of the node.
|
899
|
+
|
900
|
+
Parameters
|
901
|
+
----------
|
902
|
+
node : str
|
903
|
+
Address or Name of the node.
|
904
|
+
|
905
|
+
Returns
|
906
|
+
-------
|
907
|
+
str
|
908
|
+
The address of the node.
|
909
|
+
"""
|
910
|
+
if node not in self.get_active_nodes():
|
911
|
+
node = next((key for key, value in self._dct_node_addr_name.items() if value == node), node)
|
912
|
+
return node
|
913
|
+
|
914
|
+
def _send_command_to_box(self, command, worker, payload, show_command=False, session_id=None, **kwargs):
|
915
|
+
"""
|
916
|
+
Send a command to a node.
|
917
|
+
|
918
|
+
Parameters
|
919
|
+
----------
|
920
|
+
command : str
|
921
|
+
The command to send.
|
922
|
+
worker : str
|
923
|
+
The name of the Naeural edge node that will receive the command.
|
924
|
+
payload : dict
|
925
|
+
The payload to send.
|
926
|
+
show_command : bool, optional
|
927
|
+
If True, will print the complete command that is being sent, by default False
|
928
|
+
"""
|
929
|
+
|
930
|
+
show_command = show_command or self.__show_commands
|
931
|
+
|
932
|
+
if len(kwargs) > 0:
|
933
|
+
self.D("Ignoring extra kwargs: {}".format(kwargs), verbosity=2)
|
934
|
+
|
935
|
+
critical_data = {
|
936
|
+
comm_ct.COMM_SEND_MESSAGE.K_ACTION: command,
|
937
|
+
comm_ct.COMM_SEND_MESSAGE.K_PAYLOAD: payload,
|
938
|
+
}
|
939
|
+
|
940
|
+
# This part is duplicated with the creation of payloads
|
941
|
+
encrypt_payload = self.encrypt_comms
|
942
|
+
if encrypt_payload and worker is not None:
|
943
|
+
# TODO: use safe_json_dumps
|
944
|
+
str_data = json.dumps(critical_data)
|
945
|
+
str_enc_data = self.bc_engine.encrypt(str_data, worker)
|
946
|
+
critical_data = {
|
947
|
+
comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED: True,
|
948
|
+
comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA: str_enc_data,
|
949
|
+
}
|
950
|
+
else:
|
951
|
+
critical_data[comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED] = False
|
952
|
+
if encrypt_payload:
|
953
|
+
critical_data[comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA] = "Error! No receiver address found!"
|
954
|
+
|
955
|
+
# endif
|
956
|
+
msg_to_send = {
|
957
|
+
**critical_data,
|
958
|
+
comm_ct.COMM_SEND_MESSAGE.K_EE_ID: worker,
|
959
|
+
comm_ct.COMM_SEND_MESSAGE.K_SESSION_ID: session_id or self.name,
|
960
|
+
comm_ct.COMM_SEND_MESSAGE.K_INITIATOR_ID: self.name,
|
961
|
+
comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR: self.bc_engine.address,
|
962
|
+
comm_ct.COMM_SEND_MESSAGE.K_TIME: dt.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
|
963
|
+
}
|
964
|
+
self.bc_engine.sign(msg_to_send, use_digest=True)
|
965
|
+
if show_command:
|
966
|
+
self.P("Sending command '{}' to '{}':\n{}".format(command, worker, json.dumps(msg_to_send, indent=2)),
|
967
|
+
color='y',
|
968
|
+
verbosity=1
|
969
|
+
)
|
970
|
+
self._send_payload(worker, msg_to_send)
|
971
|
+
return
|
972
|
+
|
973
|
+
def _send_command_create_pipeline(self, worker, pipeline_config, **kwargs):
|
974
|
+
self._send_command_to_box(COMMANDS.UPDATE_CONFIG, worker, pipeline_config, **kwargs)
|
975
|
+
return
|
976
|
+
|
977
|
+
def _send_command_delete_pipeline(self, worker, pipeline_name, **kwargs):
|
978
|
+
# TODO: remove this command calls from examples
|
979
|
+
self._send_command_to_box(COMMANDS.DELETE_CONFIG, worker, pipeline_name, **kwargs)
|
980
|
+
return
|
981
|
+
|
982
|
+
def _send_command_archive_pipeline(self, worker, pipeline_name, **kwargs):
|
983
|
+
self._send_command_to_box(COMMANDS.ARCHIVE_CONFIG, worker, pipeline_name, **kwargs)
|
984
|
+
return
|
985
|
+
|
986
|
+
def _send_command_update_pipeline_config(self, worker, pipeline_config, **kwargs):
|
987
|
+
self._send_command_to_box(COMMANDS.UPDATE_CONFIG, worker, pipeline_config, **kwargs)
|
988
|
+
return
|
989
|
+
|
990
|
+
def _send_command_update_instance_config(self, worker, pipeline_name, signature, instance_id, instance_config, **kwargs):
|
991
|
+
payload = {
|
992
|
+
PAYLOAD_DATA.NAME: pipeline_name,
|
993
|
+
PAYLOAD_DATA.SIGNATURE: signature,
|
994
|
+
PAYLOAD_DATA.INSTANCE_ID: instance_id,
|
995
|
+
PAYLOAD_DATA.INSTANCE_CONFIG: {k.upper(): v for k, v in instance_config.items()}
|
996
|
+
}
|
997
|
+
self._send_command_to_box(COMMANDS.UPDATE_PIPELINE_INSTANCE, worker, payload, **kwargs)
|
998
|
+
return
|
999
|
+
|
1000
|
+
def _send_command_batch_update_instance_config(self, worker, lst_updates, **kwargs):
|
1001
|
+
for update in lst_updates:
|
1002
|
+
assert isinstance(update, dict), "All updates must be dicts"
|
1003
|
+
assert PAYLOAD_DATA.NAME in update, "All updates must have a pipeline name"
|
1004
|
+
assert PAYLOAD_DATA.SIGNATURE in update, "All updates must have a plugin signature"
|
1005
|
+
assert PAYLOAD_DATA.INSTANCE_ID in update, "All updates must have a plugin instance id"
|
1006
|
+
assert PAYLOAD_DATA.INSTANCE_CONFIG in update, "All updates must have a plugin instance config"
|
1007
|
+
assert isinstance(update[PAYLOAD_DATA.INSTANCE_CONFIG], dict), \
|
1008
|
+
"All updates must have a plugin instance config as dict"
|
1009
|
+
self._send_command_to_box(COMMANDS.BATCH_UPDATE_PIPELINE_INSTANCE, worker, lst_updates, **kwargs)
|
1010
|
+
|
1011
|
+
def _send_command_pipeline_command(self, worker, pipeline_name, command, payload=None, command_params=None, **kwargs):
|
1012
|
+
if isinstance(command, str):
|
1013
|
+
command = {command: True}
|
1014
|
+
if payload is not None:
|
1015
|
+
command.update(payload)
|
1016
|
+
if command_params is not None:
|
1017
|
+
command[COMMANDS.COMMAND_PARAMS] = command_params
|
1018
|
+
|
1019
|
+
pipeline_command = {
|
1020
|
+
PAYLOAD_DATA.NAME: pipeline_name,
|
1021
|
+
COMMANDS.PIPELINE_COMMAND: command,
|
1022
|
+
}
|
1023
|
+
self._send_command_to_box(COMMANDS.PIPELINE_COMMAND, worker, pipeline_command, **kwargs)
|
1024
|
+
return
|
1025
|
+
|
1026
|
+
def _send_command_instance_command(self, worker, pipeline_name, signature, instance_id, command, payload=None, command_params=None, **kwargs):
|
1027
|
+
if command_params is None:
|
1028
|
+
command_params = {}
|
1029
|
+
if isinstance(command, str):
|
1030
|
+
command_params[command] = True
|
1031
|
+
command = {}
|
1032
|
+
if payload is not None:
|
1033
|
+
command = {**command, **payload}
|
1034
|
+
|
1035
|
+
command[COMMANDS.COMMAND_PARAMS] = command_params
|
1036
|
+
|
1037
|
+
instance_command = {COMMANDS.INSTANCE_COMMAND: command}
|
1038
|
+
self._send_command_update_instance_config(
|
1039
|
+
worker, pipeline_name, signature, instance_id, instance_command, **kwargs)
|
1040
|
+
return
|
1041
|
+
|
1042
|
+
def _send_command_stop_node(self, worker, **kwargs):
|
1043
|
+
self._send_command_to_box(COMMANDS.STOP, worker, None, **kwargs)
|
1044
|
+
return
|
1045
|
+
|
1046
|
+
def _send_command_restart_node(self, worker, **kwargs):
|
1047
|
+
self._send_command_to_box(COMMANDS.RESTART, worker, None, **kwargs)
|
1048
|
+
return
|
1049
|
+
|
1050
|
+
def _send_command_request_heartbeat(self, worker, full_heartbeat=False, **kwargs):
|
1051
|
+
command = COMMANDS.FULL_HEARTBEAT if full_heartbeat else COMMANDS.TIMERS_ONLY_HEARTBEAT
|
1052
|
+
self._send_command_to_box(command, worker, None, **kwargs)
|
1053
|
+
|
1054
|
+
def _send_command_reload_from_disk(self, worker, **kwargs):
|
1055
|
+
self._send_command_to_box(COMMANDS.RELOAD_CONFIG_FROM_DISK, worker, None, **kwargs)
|
1056
|
+
return
|
1057
|
+
|
1058
|
+
def _send_command_archive_all(self, worker, **kwargs):
|
1059
|
+
self._send_command_to_box(COMMANDS.ARCHIVE_CONFIG_ALL, worker, None, **kwargs)
|
1060
|
+
return
|
1061
|
+
|
1062
|
+
def _send_command_delete_all(self, worker, **kwargs):
|
1063
|
+
self._send_command_to_box(COMMANDS.DELETE_CONFIG_ALL, worker, None, **kwargs)
|
1064
|
+
return
|
1065
|
+
|
1066
|
+
def _register_transaction(self, session_id: str, lst_required_responses: list = None, timeout=0, on_success_callback: callable = None, on_failure_callback: callable = None) -> Transaction:
|
1067
|
+
"""
|
1068
|
+
Register a new transaction.
|
1069
|
+
|
1070
|
+
Parameters
|
1071
|
+
----------
|
1072
|
+
session_id : str
|
1073
|
+
The session id.
|
1074
|
+
lst_required_responses : list[Response], optional
|
1075
|
+
The list of required responses, by default None
|
1076
|
+
timeout : int, optional
|
1077
|
+
The timeout, by default 0
|
1078
|
+
on_success_callback : _type_, optional
|
1079
|
+
The on success callback, by default None
|
1080
|
+
on_failure_callback : _type_, optional
|
1081
|
+
The on failure callback, by default None
|
1082
|
+
Returns
|
1083
|
+
-------
|
1084
|
+
Transaction
|
1085
|
+
The transaction object
|
1086
|
+
"""
|
1087
|
+
transaction = Transaction(
|
1088
|
+
log=self.log,
|
1089
|
+
session_id=session_id,
|
1090
|
+
lst_required_responses=lst_required_responses or [],
|
1091
|
+
timeout=timeout,
|
1092
|
+
on_success_callback=on_success_callback,
|
1093
|
+
on_failure_callback=on_failure_callback,
|
1094
|
+
)
|
1095
|
+
|
1096
|
+
with self.__open_transactions_lock:
|
1097
|
+
self.__open_transactions.append(transaction)
|
1098
|
+
return transaction
|
1099
|
+
|
1100
|
+
def __create_pipeline_from_config(self, node_addr, config):
|
1101
|
+
pipeline_config = {k.lower(): v for k, v in config.items()}
|
1102
|
+
name = pipeline_config.pop('name', None)
|
1103
|
+
plugins = pipeline_config.pop('plugins', None)
|
1104
|
+
|
1105
|
+
pipeline = Pipeline(
|
1106
|
+
is_attached=True,
|
1107
|
+
session=self,
|
1108
|
+
log=self.log,
|
1109
|
+
node_addr=node_addr,
|
1110
|
+
name=name,
|
1111
|
+
plugins=plugins,
|
1112
|
+
existing_config=pipeline_config,
|
1113
|
+
)
|
1114
|
+
|
1115
|
+
return pipeline
|
1116
|
+
|
1117
|
+
# API
|
1118
|
+
if True:
|
1119
|
+
@ property
|
1120
|
+
def server(self):
|
1121
|
+
"""
|
1122
|
+
The hostname of the server.
|
1123
|
+
"""
|
1124
|
+
return self._config[comm_ct.HOST]
|
1125
|
+
|
1126
|
+
def create_pipeline(self, *,
|
1127
|
+
node,
|
1128
|
+
name,
|
1129
|
+
data_source="Void",
|
1130
|
+
config={},
|
1131
|
+
plugins=[],
|
1132
|
+
on_data=None,
|
1133
|
+
on_notification=None,
|
1134
|
+
max_wait_time=0,
|
1135
|
+
**kwargs) -> Pipeline:
|
1136
|
+
"""
|
1137
|
+
Create a new pipeline on a node. A pipeline is the equivalent of the "config file" used by the Naeural edge node team internally.
|
1138
|
+
|
1139
|
+
A `Pipeline` is a an object that encapsulates a one-to-many, data acquisition to data processing, flow of data.
|
1140
|
+
|
1141
|
+
A `Pipeline` contains one thread of data acquisition (which does not mean only one source of data), and many
|
1142
|
+
processing units, usually named `Plugins`.
|
1143
|
+
|
1144
|
+
An `Instance` is a running thread of a `Plugin` type, and one may want to have multiple `Instances`, because each can be configured independently.
|
1145
|
+
|
1146
|
+
As such, one will work with `Instances`, by referring to them with the unique identifier (Pipeline, Plugin, Instance).
|
1147
|
+
|
1148
|
+
In the documentation, the following refer to the same thing:
|
1149
|
+
`Pipeline` == `Stream`
|
1150
|
+
|
1151
|
+
`Plugin` == `Signature`
|
1152
|
+
|
1153
|
+
This call can busy-wait for a number of seconds to listen to heartbeats, in order to check if an Naeural edge node is online or not.
|
1154
|
+
If the node does not appear online, a warning will be displayed at the stdout, telling the user that the message that handles the
|
1155
|
+
creation of the pipeline will be sent, but it is not guaranteed that the specific node will receive it.
|
1156
|
+
|
1157
|
+
Parameters
|
1158
|
+
----------
|
1159
|
+
node : str
|
1160
|
+
Address or Name of the Naeural edge node that will handle this pipeline.
|
1161
|
+
name : str
|
1162
|
+
Name of the pipeline. This is good to be kept unique, as it allows multiple parties to overwrite each others configurations.
|
1163
|
+
data_source : str, optional
|
1164
|
+
This is the name of the DCT plugin, which resembles the desired functionality of the acquisition. Defaults to Void.
|
1165
|
+
config : dict, optional
|
1166
|
+
This is the dictionary that contains the configuration of the acquisition source, by default {}
|
1167
|
+
plugins : list, optional
|
1168
|
+
List of dictionaries which contain the configurations of each plugin instance that is desired to run on the box.
|
1169
|
+
Defaults to []. Should be left [], and instances should be created with the api.
|
1170
|
+
on_data : Callable[[Pipeline, str, str, dict], None], optional
|
1171
|
+
Callback that handles messages received from any plugin instance.
|
1172
|
+
As arguments, it has a reference to this Pipeline object, the signature and the instance of the plugin
|
1173
|
+
that sent the message and the payload itself.
|
1174
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1175
|
+
the user has defined a specific callback.
|
1176
|
+
Defaults to None.
|
1177
|
+
on_notification : Callable[[Pipeline, dict], None], optional
|
1178
|
+
Callback that handles notifications received from any plugin instance.
|
1179
|
+
As arguments, it has a reference to this Pipeline object, along with the payload itself.
|
1180
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1181
|
+
the user has defined a specific callback.
|
1182
|
+
Defaults to None.
|
1183
|
+
max_wait_time : int, optional
|
1184
|
+
The maximum time to busy-wait, allowing the Session object to listen to node heartbeats
|
1185
|
+
and to check if the desired node is online in the network, by default 0.
|
1186
|
+
**kwargs :
|
1187
|
+
The user can provide the configuration of the acquisition source directly as kwargs.
|
1188
|
+
|
1189
|
+
Returns
|
1190
|
+
-------
|
1191
|
+
Pipeline
|
1192
|
+
A `Pipeline` object.
|
1193
|
+
|
1194
|
+
"""
|
1195
|
+
|
1196
|
+
found = self.wait_for_node(node, timeout=max_wait_time, verbose=False)
|
1197
|
+
|
1198
|
+
if not found:
|
1199
|
+
raise Exception("Unable to attach to pipeline. Node does not exist")
|
1200
|
+
|
1201
|
+
node_addr = self.__get_node_address(node)
|
1202
|
+
pipeline = Pipeline(
|
1203
|
+
self,
|
1204
|
+
self.log,
|
1205
|
+
node_addr=node_addr,
|
1206
|
+
name=name,
|
1207
|
+
type=data_source,
|
1208
|
+
config=config,
|
1209
|
+
plugins=plugins,
|
1210
|
+
on_data=on_data,
|
1211
|
+
on_notification=on_notification,
|
1212
|
+
is_attached=False,
|
1213
|
+
**kwargs
|
1214
|
+
)
|
1215
|
+
self.own_pipelines.append(pipeline)
|
1216
|
+
return pipeline
|
1217
|
+
|
1218
|
+
def get_node_name(self, node_addr):
|
1219
|
+
"""
|
1220
|
+
Get the name of a node.
|
1221
|
+
|
1222
|
+
Parameters
|
1223
|
+
----------
|
1224
|
+
node_addr : str
|
1225
|
+
The address of the node.
|
1226
|
+
|
1227
|
+
Returns
|
1228
|
+
-------
|
1229
|
+
str
|
1230
|
+
The name of the node.
|
1231
|
+
"""
|
1232
|
+
return self._dct_node_addr_name.get(node_addr, None)
|
1233
|
+
|
1234
|
+
def get_active_nodes(self):
|
1235
|
+
"""
|
1236
|
+
Get the list of all Naeural edge nodes that sent a message since this session was created, and that are considered online
|
1237
|
+
|
1238
|
+
Returns
|
1239
|
+
-------
|
1240
|
+
list
|
1241
|
+
List of names of all the Naeural edge nodes that are considered online
|
1242
|
+
|
1243
|
+
"""
|
1244
|
+
return [k for k, v in self._dct_node_last_seen_time.items() if tm() - v < self.online_timeout]
|
1245
|
+
|
1246
|
+
def get_allowed_nodes(self):
|
1247
|
+
"""
|
1248
|
+
Get the list of all active Naeural edge nodes to whom this session can send messages
|
1249
|
+
|
1250
|
+
Returns
|
1251
|
+
-------
|
1252
|
+
list[str]
|
1253
|
+
List of names of all the active Naeural edge nodes to whom this session can send messages
|
1254
|
+
"""
|
1255
|
+
active_nodes = self.get_active_nodes()
|
1256
|
+
return [node for node in self._dct_can_send_to_node if self._dct_can_send_to_node[node] and node in active_nodes]
|
1257
|
+
|
1258
|
+
def get_active_pipelines(self, node):
|
1259
|
+
"""
|
1260
|
+
Get a dictionary with all the pipelines that are active on this Naeural edge node
|
1261
|
+
|
1262
|
+
Parameters
|
1263
|
+
----------
|
1264
|
+
node : str
|
1265
|
+
Address or Name of the Naeural edge node
|
1266
|
+
|
1267
|
+
Returns
|
1268
|
+
-------
|
1269
|
+
dict
|
1270
|
+
The key is the name of the pipeline, and the value is the entire config dictionary of that pipeline.
|
1271
|
+
|
1272
|
+
"""
|
1273
|
+
node_address = self.__get_node_address(node)
|
1274
|
+
return self._dct_online_nodes_pipelines.get(node_address, None)
|
1275
|
+
|
1276
|
+
def get_active_supervisors(self):
|
1277
|
+
"""
|
1278
|
+
Get the list of all active supervisors
|
1279
|
+
|
1280
|
+
Returns
|
1281
|
+
-------
|
1282
|
+
list
|
1283
|
+
List of names of all the active supervisors
|
1284
|
+
"""
|
1285
|
+
active_nodes = self.get_active_nodes()
|
1286
|
+
|
1287
|
+
active_supervisors = []
|
1288
|
+
for node in active_nodes:
|
1289
|
+
last_hb = self._dct_online_nodes_last_heartbeat.get(node, None)
|
1290
|
+
if last_hb is None:
|
1291
|
+
continue
|
1292
|
+
|
1293
|
+
if last_hb.get(PAYLOAD_DATA.IS_SUPERVISOR, False):
|
1294
|
+
active_supervisors.append(node)
|
1295
|
+
|
1296
|
+
return active_supervisors
|
1297
|
+
|
1298
|
+
def attach_to_pipeline(self, *,
|
1299
|
+
node,
|
1300
|
+
name,
|
1301
|
+
on_data=None,
|
1302
|
+
on_notification=None,
|
1303
|
+
max_wait_time=0) -> Pipeline:
|
1304
|
+
"""
|
1305
|
+
Create a Pipeline object and attach to an existing pipeline on an Naeural edge node.
|
1306
|
+
Useful when one wants to treat an existing pipeline as one of his own,
|
1307
|
+
or when one wants to attach callbacks to various events (on_data, on_notification).
|
1308
|
+
|
1309
|
+
A `Pipeline` is a an object that encapsulates a one-to-many, data acquisition to data processing, flow of data.
|
1310
|
+
|
1311
|
+
A `Pipeline` contains one thread of data acquisition (which does not mean only one source of data), and many
|
1312
|
+
processing units, usually named `Plugins`.
|
1313
|
+
|
1314
|
+
An `Instance` is a running thread of a `Plugin` type, and one may want to have multiple `Instances`, because each can be configured independently.
|
1315
|
+
|
1316
|
+
As such, one will work with `Instances`, by reffering to them with the unique identifier (Pipeline, Plugin, Instance).
|
1317
|
+
|
1318
|
+
In the documentation, the following reffer to the same thing:
|
1319
|
+
`Pipeline` == `Stream`
|
1320
|
+
|
1321
|
+
`Plugin` == `Signature`
|
1322
|
+
|
1323
|
+
This call can busy-wait for a number of seconds to listen to heartbeats, in order to check if an Naeural edge node is online or not.
|
1324
|
+
If the node does not appear online, a warning will be displayed at the stdout, telling the user that the message that handles the
|
1325
|
+
creation of the pipeline will be sent, but it is not guaranteed that the specific node will receive it.
|
1326
|
+
|
1327
|
+
|
1328
|
+
Parameters
|
1329
|
+
----------
|
1330
|
+
node : str
|
1331
|
+
Address or Name of the Naeural edge node that handles this pipeline.
|
1332
|
+
name : str
|
1333
|
+
Name of the existing pipeline.
|
1334
|
+
on_data : Callable[[Pipeline, str, str, dict], None], optional
|
1335
|
+
Callback that handles messages received from any plugin instance.
|
1336
|
+
As arguments, it has a reference to this Pipeline object, the signature and the instance of the plugin
|
1337
|
+
that sent the message and the payload itself.
|
1338
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1339
|
+
the user has defined a specific callback.
|
1340
|
+
Defaults to None.
|
1341
|
+
on_notification : Callable[[Pipeline, dict], None], optional
|
1342
|
+
Callback that handles notifications received from any plugin instance.
|
1343
|
+
As arguments, it has a reference to this Pipeline object, along with the payload itself.
|
1344
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1345
|
+
the user has defined a specific callback.
|
1346
|
+
Defaults to None.
|
1347
|
+
max_wait_time : int, optional
|
1348
|
+
The maximum time to busy-wait, allowing the Session object to listen to node heartbeats
|
1349
|
+
and to check if the desired node is online in the network, by default 0.
|
1350
|
+
|
1351
|
+
Returns
|
1352
|
+
-------
|
1353
|
+
Pipeline
|
1354
|
+
A `Pipeline` object.
|
1355
|
+
|
1356
|
+
Raises
|
1357
|
+
------
|
1358
|
+
Exception
|
1359
|
+
Node does not exist (it is considered offline because the session did not receive any heartbeat)
|
1360
|
+
Exception
|
1361
|
+
Node does not host the desired pipeline
|
1362
|
+
"""
|
1363
|
+
|
1364
|
+
found = self.wait_for_node(node, timeout=max_wait_time, verbose=False)
|
1365
|
+
|
1366
|
+
if not found:
|
1367
|
+
raise Exception("Unable to attach to pipeline. Node does not exist")
|
1368
|
+
|
1369
|
+
node_addr = self.__get_node_address(node)
|
1370
|
+
|
1371
|
+
if name not in self._dct_online_nodes_pipelines[node_addr]:
|
1372
|
+
raise Exception("Unable to attach to pipeline. Pipeline does not exist")
|
1373
|
+
|
1374
|
+
pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr][name]
|
1375
|
+
|
1376
|
+
if on_data is not None:
|
1377
|
+
pipeline._add_on_data_callback(on_data)
|
1378
|
+
if on_notification is not None:
|
1379
|
+
pipeline._add_on_notification_callback(on_notification)
|
1380
|
+
|
1381
|
+
self.own_pipelines.append(pipeline)
|
1382
|
+
|
1383
|
+
return pipeline
|
1384
|
+
|
1385
|
+
def create_or_attach_to_pipeline(self, *,
|
1386
|
+
node,
|
1387
|
+
name,
|
1388
|
+
data_source,
|
1389
|
+
config={},
|
1390
|
+
plugins=[],
|
1391
|
+
on_data=None,
|
1392
|
+
on_notification=None,
|
1393
|
+
max_wait_time=0,
|
1394
|
+
**kwargs) -> Pipeline:
|
1395
|
+
"""
|
1396
|
+
Create a new pipeline on a node, or attach to an existing pipeline on an Naeural edge node.
|
1397
|
+
|
1398
|
+
Parameters
|
1399
|
+
----------
|
1400
|
+
node : str
|
1401
|
+
Address or Name of the Naeural edge node that will handle this pipeline.
|
1402
|
+
name : str
|
1403
|
+
Name of the pipeline. This is good to be kept unique, as it allows multiple parties to overwrite each others configurations.
|
1404
|
+
data_source : str
|
1405
|
+
This is the name of the DCT plugin, which resembles the desired functionality of the acquisition.
|
1406
|
+
config : dict, optional
|
1407
|
+
This is the dictionary that contains the configuration of the acquisition source, by default {}
|
1408
|
+
plugins : list
|
1409
|
+
List of dictionaries which contain the configurations of each plugin instance that is desired to run on the box.
|
1410
|
+
Defaults to []. Should be left [], and instances should be created with the api.
|
1411
|
+
on_data : Callable[[Pipeline, str, str, dict], None], optional
|
1412
|
+
Callback that handles messages received from any plugin instance.
|
1413
|
+
As arguments, it has a reference to this Pipeline object, the signature and the instance of the plugin
|
1414
|
+
that sent the message and the payload itself.
|
1415
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1416
|
+
the user has defined a specific callback.
|
1417
|
+
Defaults to None.
|
1418
|
+
on_notification : Callable[[Pipeline, dict], None], optional
|
1419
|
+
Callback that handles notifications received from any plugin instance.
|
1420
|
+
As arguments, it has a reference to this Pipeline object, along with the payload itself.
|
1421
|
+
This callback acts as a default payload processor and will be called even if for a given instance
|
1422
|
+
the user has defined a specific callback.
|
1423
|
+
Defaults to None.
|
1424
|
+
max_wait_time : int, optional
|
1425
|
+
The maximum time to busy-wait, allowing the Session object to listen to node heartbeats
|
1426
|
+
and to check if the desired node is online in the network, by default 0.
|
1427
|
+
**kwargs :
|
1428
|
+
The user can provide the configuration of the acquisition source directly as kwargs.
|
1429
|
+
|
1430
|
+
Returns
|
1431
|
+
-------
|
1432
|
+
Pipeline
|
1433
|
+
A `Pipeline` object.
|
1434
|
+
"""
|
1435
|
+
|
1436
|
+
pipeline = None
|
1437
|
+
try:
|
1438
|
+
pipeline = self.attach_to_pipeline(
|
1439
|
+
node=node,
|
1440
|
+
name=name,
|
1441
|
+
on_data=on_data,
|
1442
|
+
on_notification=on_notification,
|
1443
|
+
max_wait_time=max_wait_time,
|
1444
|
+
)
|
1445
|
+
|
1446
|
+
possible_new_configuration = {
|
1447
|
+
**config,
|
1448
|
+
**{k.upper(): v for k, v in kwargs.items()}
|
1449
|
+
}
|
1450
|
+
|
1451
|
+
if len(plugins) > 0:
|
1452
|
+
possible_new_configuration['PLUGINS'] = plugins
|
1453
|
+
|
1454
|
+
if len(possible_new_configuration) > 0:
|
1455
|
+
pipeline.update_full_configuration(config=possible_new_configuration)
|
1456
|
+
except Exception as e:
|
1457
|
+
self.D("Failed to attach to pipeline: {}".format(e))
|
1458
|
+
pipeline = self.create_pipeline(
|
1459
|
+
node=node,
|
1460
|
+
name=name,
|
1461
|
+
data_source=data_source,
|
1462
|
+
config=config,
|
1463
|
+
plugins=plugins,
|
1464
|
+
on_data=on_data,
|
1465
|
+
on_notification=on_notification,
|
1466
|
+
**kwargs
|
1467
|
+
)
|
1468
|
+
|
1469
|
+
return pipeline
|
1470
|
+
|
1471
|
+
def wait_for_transactions(self, transactions: list[Transaction]):
|
1472
|
+
"""
|
1473
|
+
Wait for the transactions to be solved.
|
1474
|
+
|
1475
|
+
Parameters
|
1476
|
+
----------
|
1477
|
+
transactions : list[Transaction]
|
1478
|
+
The transactions to wait for.
|
1479
|
+
"""
|
1480
|
+
while not self.are_transactions_finished(transactions):
|
1481
|
+
sleep(0.1)
|
1482
|
+
return
|
1483
|
+
|
1484
|
+
def are_transactions_finished(self, transactions: list[Transaction]):
|
1485
|
+
if transactions is None:
|
1486
|
+
return True
|
1487
|
+
return all([transaction.is_finished() for transaction in transactions])
|
1488
|
+
|
1489
|
+
def wait_for_all_sets_of_transactions(self, lst_transactions: list[list[Transaction]]):
|
1490
|
+
"""
|
1491
|
+
Wait for all sets of transactions to be solved.
|
1492
|
+
|
1493
|
+
Parameters
|
1494
|
+
----------
|
1495
|
+
lst_transactions : list[list[Transaction]]
|
1496
|
+
The list of sets of transactions to wait for.
|
1497
|
+
"""
|
1498
|
+
all_finished = False
|
1499
|
+
while not all_finished:
|
1500
|
+
all_finished = all([self.are_transactions_finished(transactions) for transactions in lst_transactions])
|
1501
|
+
return
|
1502
|
+
|
1503
|
+
def wait_for_any_set_of_transactions(self, lst_transactions: list[list[Transaction]]):
|
1504
|
+
"""
|
1505
|
+
Wait for any set of transactions to be solved.
|
1506
|
+
|
1507
|
+
Parameters
|
1508
|
+
----------
|
1509
|
+
lst_transactions : list[list[Transaction]]
|
1510
|
+
The list of sets of transactions to wait for.
|
1511
|
+
"""
|
1512
|
+
any_finished = False
|
1513
|
+
while not any_finished:
|
1514
|
+
any_finished = any([self.are_transactions_finished(transactions) for transactions in lst_transactions])
|
1515
|
+
return
|
1516
|
+
|
1517
|
+
def wait_for_any_node(self, timeout=15, verbose=True):
|
1518
|
+
"""
|
1519
|
+
Wait for any node to appear online.
|
1520
|
+
|
1521
|
+
Parameters
|
1522
|
+
----------
|
1523
|
+
timeout : int, optional
|
1524
|
+
The timeout, by default 15
|
1525
|
+
|
1526
|
+
Returns
|
1527
|
+
-------
|
1528
|
+
bool
|
1529
|
+
True if any node is online, False otherwise.
|
1530
|
+
"""
|
1531
|
+
if verbose:
|
1532
|
+
self.P("Waiting for any node to appear online...")
|
1533
|
+
|
1534
|
+
_start = tm()
|
1535
|
+
found = len(self.get_active_nodes()) > 0
|
1536
|
+
while (tm() - _start) < timeout and not found:
|
1537
|
+
sleep(0.1)
|
1538
|
+
found = len(self.get_active_nodes()) > 0
|
1539
|
+
# end while
|
1540
|
+
|
1541
|
+
if verbose:
|
1542
|
+
if found:
|
1543
|
+
self.P("Found nodes {} online.".format(self.get_active_nodes()))
|
1544
|
+
else:
|
1545
|
+
self.P("No nodes found online in {:.1f}s.".format(tm() - _start), color='r')
|
1546
|
+
return found
|
1547
|
+
|
1548
|
+
def wait_for_node(self, node, /, timeout=15, verbose=True):
|
1549
|
+
"""
|
1550
|
+
Wait for a node to appear online.
|
1551
|
+
|
1552
|
+
Parameters
|
1553
|
+
----------
|
1554
|
+
node : str
|
1555
|
+
The address or name of the Naeural edge node.
|
1556
|
+
timeout : int, optional
|
1557
|
+
The timeout, by default 15
|
1558
|
+
|
1559
|
+
Returns
|
1560
|
+
-------
|
1561
|
+
bool
|
1562
|
+
True if the node is online, False otherwise.
|
1563
|
+
"""
|
1564
|
+
|
1565
|
+
if verbose:
|
1566
|
+
self.P("Waiting for node '{}' to appear online...".format(node))
|
1567
|
+
|
1568
|
+
_start = tm()
|
1569
|
+
found = self.check_node_online(node)
|
1570
|
+
while (tm() - _start) < timeout and not found:
|
1571
|
+
sleep(0.1)
|
1572
|
+
found = self.check_node_online(node)
|
1573
|
+
# end while
|
1574
|
+
|
1575
|
+
if verbose:
|
1576
|
+
if found:
|
1577
|
+
self.P("Node '{}' is online.".format(node))
|
1578
|
+
else:
|
1579
|
+
self.P("Node '{}' did not appear online in {:.1f}s.".format(node, tm() - _start), color='r')
|
1580
|
+
return found
|
1581
|
+
|
1582
|
+
def check_node_online(self, node, /):
|
1583
|
+
"""
|
1584
|
+
Check if a node is online.
|
1585
|
+
|
1586
|
+
Parameters
|
1587
|
+
----------
|
1588
|
+
node : str
|
1589
|
+
The address or name of the Naeural edge node.
|
1590
|
+
|
1591
|
+
Returns
|
1592
|
+
-------
|
1593
|
+
bool
|
1594
|
+
True if the node is online, False otherwise.
|
1595
|
+
"""
|
1596
|
+
return node in self.get_active_nodes() or node in self._dct_node_addr_name.values()
|
1597
|
+
|
1598
|
+
def create_chain_dist_custom_job(
|
1599
|
+
self,
|
1600
|
+
main_node_process_real_time_collected_data,
|
1601
|
+
main_node_finish_condition,
|
1602
|
+
main_node_finish_condition_kwargs,
|
1603
|
+
main_node_aggregate_collected_data,
|
1604
|
+
worker_node_code,
|
1605
|
+
nr_remote_worker_nodes,
|
1606
|
+
node=None,
|
1607
|
+
worker_node_plugin_config={},
|
1608
|
+
worker_node_pipeline_config={},
|
1609
|
+
on_data=None,
|
1610
|
+
on_notification=None,
|
1611
|
+
deploy=False,
|
1612
|
+
):
|
1613
|
+
|
1614
|
+
pipeline: Pipeline = self.create_pipeline(
|
1615
|
+
node=node,
|
1616
|
+
name=self.log.get_unique_id(),
|
1617
|
+
data_source="Void"
|
1618
|
+
)
|
1619
|
+
|
1620
|
+
instance = pipeline.create_chain_dist_custom_plugin_instance(
|
1621
|
+
main_node_process_real_time_collected_data=main_node_process_real_time_collected_data,
|
1622
|
+
main_node_finish_condition=main_node_finish_condition,
|
1623
|
+
finish_condition_kwargs=main_node_finish_condition_kwargs,
|
1624
|
+
main_node_aggregate_collected_data=main_node_aggregate_collected_data,
|
1625
|
+
worker_node_code=worker_node_code,
|
1626
|
+
nr_remote_worker_nodes=nr_remote_worker_nodes,
|
1627
|
+
worker_node_plugin_config=worker_node_plugin_config,
|
1628
|
+
worker_node_pipeline_config=worker_node_pipeline_config,
|
1629
|
+
on_data=on_data,
|
1630
|
+
on_notification=on_notification,
|
1631
|
+
)
|
1632
|
+
|
1633
|
+
if deploy:
|
1634
|
+
pipeline.deploy()
|
1635
|
+
|
1636
|
+
return pipeline, instance
|
1637
|
+
|
1638
|
+
def create_web_app(
|
1639
|
+
self,
|
1640
|
+
*,
|
1641
|
+
node,
|
1642
|
+
name,
|
1643
|
+
signature,
|
1644
|
+
**kwargs
|
1645
|
+
):
|
1646
|
+
|
1647
|
+
pipeline: Pipeline = self.create_pipeline(
|
1648
|
+
node=node,
|
1649
|
+
name=name,
|
1650
|
+
)
|
1651
|
+
|
1652
|
+
instance = pipeline.create_plugin_instance(
|
1653
|
+
signature=signature,
|
1654
|
+
instance_id=self.log.get_unique_id(),
|
1655
|
+
**kwargs
|
1656
|
+
)
|
1657
|
+
|
1658
|
+
return pipeline, instance
|
1659
|
+
|
1660
|
+
def broadcast_instance_command_and_wait_for_response_payload(
|
1661
|
+
self,
|
1662
|
+
instances,
|
1663
|
+
require_responses_mode="any",
|
1664
|
+
command={},
|
1665
|
+
payload=None,
|
1666
|
+
command_params=None,
|
1667
|
+
timeout=10,
|
1668
|
+
response_params_key="COMMAND_PARAMS"
|
1669
|
+
):
|
1670
|
+
# """
|
1671
|
+
# Send a command to multiple instances and wait for the responses.
|
1672
|
+
# This method can wait until any or all of the instances respond.
|
1673
|
+
|
1674
|
+
# """
|
1675
|
+
"""
|
1676
|
+
Send a command to multiple instances and wait for the responses.
|
1677
|
+
This method can wait until any or all of the instances respond.
|
1678
|
+
|
1679
|
+
Parameters
|
1680
|
+
----------
|
1681
|
+
|
1682
|
+
instances : list[Instance]
|
1683
|
+
The list of instances to send the command to.
|
1684
|
+
require_responses_mode : str, optional
|
1685
|
+
The mode to wait for the responses. Can be 'any' or 'all'.
|
1686
|
+
Defaults to 'any'.
|
1687
|
+
command : str | dict, optional
|
1688
|
+
The command to send. Defaults to {}.
|
1689
|
+
payload : dict, optional
|
1690
|
+
The payload to send. This contains metadata, not used by the Edge Node. Defaults to None.
|
1691
|
+
command_params : dict, optional
|
1692
|
+
The command parameters. Can be instead of `command`. Defaults to None.
|
1693
|
+
timeout : int, optional
|
1694
|
+
The timeout in seconds. Defaults to 10.
|
1695
|
+
response_params_key : str, optional
|
1696
|
+
The key in the response that contains the response parameters.
|
1697
|
+
Defaults to 'COMMAND_PARAMS'.
|
1698
|
+
|
1699
|
+
Returns
|
1700
|
+
-------
|
1701
|
+
response_payload : Payload
|
1702
|
+
The response payload.
|
1703
|
+
"""
|
1704
|
+
|
1705
|
+
if len(instances) == 0:
|
1706
|
+
self.P("Warning! No instances provided.", color='r', verbosity=1)
|
1707
|
+
return None
|
1708
|
+
|
1709
|
+
lst_result_payload = [None] * len(instances)
|
1710
|
+
uid = self.log.get_uid()
|
1711
|
+
|
1712
|
+
def wait_payload_on_data(pos):
|
1713
|
+
def custom_func(pipeline, data):
|
1714
|
+
nonlocal lst_result_payload, pos
|
1715
|
+
if response_params_key in data and data[response_params_key].get("SDK_REQUEST") == uid:
|
1716
|
+
lst_result_payload[pos] = data
|
1717
|
+
return
|
1718
|
+
# end def custom_func
|
1719
|
+
return custom_func
|
1720
|
+
# end def wait_payload_on_data
|
1721
|
+
|
1722
|
+
lst_attachment_instance = []
|
1723
|
+
for i, instance in enumerate(instances):
|
1724
|
+
attachment = instance.temporary_attach(on_data=wait_payload_on_data(i))
|
1725
|
+
lst_attachment_instance.append((attachment, instance))
|
1726
|
+
# end for
|
1727
|
+
|
1728
|
+
if payload is None:
|
1729
|
+
payload = {}
|
1730
|
+
payload["SDK_REQUEST"] = uid
|
1731
|
+
|
1732
|
+
lst_instance_transactions = []
|
1733
|
+
for instance in instances:
|
1734
|
+
instance_transactions = instance.send_instance_command(
|
1735
|
+
command=command,
|
1736
|
+
payload=payload,
|
1737
|
+
command_params=command_params,
|
1738
|
+
wait_confirmation=False,
|
1739
|
+
timeout=timeout,
|
1740
|
+
)
|
1741
|
+
lst_instance_transactions.append(instance_transactions)
|
1742
|
+
# end for send commands
|
1743
|
+
|
1744
|
+
if require_responses_mode == "all":
|
1745
|
+
self.wait_for_all_sets_of_transactions(lst_instance_transactions)
|
1746
|
+
elif require_responses_mode == "any":
|
1747
|
+
self.wait_for_any_set_of_transactions(lst_instance_transactions)
|
1748
|
+
|
1749
|
+
start_time = tm()
|
1750
|
+
|
1751
|
+
condition_all = any([x is None for x in lst_result_payload]) and require_responses_mode == "all"
|
1752
|
+
condition_any = all([x is None for x in lst_result_payload]) and require_responses_mode == "any"
|
1753
|
+
while tm() - start_time < 3 and (condition_all or condition_any):
|
1754
|
+
sleep(0.1)
|
1755
|
+
condition_all = any([x is None for x in lst_result_payload]) and require_responses_mode == "all"
|
1756
|
+
condition_any = all([x is None for x in lst_result_payload]) and require_responses_mode == "any"
|
1757
|
+
# end while
|
1758
|
+
|
1759
|
+
for attachment, instance in lst_attachment_instance:
|
1760
|
+
instance.temporary_detach(attachment)
|
1761
|
+
# end for detach
|
1762
|
+
|
1763
|
+
return lst_result_payload
|