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.
Files changed (78) hide show
  1. naeural_client/__init__.py +13 -0
  2. naeural_client/_ver.py +13 -0
  3. naeural_client/base/__init__.py +6 -0
  4. naeural_client/base/distributed_custom_code_presets.py +44 -0
  5. naeural_client/base/generic_session.py +1763 -0
  6. naeural_client/base/instance.py +616 -0
  7. naeural_client/base/payload/__init__.py +1 -0
  8. naeural_client/base/payload/payload.py +66 -0
  9. naeural_client/base/pipeline.py +1499 -0
  10. naeural_client/base/plugin_template.py +5209 -0
  11. naeural_client/base/responses.py +209 -0
  12. naeural_client/base/transaction.py +157 -0
  13. naeural_client/base_decentra_object.py +143 -0
  14. naeural_client/bc/__init__.py +3 -0
  15. naeural_client/bc/base.py +1046 -0
  16. naeural_client/bc/chain.py +0 -0
  17. naeural_client/bc/ec.py +324 -0
  18. naeural_client/certs/__init__.py +0 -0
  19. naeural_client/certs/r9092118.ala.eu-central-1.emqxsl.com.crt +22 -0
  20. naeural_client/code_cheker/__init__.py +1 -0
  21. naeural_client/code_cheker/base.py +520 -0
  22. naeural_client/code_cheker/checker.py +294 -0
  23. naeural_client/comm/__init__.py +2 -0
  24. naeural_client/comm/amqp_wrapper.py +338 -0
  25. naeural_client/comm/mqtt_wrapper.py +539 -0
  26. naeural_client/const/README.md +3 -0
  27. naeural_client/const/__init__.py +9 -0
  28. naeural_client/const/base.py +101 -0
  29. naeural_client/const/comms.py +80 -0
  30. naeural_client/const/environment.py +26 -0
  31. naeural_client/const/formatter.py +7 -0
  32. naeural_client/const/heartbeat.py +111 -0
  33. naeural_client/const/misc.py +20 -0
  34. naeural_client/const/payload.py +190 -0
  35. naeural_client/default/__init__.py +1 -0
  36. naeural_client/default/instance/__init__.py +4 -0
  37. naeural_client/default/instance/chain_dist_custom_job_01_plugin.py +54 -0
  38. naeural_client/default/instance/custom_web_app_01_plugin.py +118 -0
  39. naeural_client/default/instance/net_mon_01_plugin.py +45 -0
  40. naeural_client/default/instance/view_scene_01_plugin.py +28 -0
  41. naeural_client/default/session/mqtt_session.py +72 -0
  42. naeural_client/io_formatter/__init__.py +2 -0
  43. naeural_client/io_formatter/base/__init__.py +1 -0
  44. naeural_client/io_formatter/base/base_formatter.py +80 -0
  45. naeural_client/io_formatter/default/__init__.py +3 -0
  46. naeural_client/io_formatter/default/a_dummy.py +51 -0
  47. naeural_client/io_formatter/default/aixp1.py +113 -0
  48. naeural_client/io_formatter/default/default.py +22 -0
  49. naeural_client/io_formatter/io_formatter_manager.py +96 -0
  50. naeural_client/logging/__init__.py +1 -0
  51. naeural_client/logging/base_logger.py +2056 -0
  52. naeural_client/logging/logger_mixins/__init__.py +12 -0
  53. naeural_client/logging/logger_mixins/class_instance_mixin.py +92 -0
  54. naeural_client/logging/logger_mixins/computer_vision_mixin.py +443 -0
  55. naeural_client/logging/logger_mixins/datetime_mixin.py +344 -0
  56. naeural_client/logging/logger_mixins/download_mixin.py +421 -0
  57. naeural_client/logging/logger_mixins/general_serialization_mixin.py +242 -0
  58. naeural_client/logging/logger_mixins/json_serialization_mixin.py +481 -0
  59. naeural_client/logging/logger_mixins/pickle_serialization_mixin.py +301 -0
  60. naeural_client/logging/logger_mixins/process_mixin.py +63 -0
  61. naeural_client/logging/logger_mixins/resource_size_mixin.py +81 -0
  62. naeural_client/logging/logger_mixins/timers_mixin.py +501 -0
  63. naeural_client/logging/logger_mixins/upload_mixin.py +260 -0
  64. naeural_client/logging/logger_mixins/utils_mixin.py +675 -0
  65. naeural_client/logging/small_logger.py +93 -0
  66. naeural_client/logging/tzlocal/__init__.py +20 -0
  67. naeural_client/logging/tzlocal/unix.py +231 -0
  68. naeural_client/logging/tzlocal/utils.py +113 -0
  69. naeural_client/logging/tzlocal/win32.py +151 -0
  70. naeural_client/logging/tzlocal/windows_tz.py +718 -0
  71. naeural_client/plugins_manager_mixin.py +273 -0
  72. naeural_client/utils/__init__.py +2 -0
  73. naeural_client/utils/comm_utils.py +44 -0
  74. naeural_client/utils/dotenv.py +75 -0
  75. naeural_client-2.0.0.dist-info/METADATA +365 -0
  76. naeural_client-2.0.0.dist-info/RECORD +78 -0
  77. naeural_client-2.0.0.dist-info/WHEEL +4 -0
  78. 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