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,1499 @@
1
+ # TODO: for custom plugin, do the plugin verification locally too
2
+
3
+ import os
4
+ from time import sleep, time
5
+
6
+ from ..code_cheker.base import BaseCodeChecker
7
+ from ..const import PAYLOAD_DATA
8
+ from .distributed_custom_code_presets import DistributedCustomCodePresets
9
+ from .instance import Instance
10
+ from .responses import PipelineArchiveResponse, PipelineOKResponse
11
+ from .transaction import Transaction
12
+
13
+
14
+ class Pipeline(BaseCodeChecker):
15
+ """
16
+ A `Pipeline` is a an object that encapsulates a one-to-many, data acquisition to data processing, flow of data.
17
+
18
+ A `Pipeline` contains one thread of data acquisition (which does not mean only one source of data), and many
19
+ processing units, usually named `Plugins`.
20
+
21
+ An `Instance` is a running thread of a `Plugin` type, and one may want to have multiple `Instances`, because each can be configured independently.
22
+
23
+ As such, one will work with `Instances`, by reffering to them with the unique identifier (Pipeline, Plugin, Instance).
24
+
25
+ In the documentation, the following reffer to the same thing:
26
+ `Pipeline` == `Stream`
27
+
28
+ `Plugin` == `Signature`
29
+ """
30
+
31
+ def __init__(self, session, log, *, node_addr, name, config={}, plugins=[], on_data=None, on_notification=None, is_attached=False, existing_config=None, **kwargs) -> None:
32
+ """
33
+ A `Pipeline` is a an object that encapsulates a one-to-many, data acquisition to data processing, flow of data.
34
+
35
+ A `Pipeline` contains one thread of data acquisition (which does not mean only one source of data), and many
36
+ processing units, usually named `Plugins`.
37
+
38
+ An `Instance` is a running thread of a `Plugin` type, and one may want to have multiple `Instances`, because each can be configured independently.
39
+
40
+ As such, one will work with `Instances`, by referring to them with the unique identifier (Pipeline, Plugin, Instance).
41
+
42
+ In the documentation, the following refer to the same thing:
43
+ `Pipeline` == `Stream`
44
+
45
+ `Plugin` == `Signature`
46
+
47
+ Parameters
48
+ ----------
49
+ session : Session
50
+ The Session object which owns this pipeline. A pipeline must be attached to a Session because that is the only
51
+ way the `on_X` callbacks are called
52
+ log : Logger
53
+ A logger object which implements basic logging functionality and some other utils stuff. Can be ignored for now.
54
+ In the future, the documentation for the Logger base class will be available and developers will be able to use
55
+ custom-made Loggers.
56
+ node_addr : str
57
+ Address of the Naeural edge node that will handle this pipeline.
58
+ name : str
59
+ The name of this pipeline.
60
+ data_source : str
61
+ This is the name of the DCT plugin, which resembles the desired functionality of the acquisition.
62
+ config : dict, optional
63
+ This is the dictionary that contains the configuration of the acquisition source, by default {}
64
+ plugins : List | None, optional
65
+ This is the list with manually configured business plugins that will be in the pipeline at creation time.
66
+ We recommend to leave this as `[]` or as `None` and use the API to create plugin instances.
67
+ on_data : Callable[[Pipeline, str, str, dict], None], optional,
68
+ Callback that handles messages received from any plugin instance.
69
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
70
+ This callback acts as a default payload processor and will be called even if for a given instance
71
+ the user has defined a specific callback.
72
+ Defaults to None.
73
+ on_notification : Callable[[Pipeline, dict], None], optional
74
+ Callback that handles notifications received from this instance.
75
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
76
+ This callback acts as a default payload processor and will be called even if for a given instance
77
+ the user has defined a specific callback.
78
+ Defaults to None.
79
+ is_attached : bool
80
+ This is used internally to allow the user to create or attach to a pipeline, and then use the same
81
+ objects in the same way, by default True
82
+ **kwargs : dict
83
+ The user can provide the configuration of the acquisition source directly as kwargs.
84
+ """
85
+ self.log = log
86
+ self.session = session
87
+ self.node_addr = node_addr
88
+ self.name = name
89
+
90
+ self.config = {}
91
+ plugins = config.pop('PLUGINS', plugins)
92
+
93
+ if is_attached:
94
+ assert existing_config is not None, "When attaching to a pipeline, the existing configuration should be found in the heartbeat of the Naeural edge node."
95
+ assert config == {}, "Cannot provide a configuration when attaching to a pipeline."
96
+ assert len(kwargs) == 0, "Cannot provide a configuration when attaching to a pipeline."
97
+ self.config = {k.upper(): v for k, v in existing_config.items()}
98
+ self.config = self.__pop_ignored_keys_from_config(self.config)
99
+ self.proposed_config = None
100
+ else:
101
+ self.proposed_config = {**config, **kwargs}
102
+ self.proposed_config = {k.upper(): v for k, v in self.proposed_config.items()}
103
+ self.proposed_config = self.__pop_ignored_keys_from_config(self.proposed_config)
104
+ self.__staged_config = None
105
+
106
+ self.__was_last_operation_successful = None
107
+
108
+ self.proposed_remove_instances = []
109
+ self.__staged_remove_instances = []
110
+
111
+ self.on_data_callbacks = []
112
+ self.on_notification_callbacks = []
113
+
114
+ if on_data is not None:
115
+ self.on_data_callbacks.append(on_data)
116
+ if on_notification is not None:
117
+ self.on_notification_callbacks.append(on_notification)
118
+
119
+ self.lst_plugin_instances: list[Instance] = []
120
+
121
+ self.__init_plugins(plugins, is_attached)
122
+ return
123
+
124
+ # Utils
125
+ if True:
126
+ def __init_instance(self, signature, instance_id, config, on_data, on_notification, is_attached):
127
+ instance_class = None
128
+ str_signature = None
129
+ if isinstance(signature, str):
130
+ instance_class = Instance
131
+ str_signature = signature.upper()
132
+ else:
133
+ instance_class = signature
134
+ str_signature = instance_class.signature.upper()
135
+ instance = instance_class(self.log,
136
+ pipeline=self,
137
+ signature=str_signature,
138
+ instance_id=instance_id,
139
+ config=config,
140
+ on_data=on_data,
141
+ on_notification=on_notification,
142
+ is_attached=is_attached
143
+ )
144
+ self.lst_plugin_instances.append(instance)
145
+ return instance
146
+
147
+ def __init_plugins(self, plugins, is_attached):
148
+ """
149
+ Initialize the plugins list. This method is called at the creation of the pipeline and is used to create the instances of the plugins that are part of the pipeline.
150
+
151
+ Parameters
152
+ ----------
153
+ plugins : List | None
154
+ The list of plugins, as they are found in the pipeline configuration dictionary in the heartbeat.
155
+ is_attached : bool
156
+ This is used internally to allow the user to create or attach to a pipeline, and then use the same objects in the same way.
157
+
158
+ """
159
+ if plugins is None:
160
+ return
161
+
162
+ for dct_signature_instances in plugins:
163
+ signature = dct_signature_instances['SIGNATURE']
164
+ instances = dct_signature_instances['INSTANCES']
165
+ for dct_instance in instances:
166
+ config = {k.upper(): v for k, v in dct_instance.items()}
167
+ instance_id = config.pop('INSTANCE_ID')
168
+ self.__init_instance(signature, instance_id, config, None, None, is_attached=is_attached)
169
+ # end for dct_instance
170
+ # end for dct_signature_instances
171
+ return
172
+
173
+ def __get_proposed_pipeline_config(self):
174
+ """
175
+ Construct the proposed pipeline configuration dictionary.
176
+
177
+ Returns
178
+ -------
179
+ dict
180
+ The proposed pipeline configuration dictionary.
181
+ """
182
+
183
+ plugin_dict = self.__construct_plugins_dictionary(skip_instances=self.proposed_remove_instances)
184
+
185
+ plugins_list = []
186
+ for signature, instances in plugin_dict.items():
187
+ plugins_list.append({
188
+ 'SIGNATURE': signature,
189
+ 'INSTANCES': [instance._get_proposed_config_dictionary(full=True) for instance in instances]
190
+ })
191
+
192
+ proposed_pipeline_config = {
193
+ 'NAME': self.name,
194
+ 'DEFAULT_PLUGIN': False,
195
+ 'PLUGINS': plugins_list,
196
+ **self.config,
197
+ **(self.proposed_config or {}),
198
+ }
199
+ return proposed_pipeline_config
200
+
201
+ def __register_transactions_for_update(self, session_id: str = None, timeout: float = 0) -> list[Transaction]:
202
+ """
203
+ Register transactions for updating the pipeline and instances configuration.
204
+ This method is called before sending an update pipeline configuration command to the Naeural edge node.
205
+
206
+ Parameters
207
+ ----------
208
+ session_id : str, optional
209
+ The session id. A unique id for the session. Defaults to None.
210
+
211
+ timeout : int, optional
212
+ The timeout for the transaction. Defaults to 0.
213
+
214
+ Returns
215
+ -------
216
+ transactions : list[Transaction]
217
+ The list of transactions generated.
218
+ """
219
+ transactions = []
220
+
221
+ # TODO: add different responses for different states of the plugin
222
+ # TODO: maybe this should be introduced as "pre-defined" business plugins, based on a schema
223
+ # and the user can specify them when creating the pipeline
224
+
225
+ for instance in self.lst_plugin_instances:
226
+ if instance._is_tainted():
227
+ transactions.append(self.session._register_transaction(
228
+ session_id=session_id,
229
+ lst_required_responses=instance._get_instance_update_required_responses(),
230
+ timeout=timeout,
231
+ on_success_callback=instance._apply_staged_config,
232
+ # TODO: if the instance was newly added, remove it from the tracked list
233
+ on_failure_callback=instance._discard_staged_config,
234
+ ))
235
+ # end for register to update instances
236
+
237
+ for instance in self.proposed_remove_instances:
238
+ transactions.append(self.session._register_transaction(
239
+ session_id=session_id,
240
+ lst_required_responses=instance._get_instance_remove_required_responses(),
241
+ timeout=timeout,
242
+ on_success_callback=lambda: self.__apply_staged_remove_instance(instance),
243
+ on_failure_callback=lambda fail_reason: self.__discard_staged_remove_instance(instance, fail_reason),
244
+ ))
245
+ # end for register to remove instances
246
+
247
+ if self.proposed_config is not None:
248
+ required_responses = [
249
+ PipelineOKResponse(self.node_id, self.name),
250
+ ]
251
+ transactions.append(self.session._register_transaction(
252
+ session_id=session_id,
253
+ lst_required_responses=required_responses,
254
+ timeout=timeout,
255
+ on_success_callback=self.__apply_staged_config,
256
+ on_failure_callback=self.__discard_staged_config,
257
+ ))
258
+
259
+ return transactions
260
+
261
+ def __register_transactions_for_delete(self, session_id: str = None, timeout: float = 0) -> list[Transaction]:
262
+ """
263
+ Register transactions for deleting the pipeline.
264
+ This method is called before sending a delete pipeline command to the Naeural edge node.
265
+
266
+ Parameters
267
+ ----------
268
+ session_id : str, optional
269
+ The session id. A unique id for the session. Defaults to None.
270
+
271
+ timeout : int, optional
272
+ The timeout for the transaction. Defaults to 0.
273
+
274
+ Returns
275
+ -------
276
+ transactions : list[Transaction]
277
+ The list of transactions generated.
278
+ """
279
+ transactions = []
280
+
281
+ required_responses = [
282
+ PipelineArchiveResponse(self.node_id, self.name),
283
+ ]
284
+ transactions.append(self.session._register_transaction(
285
+ session_id=session_id,
286
+ lst_required_responses=required_responses,
287
+ timeout=timeout,
288
+ on_success_callback=self.__set_last_operation_successful,
289
+ on_failure_callback=self.__set_last_operation_failed,
290
+ ))
291
+
292
+ return transactions
293
+
294
+ def __register_transaction_for_pipeline_command(self, session_id: str = None, timeout: float = 0) -> list[Transaction]:
295
+ """
296
+ Register a transaction for a pipeline command.
297
+ This method is called before sending a pipeline command to the Naeural edge node.
298
+
299
+ Parameters
300
+ ----------
301
+ session_id : str, optional
302
+ The session id. A unique id for the session. Defaults to None.
303
+
304
+ timeout : int, optional
305
+ The timeout for the transaction. Defaults to 0.
306
+
307
+ Returns
308
+ -------
309
+ transactions : list[Transaction]
310
+ The list of transactions generated.
311
+ """
312
+ transactions = []
313
+
314
+ # TODO: implement
315
+ self.__set_last_operation_successful()
316
+
317
+ return transactions
318
+
319
+ def __construct_plugins_list(self):
320
+ """
321
+ Construct the plugins list that will be in the pipeline configuration dictionary.
322
+
323
+ Returns
324
+ -------
325
+ list
326
+ The plugins list that will be in the pipeline configuration dictionary.
327
+ """
328
+ plugins = []
329
+ dct_signature_instances = {}
330
+ for instance in self.lst_plugin_instances:
331
+ signature = instance.signature
332
+ if instance.signature not in dct_signature_instances:
333
+ dct_signature_instances[instance.signature] = []
334
+ dct_signature_instances[instance.signature].append(instance)
335
+ # end for construct dct_signature_instances
336
+
337
+ for signature, instances in dct_signature_instances.items():
338
+ plugins.append({
339
+ 'SIGNATURE': signature,
340
+ 'INSTANCES': [instance._get_config_dictionary() for instance in instances]
341
+ })
342
+ # end for construct plugins list
343
+ return plugins
344
+
345
+ def __construct_plugins_dictionary(self, skip_instances=None):
346
+ """
347
+ Construct the plugins dictionary that will be in the pipeline configuration dictionary.
348
+
349
+ Returns
350
+ -------
351
+ dict
352
+ The plugins dictionary that will be in the pipeline configuration dictionary.
353
+ """
354
+ # plugins = []
355
+ skip_instances = skip_instances or []
356
+ dct_signature_instances = {}
357
+ for instance in self.lst_plugin_instances:
358
+ if instance in skip_instances:
359
+ continue
360
+ if instance.signature not in dct_signature_instances:
361
+ dct_signature_instances[instance.signature] = []
362
+ dct_signature_instances[instance.signature].append(instance)
363
+ # end for construct dct_signature_instances
364
+
365
+ return dct_signature_instances
366
+
367
+ def __send_update_config_to_box(self, session_id=None):
368
+ """
369
+ Send an update pipeline configuration command to the Naeural edge node.
370
+ """
371
+ self.session._send_command_update_pipeline_config(
372
+ worker=self.node_addr,
373
+ pipeline_config=self.__get_proposed_pipeline_config(),
374
+ session_id=session_id
375
+ )
376
+ return
377
+
378
+ def __batch_update_instances(self, lst_instances, session_id=None):
379
+ """
380
+ Update the configuration of multiple instances at once.
381
+ ```
382
+
383
+ Parameters
384
+ ----------
385
+ lst_updates : List[Instance]
386
+ A list of instances.
387
+ """
388
+ lst_updates = []
389
+
390
+ for instance in lst_instances:
391
+ lst_updates.append({
392
+ PAYLOAD_DATA.NAME: self.name,
393
+ PAYLOAD_DATA.SIGNATURE: instance.signature,
394
+ PAYLOAD_DATA.INSTANCE_ID: instance.instance_id,
395
+ PAYLOAD_DATA.INSTANCE_CONFIG: instance._get_proposed_config_dictionary(full=False)
396
+ })
397
+
398
+ self.session._send_command_batch_update_instance_config(
399
+ worker=self.node_addr,
400
+ lst_updates=lst_updates,
401
+ session_id=session_id
402
+ )
403
+
404
+ def __pop_ignored_keys_from_config(self, config):
405
+ """
406
+ Pop the ignored keys from the configuration.
407
+
408
+ Parameters
409
+ ----------
410
+ config : dict
411
+ The configuration dictionary.
412
+
413
+ Returns
414
+ -------
415
+ dict
416
+ The configuration dictionary without the ignored keys.
417
+ """
418
+ ignored_keys = ["INITIATOR_ADDR", "INITIATOR_ID", "LAST_UPDATE_TIME", "MODIFIED_BY_ADDR", "MODIFIED_BY_ID"]
419
+ return {k: v for k, v in config.items() if k not in ignored_keys}
420
+
421
+ def __get_instance_object(self, signature, instance_id):
422
+ """
423
+ Get the instance object by signature and instance id.
424
+
425
+ Parameters
426
+ ----------
427
+ signature : str
428
+ The signature of the plugin.
429
+ instance_id : str
430
+ The name of the instance.
431
+
432
+ Returns
433
+ -------
434
+ Instance
435
+ The instance object.
436
+ """
437
+ for instance in self.lst_plugin_instances:
438
+ if instance.signature == signature and instance.instance_id == instance_id:
439
+ return instance
440
+ return None
441
+
442
+ def __set_last_operation_successful(self):
443
+ """
444
+ Set the last operation successful.
445
+ """
446
+ self.__was_last_operation_successful = True
447
+ return
448
+
449
+ def __set_last_operation_failed(self, fail_reason):
450
+ """
451
+ Set the last operation failed.
452
+ """
453
+ self.__was_last_operation_successful = False
454
+ return
455
+
456
+ @staticmethod
457
+ def __custom_exec_on_data(self, instance_id, on_data_callback, data):
458
+ """
459
+ Handle the data received from a custom execution instance. This method is called by the Session object when a message is received from a custom execution instance.
460
+
461
+ Parameters
462
+ ----------
463
+ instance_id : str
464
+ The name of the instance that sent the message.
465
+ on_data_callback : Callable[[Pipeline, dict, dict], None]
466
+ The callback that handles the message. The first dict is the payload, and the second dict is the entire message.
467
+ data : dict | Payload
468
+ The payload of the message.
469
+ """
470
+ # TODO: use formatter for this message
471
+ # TODO: expose the other fields from data
472
+ exec_data = None
473
+
474
+ exec_data = data.get('EXEC_RESULT', data.get('EXEC_INFO'))
475
+ exec_error = data.get('EXEC_ERRORS', 'no keyword error')
476
+
477
+ if exec_error is not None:
478
+ self.P("Error received from <CUSTOM_EXEC_01:{}>: {}".format(instance_id, exec_error), color="r", verbosity=1)
479
+ if exec_data is not None:
480
+ on_data_callback(self, exec_data, data)
481
+ return
482
+
483
+ def __apply_staged_remove_instance(self, instance: Instance):
484
+ """
485
+ Remove an instance from the pipeline.
486
+
487
+ Parameters
488
+ ----------
489
+ instance : Instance
490
+ The instance to be removed.
491
+ """
492
+ instance.config = None
493
+ try:
494
+ self.__staged_remove_instances.remove(instance)
495
+ except:
496
+ self.P("Attempted to remove instance <{}:{}>, but it was not found in the staged remove list. "
497
+ "Most likely the instance deletion used `with_confirmation=False`".format(
498
+ instance.signature, instance.instance_id), color="r")
499
+ return
500
+
501
+ def __discard_staged_remove_instance(self, instance: Instance, fail_reason: str):
502
+ """
503
+ Discard the removal of an instance from the pipeline.
504
+
505
+ Parameters
506
+ ----------
507
+ instance : Instance
508
+ The instance to be removed.
509
+ """
510
+
511
+ self.P(
512
+ f"Discarding staged removal of instance <{instance.signature}:{instance.instance_id}>. Reason: {fail_reason}", color="r")
513
+
514
+ try:
515
+ self.__staged_remove_instances.remove(instance)
516
+ except:
517
+ self.P("Attempted to remove instance <{}:{}>, but it was not found in the staged remove list. "
518
+ "Most likely the instance deletion used `with_confirmation=False`".format(
519
+ instance.signature, instance.instance_id), color="r")
520
+
521
+ self.lst_plugin_instances.append(instance)
522
+ return
523
+
524
+ def __apply_staged_config(self, verbose=False):
525
+ """
526
+ Apply the staged configuration to the pipeline.
527
+ """
528
+ if self.__staged_config is None:
529
+ return
530
+
531
+ if verbose:
532
+ self.P("Deployed pipeline <{}> on <{}>".format(self.name, self.node_addr), color="g")
533
+ self.__was_last_operation_successful = True
534
+
535
+ self.config = {**self.config, **self.__staged_config}
536
+ self.__staged_config = None
537
+
538
+ return
539
+
540
+ def __apply_staged_instances_config(self, verbose=False):
541
+ """
542
+ Apply the staged configuration to the instances.
543
+ """
544
+ for instance in self.lst_plugin_instances:
545
+ instance._apply_staged_config(verbose=verbose)
546
+
547
+ for instance in self.__staged_remove_instances:
548
+ instance.config = None
549
+ self.lst_plugin_instances.remove(instance)
550
+
551
+ self.__staged_remove_instances = []
552
+ return
553
+
554
+ def __discard_staged_config(self, fail_reason: str):
555
+ """
556
+ Discard the staged configuration for the pipeline.
557
+ """
558
+
559
+ self.P(f'Discarding staged configuration for pipeline <{self.name}>. Reason: {fail_reason}', color="r")
560
+ self.__was_last_operation_successful = False
561
+
562
+ self.__staged_config = None
563
+ self.__staged_remove_instances = []
564
+ return
565
+
566
+ def __stage_proposed_config(self):
567
+ """
568
+ Stage the proposed configuration.
569
+ """
570
+ if self.proposed_config is not None:
571
+ if self.__staged_config is not None:
572
+ raise ValueError(
573
+ "Pipeline configuration has already been staged, waiting for confirmation from Execution Engine")
574
+
575
+ self.__staged_config = self.proposed_config
576
+ self.proposed_config = None
577
+
578
+ for instance in self.lst_plugin_instances:
579
+ instance._stage_proposed_config()
580
+
581
+ self.__staged_remove_instances.extend(self.proposed_remove_instances)
582
+ self.proposed_remove_instances = []
583
+
584
+ self.__was_last_operation_successful = None
585
+ return
586
+
587
+ def __print_proposed_changes(self):
588
+ """
589
+ Print the proposed changes to the pipeline.
590
+ """
591
+
592
+ if self.proposed_config is not None:
593
+ self.P("Proposed changes to pipeline <{}>:".format(self.name), verbosity=1)
594
+ self.P(" - Current config: {}".format(self.config), verbosity=1)
595
+ self.P(" - New pipeline config: {}".format(self.proposed_config), verbosity=1)
596
+
597
+ if len(self.proposed_remove_instances) > 0:
598
+ self.P(
599
+ " - Remove instances: {}".format([instance.instance_id for instance in self.proposed_remove_instances]), verbosity=1)
600
+
601
+ for instance in self.lst_plugin_instances:
602
+ if instance._is_tainted():
603
+ self.P(" - Plugin <{}:{}>:".format(instance.signature, instance.instance_id), verbosity=1)
604
+ self.P(" - Current config: {}".format(instance.config), verbosity=1)
605
+ self.P(" - Proposed config: {}".format(instance.proposed_config), verbosity=1)
606
+ return
607
+
608
+ def _close(self, timeout=10):
609
+ """
610
+ Close the pipeline.
611
+
612
+ Returns
613
+ -------
614
+ list[Transaction]
615
+ The list of transactions generated.
616
+ """
617
+ transactions = self.__register_transactions_for_delete(timeout=timeout)
618
+
619
+ self.__was_last_operation_successful = None
620
+
621
+ self.session._send_command_archive_pipeline(
622
+ worker=self.node_addr,
623
+ pipeline_name=self.name,
624
+ )
625
+
626
+ return transactions
627
+
628
+ def _get_base64_code(self, custom_code):
629
+ """
630
+ Get the base64 code.
631
+
632
+ Parameters
633
+ ----------
634
+ custom_code : str | callable
635
+ The custom code.
636
+
637
+ Returns
638
+ -------
639
+ str
640
+ The base64 code.
641
+ """
642
+ if custom_code is None:
643
+ return None
644
+
645
+ if isinstance(custom_code, str):
646
+ # it is a path
647
+ if os.path.exists(custom_code):
648
+ with open(custom_code, "r") as fd:
649
+ plain_code = "".join(fd.readlines())
650
+ # it is a string
651
+ else:
652
+ try:
653
+ method_name = "_DistributedCustomCodePresets__{}".format(custom_code.lower())
654
+ preset_code = getattr(DistributedCustomCodePresets, method_name)
655
+ plain_code = self.get_function_source_code(preset_code)
656
+ except:
657
+ plain_code = custom_code
658
+ elif callable(custom_code):
659
+ # we have a function
660
+ plain_code = self.get_function_source_code(custom_code)
661
+ else:
662
+ raise Exception("custom_code is not a string or a callable")
663
+ # endif get plain code
664
+
665
+ return self.code_to_base64(plain_code, verbose=False)
666
+
667
+ # Message handling
668
+ if True:
669
+ def _on_data(self, signature, instance_id, data):
670
+ """
671
+ Handle the data received from the Naeural edge node. This method is called by the Session object when a message is received from the Naeural edge node.
672
+ This method will call all the `on_data` callbacks of the pipeline and the instance that received the message.
673
+
674
+ Parameters
675
+ ----------
676
+ signature : str
677
+ The signature of the plugin that sent the message.
678
+ instance_id : str
679
+ The name of the instance that sent the message.
680
+ data : dict | Payload
681
+ The payload of the message.
682
+ """
683
+ # call all self callbacks
684
+ for callback in self.on_data_callbacks:
685
+ callback(self, signature, instance_id, data)
686
+
687
+ # call all instance callbacks
688
+ self.__call_instance_on_data_callbacks(signature, instance_id, data)
689
+ return
690
+
691
+ def _on_notification(self, signature, instance_id, data):
692
+ """
693
+ Handle the notification received from the Naeural edge node. This method is called by the Session object when a notification is received from the Naeural edge node.
694
+
695
+ Parameters
696
+ ----------
697
+ signature : str
698
+ The signature of the plugin that sent the notification.
699
+ instance_id : str
700
+ The name of the instance that sent the notification.
701
+ data : dict | Payload
702
+ The payload of the notification.
703
+ """
704
+ # call all self callbacks
705
+ for callback in self.on_notification_callbacks:
706
+ callback(self, data)
707
+
708
+ # call all instance callbacks
709
+ self.__call_instance_on_notification_callbacks(signature, instance_id, data)
710
+ return
711
+
712
+ def _add_on_data_callback(self, callback):
713
+ """
714
+ Add a new callback to the list of callbacks that handle the data received from the pipeline.
715
+
716
+ Parameters
717
+ ----------
718
+ callback : Callable[[Pipeline, str, str, dict], None]
719
+ The callback to add
720
+ """
721
+ self.on_data_callbacks.append(callback)
722
+ return
723
+
724
+ def _reset_on_data_callback(self):
725
+ """
726
+ Reset the list of callbacks that handle the data received from the pipeline.
727
+ """
728
+ self.on_data_callbacks = []
729
+ return
730
+
731
+ def _add_on_notification_callback(self, callback):
732
+ """
733
+ Add a new callback to the list of callbacks that handle the notifications received from the pipeline.
734
+
735
+ Parameters
736
+ ----------
737
+ callback : Callable[[Pipeline, dict], None]
738
+ The callback to add
739
+ """
740
+ self.on_notification_callbacks.append(callback)
741
+ return
742
+
743
+ def _reset_on_notification_callback(self):
744
+ """
745
+ Reset the list of callbacks that handle the notifications received from the pipeline.
746
+ """
747
+ self.on_notification_callbacks = []
748
+ return
749
+
750
+ def __call_instance_on_data_callbacks(self, signature, instance_id, data):
751
+ """
752
+ Call all the `on_data` callbacks of the instance that sent the message.
753
+
754
+ Parameters
755
+ ----------
756
+ signature : str
757
+ The signature of the plugin that sent the payload.
758
+ instance_id : str
759
+ The name of the instance that sent the payload.
760
+ data : dict | Payload
761
+ The payload of the payload.
762
+ """
763
+ for instance in self.lst_plugin_instances:
764
+ if instance.signature == signature and instance.instance_id == instance_id:
765
+ instance._on_data(self, data)
766
+ return
767
+
768
+ def __call_instance_on_notification_callbacks(self, signature, instance_id, data):
769
+ """
770
+ Call all the `on_notification` callbacks of the instance that sent the notification.
771
+
772
+ Parameters
773
+ ----------
774
+ signature : str
775
+ The signature of the plugin that sent the notification.
776
+ instance_id : str
777
+ The name of the instance that sent the notification.
778
+ data : dict | Payload
779
+ The payload of the notification.
780
+ """
781
+ for instance in self.lst_plugin_instances:
782
+ if instance.signature == signature and instance.instance_id == instance_id:
783
+ instance._on_notification(self, data)
784
+ return
785
+
786
+ # API
787
+ if True:
788
+ @property
789
+ def was_last_operation_successful(self):
790
+ """
791
+ Return whether the last operation was successful.
792
+
793
+ Returns
794
+ -------
795
+ bool
796
+ True if the last operation was successful, False if it failed, None if the ACK has not been received yet
797
+ """
798
+ return self.__was_last_operation_successful
799
+
800
+ def create_plugin_instance(self, *, signature, instance_id, config={}, on_data=None, on_notification=None, **kwargs) -> Instance:
801
+ """
802
+ Create a new instance of a desired plugin, with a given configuration. This instance is attached to this pipeline,
803
+ meaning it processes data from this pipelines data source. Parameters can be passed either in the `config` dict, or as `kwargs`.
804
+
805
+ Parameters
806
+ ----------
807
+ signature : str
808
+ The name of the plugin signature. This is the name of the desired overall functionality.
809
+ instance_id : str
810
+ The name of the instance. There can be multiple instances of the same plugin, mostly with different parameters
811
+ config : dict, optional
812
+ parameters used to customize the functionality. One can change the AI engine used for object detection,
813
+ or finetune alerter parameters to better fit a camera located in a low light environment.
814
+ Defaults to {}
815
+ on_data : Callable[[Pipeline, dict], None], optional
816
+ Callback that handles messages received from this instance.
817
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
818
+ Defaults to None
819
+ on_notification : Callable[[Pipeline, dict], None], optional
820
+ Callback that handles notifications received from this instance.
821
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
822
+ Defaults to None
823
+
824
+ Returns
825
+ -------
826
+ instance : Instance
827
+ An `Instance` object.
828
+
829
+ Raises
830
+ ------
831
+ Exception
832
+ Plugin instance already exists.
833
+ """
834
+ if isinstance(signature, str):
835
+ str_signature = signature.upper()
836
+ else:
837
+ plugin_template = signature
838
+ str_signature = plugin_template.signature.upper()
839
+
840
+ for instance in self.lst_plugin_instances:
841
+ if instance.instance_id == instance_id and instance.signature == str_signature:
842
+ raise Exception("plugin {} with instance {} already exists".format(str_signature, instance_id))
843
+
844
+ # create the new instance and add it to the list
845
+ config = {**config, **kwargs}
846
+ instance = self.__init_instance(signature, instance_id, config, on_data, on_notification, is_attached=False)
847
+ return instance
848
+
849
+ def __remove_plugin_instance(self, instance):
850
+ """
851
+ Remove a plugin instance from this pipeline.
852
+
853
+ Parameters
854
+ ----------
855
+ instance : Instance
856
+ The instance to be removed.
857
+ """
858
+ if instance is None:
859
+ raise Exception("The provided instance is None. Please provide a valid instance")
860
+
861
+ if instance not in self.lst_plugin_instances:
862
+ raise Exception("plugin <{}/{}> does not exist on this pipeline".format(instance.signature, instance.instance_id))
863
+
864
+ self.lst_plugin_instances.remove(instance)
865
+ return
866
+
867
+ def remove_plugin_instance(self, instance):
868
+ """
869
+ Stop a plugin instance from this pipeline.
870
+
871
+
872
+ Parameters
873
+ ----------
874
+ instance : Instance
875
+ The instance to be stopped.
876
+
877
+ """
878
+
879
+ self.__remove_plugin_instance(instance)
880
+ self.proposed_remove_instances.append(instance)
881
+ return
882
+
883
+ def create_custom_plugin_instance(self, *, instance_id, custom_code: callable, config={}, on_data=None, on_notification=None, **kwargs) -> Instance:
884
+ """
885
+ Create a new custom execution instance, with a given configuration. This instance is attached to this pipeline,
886
+ meaning it processes data from this pipelines data source. The code used for the custom instance must be provided
887
+ either as a string, or as a path to a file. Parameters can be passed either in the `config` dict, or as kwargs.
888
+ The custom plugin instance will run periodically. If one desires to execute a custom code only once, use `wait_exec`.
889
+
890
+ Parameters
891
+ ----------
892
+ instance_id : str
893
+ The name of the instance. There can be multiple instances of the same plugin, mostly with different parameters
894
+ custom_code : Callable[[CustomPluginTemplate], Any], optional
895
+ A string containing the entire code, a path to a file containing the code as a string or a function with the code.
896
+ This code will be executed remotely on an Naeural edge node. Defaults to None.
897
+ config : dict, optional
898
+ parameters used to customize the functionality. One can change the AI engine used for object detection,
899
+ or finetune alerter parameters to better fit a camera located in a low light environment.
900
+ Defaults to {}
901
+ on_data : Callable[[Pipeline, dict], None], optional
902
+ Callback that handles messages received from this instance.
903
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
904
+ Defaults to None
905
+ on_notification : Callable[[Pipeline, dict], None], optional
906
+ Callback that handles notifications received from this instance.
907
+ As arguments, it has a reference to this Pipeline object, along with the payload itsel.
908
+ Defaults to None
909
+
910
+ Returns
911
+ -------
912
+ instance : Instance
913
+ An `Instance` object.
914
+
915
+ Raises
916
+ ------
917
+ Exception
918
+ The code was not provided.
919
+ Exception
920
+ Plugin instance already exists.
921
+ """
922
+
923
+ b64code = self._get_base64_code(custom_code)
924
+
925
+ def callback(pipeline, data): return self.__custom_exec_on_data(pipeline, instance_id, on_data, data)
926
+ callback = callback if on_data is not None else None
927
+
928
+ return self.create_plugin_instance(
929
+ signature='CUSTOM_EXEC_01',
930
+ instance_id=instance_id,
931
+ config={
932
+ 'CODE': b64code,
933
+ **config
934
+ },
935
+ on_data=callback,
936
+ on_notification=on_notification,
937
+ **kwargs
938
+ )
939
+
940
+ # TODO: rename this!!!
941
+ def create_chain_dist_custom_plugin_instance(self,
942
+ *,
943
+ main_node_process_real_time_collected_data: any,
944
+ main_node_finish_condition: any,
945
+ main_node_aggregate_collected_data: any,
946
+ worker_node_code: any,
947
+ nr_remote_worker_nodes: int,
948
+ instance_id=None,
949
+ worker_node_pipeline_config={},
950
+ worker_node_plugin_signature='CUSTOM_EXEC_01',
951
+ worker_node_plugin_config={},
952
+ config={},
953
+ on_data=None,
954
+ on_notification=None,
955
+ **kwargs) -> Instance:
956
+ b64code_process_real_time_collected_data = self._get_base64_code(main_node_process_real_time_collected_data)
957
+ b64code_finish_condition = self._get_base64_code(main_node_finish_condition)
958
+ b64code_aggregate_collected_data = self._get_base64_code(main_node_aggregate_collected_data)
959
+ b64code_remote_node = self._get_base64_code(worker_node_code)
960
+
961
+ if instance_id is None:
962
+ instance_id = self.name + "_chain_dist_custom_exec_{}".format(self.log.get_unique_id())
963
+
964
+ return self.create_plugin_instance(
965
+ signature='PROCESS_REAL_TIME_COLLECTED_DATA_CUSTOM_EXEC_CHAIN_DIST',
966
+ instance_id=instance_id,
967
+ config={
968
+ 'CUSTOM_CODE_PROCESS_REAL_TIME_COLLECTED_DATA': b64code_process_real_time_collected_data,
969
+ 'CUSTOM_CODE_FINISH_CONDITION': b64code_finish_condition,
970
+ 'CUSTOM_CODE_AGGREGATE_COLLECTED_DATA': b64code_aggregate_collected_data,
971
+ 'CUSTOM_CODE_REMOTE_NODE': b64code_remote_node,
972
+
973
+ 'NR_REMOTE_NODES': nr_remote_worker_nodes,
974
+
975
+ 'NODE_PIPELINE_CONFIG': {
976
+ 'stream_type': "Void",
977
+ **worker_node_pipeline_config
978
+ },
979
+ 'NODE_PLUGIN_SIGNATURE': worker_node_plugin_signature,
980
+ 'NODE_PLUGIN_CONFIG': {
981
+ **worker_node_plugin_config
982
+ },
983
+ **config
984
+ },
985
+ on_data=on_data,
986
+ on_notification=on_notification,
987
+ **kwargs
988
+ )
989
+
990
+ def deploy(self, with_confirmation=True, wait_confirmation=True, timeout=10, verbose=False):
991
+ """
992
+ This method is used to deploy the pipeline on the Naeural edge node.
993
+ Here we collect all the proposed configurations and send them to the Naeural edge node.
994
+ All proposed configs become staged configs.
995
+ After all responses, apply the staged configs to finish the transaction.
996
+ """
997
+ # generate a unique session id for this deploy operation
998
+ # this session id will be used to track the transactions
999
+
1000
+ # step 0: print the proposed changes
1001
+ if verbose:
1002
+ self.__print_proposed_changes()
1003
+
1004
+ # step 1: register transactions for updates
1005
+ transactions = []
1006
+
1007
+ if with_confirmation:
1008
+ transactions: list[Transaction] = self.__register_transactions_for_update(timeout=timeout)
1009
+
1010
+ # step 1: send the proposed config to the box
1011
+ pipeline_config_changed = self.proposed_config is not None
1012
+ have_to_remove_instances = len(self.proposed_remove_instances) > 0
1013
+ have_new_instances = any([instance._is_tainted() and len(instance.config) == 0
1014
+ for instance in self.lst_plugin_instances])
1015
+ if pipeline_config_changed or have_to_remove_instances or have_new_instances:
1016
+ # updated pipeline config or deleted instances
1017
+ self.__send_update_config_to_box()
1018
+ elif any([instance._is_tainted() for instance in self.lst_plugin_instances]):
1019
+ # updated instances only
1020
+ tainted_instances = [instance for instance in self.lst_plugin_instances if instance._is_tainted()]
1021
+ self.__batch_update_instances(tainted_instances)
1022
+ else:
1023
+ return
1024
+
1025
+ # step 3: stage the proposed config
1026
+ self.__stage_proposed_config()
1027
+
1028
+ # step 3: wait for the box to respond
1029
+ if with_confirmation and wait_confirmation:
1030
+ self.session.wait_for_transactions(transactions)
1031
+
1032
+ # step 4: apply the staged config
1033
+ if not with_confirmation:
1034
+ self.__apply_staged_config(verbose=verbose)
1035
+ self.__apply_staged_instances_config(verbose=verbose)
1036
+
1037
+ self.P("Pipeline <{}> deployed".format(self.name), color="g")
1038
+
1039
+ if with_confirmation and not wait_confirmation:
1040
+ return transactions
1041
+ return
1042
+
1043
+ def wait_exec(self, *, custom_code: callable, instance_config={}, timeout=10):
1044
+ """
1045
+ Create a new REST-like custom execution instance, with a given configuration. This instance is attached to this pipeline,
1046
+ meaning it processes data from this pipelines data source. The code used for the custom instance must be provided either as a string, or as a path to a file. Parameters can be passed either in the config dict, or as kwargs.
1047
+ The REST-like custom plugin instance will execute only once. If one desires to execute a custom code periodically, use `create_custom_plugin_instance`.
1048
+
1049
+ Parameters
1050
+ ----------
1051
+ custom_code : Callable[[CustomPluginTemplate], Any], optional
1052
+ A string containing the entire code, a path to a file containing the code as a string or a function with the code.
1053
+ This code will be executed remotely on an Naeural edge node. Defaults to None.
1054
+ config : dict, optional
1055
+ parameters used to customize the functionality, by default {}
1056
+
1057
+ Returns
1058
+ -------
1059
+ Tuple[Any, Any]
1060
+ a tuple containing the result of the execution and the error, if any.
1061
+ If the execution completed successfully, the `error` is None, and the `result` is the returned value of the custom code.
1062
+
1063
+ Raises
1064
+ ------
1065
+ Exception
1066
+ The code was not provided.
1067
+ Exception
1068
+ Plugin instance already exists.
1069
+ """
1070
+
1071
+ b64code = self._get_base64_code(custom_code)
1072
+
1073
+ finished = False
1074
+ result = None
1075
+ error = None
1076
+
1077
+ def on_data(pipeline, data):
1078
+ nonlocal finished
1079
+ nonlocal result
1080
+ nonlocal error
1081
+
1082
+ if 'REST_EXECUTION_RESULT' in data and 'REST_EXECUTION_ERROR' in data:
1083
+ result = data['REST_EXECUTION_RESULT']
1084
+ error = data['REST_EXECUTION_ERROR']
1085
+ finished = True
1086
+ return
1087
+
1088
+ instance_id = self.name + "_rest_custom_exec_synchronous_" + self.log.get_unique_id()
1089
+ instance_config = {
1090
+ 'REQUEST': {
1091
+ 'DATA': {
1092
+ 'CODE': b64code,
1093
+ },
1094
+ 'TIMESTAMP': self.log.time_to_str()
1095
+ },
1096
+ 'RESULT_KEY': 'REST_EXECUTION_RESULT',
1097
+ 'ERROR_KEY': 'REST_EXECUTION_ERROR',
1098
+ **instance_config
1099
+ }
1100
+
1101
+ prop_config = self.__get_proposed_pipeline_config()
1102
+ if prop_config['TYPE'] == 'Void':
1103
+ instance_config['ALLOW_EMPTY_INPUTS'] = True
1104
+ instance_config['RUN_WITHOUT_IMAGE'] = True
1105
+
1106
+ self.create_plugin_instance(
1107
+ signature='REST_CUSTOM_EXEC_01',
1108
+ instance_id=instance_id,
1109
+ config=instance_config,
1110
+ on_data=on_data
1111
+ )
1112
+
1113
+ self.deploy()
1114
+
1115
+ start_time = time()
1116
+ while not finished and time() - start_time < timeout:
1117
+ sleep(0.1)
1118
+
1119
+ return result, error
1120
+
1121
+ def close(self, wait_confirmation=True, timeout=10):
1122
+ """
1123
+ Close the pipeline, stopping all the instances associated with it.
1124
+ """
1125
+
1126
+ transactions = self._close(timeout=timeout)
1127
+
1128
+ if wait_confirmation:
1129
+ self.session.wait_for_transactions(transactions)
1130
+ else:
1131
+ return transactions
1132
+ return
1133
+
1134
+ def P(self, *args, **kwargs):
1135
+ """
1136
+ Print info to stdout.
1137
+ """
1138
+ return self.log.P(*args, **kwargs)
1139
+
1140
+ def D(self, *args, **kwargs):
1141
+ """
1142
+ Call the `Logger.D` method.
1143
+ If using the default Logger, this call will print debug info to stdout if `silent` is set to `False`.
1144
+ The logger object is passed from the Session object to the Pipeline object when creating
1145
+ it with `create_pipeline` or `attach_to_pipeline`.
1146
+ """
1147
+ return self.session.D(*args, **kwargs)
1148
+
1149
+ def attach_to_plugin_instance(self, signature, instance_id, on_data=None, on_notification=None) -> Instance:
1150
+ """
1151
+ Attach to an existing instance on this pipeline.
1152
+ This method is useful when one wishes to attach an
1153
+ `on_data` and `on_notification` callbacks to said instance.
1154
+
1155
+ Parameters
1156
+ ----------
1157
+ signature : str
1158
+ name of the plugin signature.
1159
+ instance_id : str
1160
+ name of the instance.
1161
+ on_data : Callable[[Pipeline, dict], None], optional
1162
+ Callback that handles messages received from this instance. As arguments, it has a reference to this Pipeline object, along with the payload itself.
1163
+ Defaults to None.
1164
+ on_notification : Callable[[Pipeline, dict], None], optional
1165
+ Callback that handles notifications received from this instance. As arguments, it has a reference to this Pipeline object, along with the payload itself.
1166
+ Defaults to None.
1167
+
1168
+ Returns
1169
+ -------
1170
+ instance : Instance
1171
+ An `Instance` object.
1172
+
1173
+ Raises
1174
+ ------
1175
+ Exception
1176
+ the pipeline does not contain plugins with a given signature.
1177
+ Exception
1178
+ The pipeline does not contain the desired instance.
1179
+ """
1180
+
1181
+ # search for the instance in the list
1182
+ plugin_template = None
1183
+ if isinstance(signature, str):
1184
+ str_signature = signature.upper()
1185
+ else:
1186
+ plugin_template = signature
1187
+ str_signature = plugin_template.signature.upper()
1188
+ found_instance = None
1189
+ for instance in self.lst_plugin_instances:
1190
+ if instance.instance_id == instance_id and instance.signature == str_signature.upper():
1191
+ found_instance = instance
1192
+ break
1193
+
1194
+ if found_instance is None:
1195
+ raise Exception(f"Unable to attach to instance. Instance <{str_signature}/{instance_id}> does not exist")
1196
+
1197
+ # add the callbacks to the session
1198
+ if on_data is not None:
1199
+ found_instance._add_on_data_callback(on_data)
1200
+
1201
+ if on_notification is not None:
1202
+ found_instance._add_on_notification_callback(on_notification)
1203
+
1204
+ if plugin_template is not None:
1205
+ found_instance.convert_to_specialized_class(plugin_template)
1206
+
1207
+ return found_instance
1208
+
1209
+ def attach_to_custom_plugin_instance(self, instance_id, on_data=None, on_notification=None) -> Instance:
1210
+ """
1211
+ Attach to an existing custom execution instance on this pipeline.
1212
+ This method is useful when one wishes to attach an
1213
+ `on_data` and `on_notification` callbacks to said instance.
1214
+
1215
+ Parameters
1216
+ ----------
1217
+ instance_id : str
1218
+ name of the instance.
1219
+ on_data : Callable[[Pipeline, str, str, dict], None], optional
1220
+ Callback that handles messages received from this instance. As arguments, it has a reference to this Pipeline object, along with the payload itself.
1221
+ Defaults to None.
1222
+ on_notification : Callable[[Pipeline, dict], None], optional
1223
+ Callback that handles notifications received from this instance. As arguments, it has a reference to this Pipeline object, along with the payload itself.
1224
+ Defaults to None.
1225
+
1226
+ Returns
1227
+ -------
1228
+ str
1229
+ An identifier for this instance, useful for stopping an instance.
1230
+
1231
+ Raises
1232
+ ------
1233
+ Exception
1234
+ the pipeline does not contain any custom plugin.
1235
+ Exception
1236
+ The pipeline does not contain the desired instance.
1237
+ """
1238
+
1239
+ def callback(pipeline, data): return self.__custom_exec_on_data(pipeline, instance_id, on_data, data)
1240
+ callback = callback if on_data is not None else None
1241
+
1242
+ return self.attach_to_plugin_instance("CUSTOM_EXEC_01", instance_id, callback, on_notification)
1243
+
1244
+ def detach_from_instance(self, instance: Instance):
1245
+ # search for the instance in the list
1246
+ if instance is None:
1247
+ raise Exception("The provided instance is None. Please provide a valid instance")
1248
+
1249
+ instance._reset_on_data_callback()
1250
+ instance._reset_on_notification_callback()
1251
+ return
1252
+
1253
+ def update_acquisition_parameters(self, config={}, **kwargs):
1254
+ """
1255
+ Update the acquisition parameters of this pipeline.
1256
+ Parameters can be passed either in the `config` dict, or as `kwargs`.
1257
+
1258
+ Parameters
1259
+ ----------
1260
+ config : dict, optional
1261
+ The new configuration of the acquisition source, by default {}
1262
+ """
1263
+ if self.__staged_config is not None:
1264
+ raise ValueError("Pipeline configuration has already been staged, waiting for confirmation from Execution Engine")
1265
+
1266
+ if self.proposed_config is None:
1267
+ self.proposed_config = {}
1268
+
1269
+ self.proposed_config = {**self.proposed_config, **config, **{k.upper(): v for k, v in kwargs.items()}}
1270
+ self.proposed_config = self.__pop_ignored_keys_from_config(self.proposed_config)
1271
+
1272
+ for k, v in self.config.items():
1273
+ if k in self.proposed_config:
1274
+ if self.proposed_config[k] == v:
1275
+ del self.proposed_config[k]
1276
+
1277
+ if len(self.proposed_config) == 0:
1278
+ self.proposed_config = None
1279
+
1280
+ return
1281
+
1282
+ def send_pipeline_command(self, command, payload=None, command_params=None, wait_confirmation=True, timeout=10) -> list[Transaction]:
1283
+ """
1284
+ Send a pipeline command to the Naeural edge node.
1285
+ This command can block until the command is confirmed by the Naeural edge node.
1286
+
1287
+ Example:
1288
+ --------
1289
+ ```python
1290
+ pipeline.send_pipeline_command('START', wait_confirmation=True)
1291
+
1292
+ transactions_p1 = pipeline1.send_pipeline_command('START', wait_confirmation=False)
1293
+ transactions_p2 = pipeline2.send_pipeline_command('START', wait_confirmation=False)
1294
+ # wait for both commands to be confirmed, but after both commands are sent
1295
+ session.wait_for_transactions(transactions_p1 + transactions_p2)
1296
+ ```
1297
+
1298
+ Parameters
1299
+ ----------
1300
+ command : str
1301
+ The name of the command.
1302
+ payload : dict, optional
1303
+ The payload of the command, by default {}
1304
+ command_params : dict, optional
1305
+ The parameters of the command, by default {}
1306
+ wait_confirmation : bool, optional
1307
+ Whether to wait for the confirmation of the command, by default False
1308
+ timeout : int, optional
1309
+ The timeout for the transaction, by default 10
1310
+
1311
+ Returns
1312
+ -------
1313
+ list[Transaction] | None
1314
+ The list of transactions generated, or None if `wait_confirmation` is False.
1315
+ """
1316
+ transactions = self.__register_transaction_for_pipeline_command(timeout=timeout)
1317
+
1318
+ self.__was_last_operation_successful = None
1319
+
1320
+ self.session._send_command_pipeline_command(
1321
+ worker=self.node_addr,
1322
+ pipeline_name=self.name,
1323
+ command=command,
1324
+ payload=payload,
1325
+ command_params=command_params,
1326
+ )
1327
+
1328
+ if wait_confirmation:
1329
+ self.session.wait_for_transactions(transactions)
1330
+ else:
1331
+ return transactions
1332
+ return
1333
+
1334
+ def create_or_attach_to_plugin_instance(self, *, signature, instance_id, config={}, on_data=None, on_notification=None, **kwargs) -> Instance:
1335
+ """
1336
+ Create a new instance of a desired plugin, with a given configuration, or attach to an existing instance.
1337
+
1338
+ Parameters
1339
+ ----------
1340
+ signature : str
1341
+ The name of the plugin signature. This is the name of the desired overall functionality.
1342
+ instance_id : str
1343
+ The name of the instance. There can be multiple instances of the same plugin, mostly with different parameters
1344
+ config : dict, optional
1345
+ parameters used to customize the functionality. One can change the AI engine used for object detection,
1346
+ or finetune alerter parameters to better fit a camera located in a low light environment.
1347
+ Defaults to {}
1348
+ on_data : Callable[[Pipeline, dict], None], optional
1349
+ Callback that handles messages received from this instance.
1350
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
1351
+ Defaults to None
1352
+ on_notification : Callable[[Pipeline, dict], None], optional
1353
+ Callback that handles notifications received from this instance.
1354
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
1355
+ Defaults to None
1356
+
1357
+ Returns
1358
+ -------
1359
+ instance : Instance
1360
+ An `Instance` object.
1361
+ """
1362
+ try:
1363
+ instance = self.attach_to_plugin_instance(signature, instance_id, on_data, on_notification)
1364
+ instance.update_instance_config(config, **kwargs)
1365
+ except Exception as e:
1366
+ instance = self.create_plugin_instance(
1367
+ signature=signature,
1368
+ instance_id=instance_id,
1369
+ config=config,
1370
+ on_data=on_data,
1371
+ on_notification=on_notification,
1372
+ **kwargs
1373
+ )
1374
+ return instance
1375
+
1376
+ def create_or_attach_to_custom_plugin_instance(self, *, instance_id, custom_code, config={}, on_data=None, on_notification=None, **kwargs) -> Instance:
1377
+ """
1378
+ Create a new instance of a desired plugin, with a given configuration, or attach to an existing instance.
1379
+
1380
+ Parameters
1381
+ ----------
1382
+ signature : str
1383
+ The name of the plugin signature. This is the name of the desired overall functionality.
1384
+ instance_id : str
1385
+ The name of the instance. There can be multiple instances of the same plugin, mostly with different parameters
1386
+ config : dict, optional
1387
+ parameters used to customize the functionality. One can change the AI engine used for object detection,
1388
+ or finetune alerter parameters to better fit a camera located in a low light environment.
1389
+ Defaults to {}
1390
+ on_data : Callable[[Pipeline, dict], None], optional
1391
+ Callback that handles messages received from this instance.
1392
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
1393
+ Defaults to None
1394
+ on_notification : Callable[[Pipeline, dict], None], optional
1395
+ Callback that handles notifications received from this instance.
1396
+ As arguments, it has a reference to this Pipeline object, along with the payload itself.
1397
+ Defaults to None
1398
+
1399
+ Returns
1400
+ -------
1401
+ instance : Instance
1402
+ An `Instance` object.
1403
+ """
1404
+
1405
+ try:
1406
+ instance = self.attach_to_custom_plugin_instance(instance_id, on_data, on_notification)
1407
+ instance.update_instance_config(config, **kwargs)
1408
+ except:
1409
+ instance = self.create_custom_plugin_instance(
1410
+ instance_id=instance_id,
1411
+ custom_code=custom_code,
1412
+ config=config,
1413
+ on_data=on_data,
1414
+ on_notification=on_notification,
1415
+ **kwargs
1416
+ )
1417
+ return instance
1418
+
1419
+ def _sync_configuration_with_remote(self, config={}):
1420
+ config.pop('NAME', None)
1421
+ config.pop('TYPE', None)
1422
+ plugins = config.pop('PLUGINS', {})
1423
+
1424
+ self.config = {**self.config, **config}
1425
+
1426
+ active_plugins = []
1427
+ for dct_signature_instances in plugins:
1428
+ signature = dct_signature_instances['SIGNATURE']
1429
+ instances = dct_signature_instances['INSTANCES']
1430
+ for dct_instance in instances:
1431
+ instance_id = dct_instance.pop('INSTANCE_ID')
1432
+ active_plugins.append((signature, instance_id))
1433
+ instance_object = self.__get_instance_object(signature, instance_id)
1434
+ if instance_object is None:
1435
+ self.__init_instance(signature, instance_id, dct_instance, None, None, is_attached=True)
1436
+ else:
1437
+ instance_object._sync_configuration_with_remote(dct_instance)
1438
+ # end for dct_instance
1439
+ # end for dct_signature_instances
1440
+
1441
+ for instance in self.lst_plugin_instances:
1442
+ if (instance.signature, instance.instance_id) not in active_plugins:
1443
+ self.__remove_plugin_instance(instance)
1444
+ # end for instance
1445
+ return
1446
+
1447
+ def update_full_configuration(self, config={}):
1448
+ """
1449
+ Update the full configuration of this pipeline.
1450
+ Parameters are passed in the `config` dict.
1451
+ We do not support kwargs yet because it makes it difficult to check priority of dictionary, merging values, etc.
1452
+
1453
+ Parameters
1454
+ ----------
1455
+ config : dict, optional
1456
+ The new configuration of the pipeline, by default {}
1457
+ """
1458
+ if self.__staged_config is not None:
1459
+ raise ValueError("Pipeline configuration has already been staged, waiting for confirmation from Execution Engine")
1460
+
1461
+ # pop the illegal to modify keys
1462
+ config.pop('NAME', None)
1463
+ config.pop('TYPE', None)
1464
+ plugins = config.pop('PLUGINS', None)
1465
+
1466
+ self.update_acquisition_parameters(config)
1467
+
1468
+ if plugins is None:
1469
+ return
1470
+
1471
+ new_plugins = []
1472
+ for dct_signature_instances in plugins:
1473
+ signature = dct_signature_instances['SIGNATURE']
1474
+ instances = dct_signature_instances['INSTANCES']
1475
+ for dct_instance in instances:
1476
+ instance_id = dct_instance.pop('INSTANCE_ID')
1477
+ new_plugins.append((signature, instance_id))
1478
+ instance_object = self.__get_instance_object(signature, instance_id)
1479
+
1480
+ if instance_object is None:
1481
+ self.create_plugin_instance(signature=signature, instance_id=instance_id, config=dct_instance)
1482
+ else:
1483
+ instance_object.update_instance_config(dct_instance)
1484
+ # end for dct_instance
1485
+ # end for dct_signature_instances
1486
+
1487
+ # now check if we have to remove any instances
1488
+ for instance in self.lst_plugin_instances:
1489
+ if (instance.signature, instance.instance_id) not in new_plugins:
1490
+ self.remove_plugin_instance(instance)
1491
+ # end for instance
1492
+ return
1493
+
1494
+ @property
1495
+ def node_id(self):
1496
+ """
1497
+ Return the node id of the pipeline.
1498
+ """
1499
+ return self.session.get_node_name(self.node_addr)