naeural-client 3.1.5__py3-none-any.whl → 3.2.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.
@@ -34,7 +34,9 @@ from .pipeline import Pipeline
34
34
  from .webapp_pipeline import WebappPipeline
35
35
  from .transaction import Transaction
36
36
  from ..utils.config import (
37
- load_user_defined_config, get_user_config_file, get_user_folder, seconds_to_short_format
37
+ load_user_defined_config, get_user_config_file, get_user_folder,
38
+ seconds_to_short_format, log_with_color, set_client_alias,
39
+ EE_SDK_ALIAS_ENV_KEY, EE_SDK_ALIAS_DEFAULT
38
40
  )
39
41
 
40
42
  # from ..default.instance import PLUGIN_TYPES # circular import
@@ -45,6 +47,8 @@ DEBUG_MQTT_SERVER = "r9092118.ala.eu-central-1.emqxsl.com"
45
47
  SDK_NETCONFIG_REQUEST_DELAY = 300
46
48
 
47
49
 
50
+
51
+
48
52
  class GenericSession(BaseDecentrAIObject):
49
53
  """
50
54
  A Session is a connection to a communication server which provides the channel to interact with nodes from the Naeural Edge Protocol network.
@@ -80,7 +84,7 @@ class GenericSession(BaseDecentrAIObject):
80
84
  user=None,
81
85
  pwd=None,
82
86
  secured=None,
83
- name='R1SDK',
87
+ name=None,
84
88
  encrypt_comms=True,
85
89
  config={},
86
90
  filter_workers=None,
@@ -89,9 +93,9 @@ class GenericSession(BaseDecentrAIObject):
89
93
  on_notification=None,
90
94
  on_heartbeat=None,
91
95
  debug_silent=True,
92
- debug=1,
93
- silent=False,
96
+ debug=1, # TODO: debug or verbosity - fix this
94
97
  verbosity=1,
98
+ silent=False,
95
99
  dotenv_path=None,
96
100
  show_commands=False,
97
101
  blockchain_config=BLOCKCHAIN_CONFIG,
@@ -114,32 +118,42 @@ class GenericSession(BaseDecentrAIObject):
114
118
  ----------
115
119
  host : str, optional
116
120
  The hostname of the server. If None, it will be retrieved from the environment variable AIXP_HOSTNAME
121
+
117
122
  port : int, optional
118
123
  The port. If None, it will be retrieved from the environment variable AIXP_PORT
124
+
119
125
  user : str, optional
120
126
  The user name. If None, it will be retrieved from the environment variable AIXP_USERNAME
127
+
121
128
  pwd : str, optional
122
129
  The password. If None, it will be retrieved from the environment variable AIXP_PASSWORD
130
+
123
131
  secured: bool, optional
124
132
  True if connection is secured, by default None
133
+
125
134
  name : str, optional
126
135
  The name of this connection, used to identify owned pipelines on a specific Naeural Edge Protocol edge node.
127
136
  The name will be used as `INITIATOR_ID` and `SESSION_ID` when communicating with Naeural Edge Protocol edge nodes, by default 'pySDK'
137
+
128
138
  config : dict, optional
129
139
  Configures the names of the channels this session will connect to.
130
140
  If using a Mqtt server, these channels are in fact topics.
131
141
  Modify this if you are absolutely certain of what you are doing.
132
142
  By default {}
143
+
133
144
  filter_workers: list, optional
134
145
  If set, process the messages that come only from the nodes from this list.
135
146
  Defaults to None
147
+
136
148
  show_commands : bool
137
149
  If True, will print the commands that are being sent to the Naeural Edge Protocol edge nodes.
138
150
  Defaults to False
151
+
139
152
  log : Logger, optional
140
153
  A logger object which implements basic logging functionality and some other utils stuff. Can be ignored for now.
141
154
  In the future, the documentation for the Logger base class will be available and developers will be able to use
142
155
  custom-made Loggers.
156
+
143
157
  on_payload : Callable[[Session, str, str, str, str, dict], None], optional
144
158
  Callback that handles all payloads received from this network.
145
159
  As arguments, it has a reference to this Session object, the node name, the pipeline, signature and instance, and the payload.
@@ -191,11 +205,24 @@ class GenericSession(BaseDecentrAIObject):
191
205
  If True, the SDK will use the home folder as the base folder for the local cache.
192
206
  NOTE: if you need to use development style ./_local_cache, set this to False.
193
207
  """
208
+
209
+ # TODO: clarify verbosity vs debug
210
+
194
211
  debug = debug or not debug_silent
195
212
  if isinstance(debug, bool):
196
213
  debug = 2 if debug else 0
214
+
215
+ if verbosity > 1 and debug <=1:
216
+ debug = 2
197
217
 
198
218
  self.__debug = int(debug) > 0
219
+ self._verbosity = verbosity
220
+
221
+ if self.__debug:
222
+ if not silent:
223
+ log_with_color(f"Debug mode enabled: {debug=}, {verbosity=}", color='y')
224
+
225
+ ### END verbosity fix needed
199
226
 
200
227
  self.__at_least_one_node_peered = False
201
228
  self.__at_least_a_netmon_received = False
@@ -214,12 +241,13 @@ class GenericSession(BaseDecentrAIObject):
214
241
  self.__auto_configuration = auto_configuration
215
242
 
216
243
  self.log = log
244
+
245
+
217
246
  self.name = name
218
247
  self.silent = silent
219
248
 
220
249
  self.__eth_enabled = eth_enabled
221
250
 
222
- self._verbosity = verbosity
223
251
  self.encrypt_comms = encrypt_comms
224
252
 
225
253
  self._dct_online_nodes_pipelines: dict[str, Pipeline] = {}
@@ -283,6 +311,9 @@ class GenericSession(BaseDecentrAIObject):
283
311
  # use_home_folder allows us to use the home folder as the base folder
284
312
  local_cache_base_folder = str(get_user_folder())
285
313
  # end if
314
+
315
+ ## 1st config step before anything else - we prepare config via ~/.ratio1/config or .env
316
+ self.__load_user_config(dotenv_path=self.__dotenv_path)
286
317
 
287
318
 
288
319
  super(GenericSession, self).__init__(
@@ -295,16 +326,15 @@ class GenericSession(BaseDecentrAIObject):
295
326
  )
296
327
  return
297
328
 
298
- def Pd(self, *args, **kwargs):
299
- if self.__debug:
329
+ def Pd(self, *args, verbosity=1, **kwargs):
330
+ if self.__debug and verbosity <= self._verbosity:
300
331
  kwargs["color"] = 'd' if kwargs.get("color") != 'r' else 'r'
301
- self.log.P(*args, **kwargs)
332
+ kwargs['forced_debug'] = True
333
+ self.D(*args, **kwargs)
302
334
  return
303
335
 
304
336
 
305
- def startup(self):
306
- ## 1st config step - we prepare config via ~/.naeural/config or .env
307
- self.__load_user_config(dotenv_path=self.__dotenv_path)
337
+ def startup(self):
308
338
 
309
339
  # TODO: needs refactoring - suboptimal design
310
340
  # start the blockchain engine assuming config is already set
@@ -319,7 +349,7 @@ class GenericSession(BaseDecentrAIObject):
319
349
  dauth_endp=None, # get from consts or env
320
350
  add_env=self.__auto_configuration,
321
351
  debug=False,
322
- sender_alias='SDK'
352
+ sender_alias=self.name
323
353
  )
324
354
  # end bc_engine
325
355
  # END TODO
@@ -371,6 +401,13 @@ class GenericSession(BaseDecentrAIObject):
371
401
  self.__start_main_loop_thread()
372
402
  super(GenericSession, self).startup()
373
403
 
404
+
405
+ def _shorten_addr(self, addr: str) -> str:
406
+ if not isinstance(addr, str) or len(addr) < 15 or '...' in addr:
407
+ return addr
408
+ return addr[:11] + '...' + addr[-4:]
409
+
410
+
374
411
  # Message callbacks
375
412
  if True:
376
413
  def __create_user_callback_threads(self):
@@ -578,19 +615,32 @@ class GenericSession(BaseDecentrAIObject):
578
615
  **kwargs
579
616
  )
580
617
  self.bc_engine.sign(msg_to_send)
581
- self.P(f'Sending encrypted payload to <{node_addr}>', color='d')
618
+ self.P(f'Sending encrypted payload to <{self._shorten_addr(node_addr)}>', color='d')
582
619
  self._send_payload(msg_to_send)
583
620
  return
584
621
 
585
- def __request_pipelines_from_net_config_monitor(self, node_addr):
622
+ def __request_pipelines_from_net_config_monitor(self, node_addr=None):
586
623
  """
587
- Request the pipelines for a node from the net-config monitor plugin instance.
624
+ Request the pipelines for a node sending the payload to the
625
+ the net-config monitor plugin instance of that given node or nodes.
626
+
627
+
588
628
  Parameters
589
629
  ----------
590
- node_addr : str or list
630
+ node_addr : str or list (optional)
591
631
  The address or list of the edge node(s) that sent the message.
632
+ If None, the request will be sent to all nodes that are allowed to receive messages.
633
+
634
+ OBSERVATION:
635
+ This method should be called without node_addr(s) as it will get all the known peered nodes
636
+ and request the pipelines from them. Formely, this method was called following a netmon message
637
+ however, this was not the best approach as the netmon message might contain limited amount of
638
+ peer information is some cases.
592
639
 
593
640
  """
641
+ if node_addr is None:
642
+ node_addr = [k for k, v in self._dct_can_send_to_node.items() if v]
643
+ # end if
594
644
  assert node_addr is not None, "Node address cannot be None"
595
645
  payload = {
596
646
  NET_CONFIG.NET_CONFIG_DATA: {
@@ -603,17 +653,53 @@ class GenericSession(BaseDecentrAIObject):
603
653
  }
604
654
  if isinstance(node_addr, str):
605
655
  node_addr = [node_addr]
606
- dest = [
607
- f"<{x}> '{self.__dct_node_address_to_alias.get(x, None)}'" for x in node_addr
608
- ]
609
- self.D(f"<NETCFG> Sending request to:\n{json.dumps(dest, indent=2)}", color='y')
610
- self.send_encrypted_payload(
611
- node_addr=node_addr, payload=payload,
612
- additional_data=additional_data
613
- )
614
- for node in node_addr:
615
- self._dct_netconfig_pipelines_requests[node] = tm()
656
+
657
+ # now we filter only the nodes that have not been requested recently
658
+ node_addr = [x for x in node_addr if self.__needs_netconfig_request(x)]
659
+
660
+ if len(node_addr) > 0:
661
+ dest = [
662
+ f"<{x}> '{self.__dct_node_address_to_alias.get(x, None)}'" for x in node_addr
663
+ ]
664
+ self.D(f"<NC> Sending request to:\n{json.dumps(dest, indent=2)}")
665
+
666
+ self.send_encrypted_payload(
667
+ node_addr=node_addr, payload=payload,
668
+ additional_data=additional_data
669
+ )
670
+ for node in node_addr:
671
+ self._dct_netconfig_pipelines_requests[node] = tm()
672
+ # end if
616
673
  return
674
+
675
+
676
+
677
+ def __needs_netconfig_request(self, node_addr : str) -> bool:
678
+ """
679
+ Check if a net-config request is needed for a node.
680
+
681
+ Parameters
682
+ ----------
683
+ node_addr : str
684
+ The address of the edge node.
685
+
686
+ Returns
687
+ -------
688
+ bool
689
+ True if a net-config request is needed, False otherwise
690
+ """
691
+ short_addr = self._shorten_addr(node_addr)
692
+ last_requested_by_netmon = self._dct_netconfig_pipelines_requests.get(node_addr, 0)
693
+ elapsed = tm() - last_requested_by_netmon
694
+ str_elapsed = f"{elapsed:.0f}s ago" if elapsed < 9999999 else "never"
695
+ needs_netconfig_request = elapsed > SDK_NETCONFIG_REQUEST_DELAY
696
+ if needs_netconfig_request:
697
+ self.D(f"<NC> Node <{short_addr}> needs update as last request was {str_elapsed} > {SDK_NETCONFIG_REQUEST_DELAY}")
698
+ else:
699
+ self.D(f"<NC> Node <{short_addr}> does NOT need update as last request was {str_elapsed} < {SDK_NETCONFIG_REQUEST_DELAY}")
700
+ return needs_netconfig_request
701
+
702
+
617
703
 
618
704
  def __track_allowed_node_by_netmon(self, node_addr, dict_msg):
619
705
  """
@@ -643,38 +729,47 @@ class GenericSession(BaseDecentrAIObject):
643
729
 
644
730
  client_is_allowed = self.bc_engine.contains_current_address(node_whitelist)
645
731
  can_send = not node_secured or client_is_allowed or self.bc_engine.address == node_addr
646
- if node_addr not in self._dct_can_send_to_node and can_send:
732
+ self._dct_can_send_to_node[node_addr] = can_send
733
+ short_addr = self._shorten_addr(node_addr)
734
+ if can_send:
647
735
  if node_online:
648
736
  # only attempt to request pipelines if the node is online and if not recently requested
649
- last_requested_by_netmon = self._dct_netconfig_pipelines_requests.get(node_addr, 0)
650
- if tm() - last_requested_by_netmon > SDK_NETCONFIG_REQUEST_DELAY:
651
- needs_netconfig = True
652
- else:
653
- self.D(f"Node <{node_addr}> is online but pipelines were recently requested", color='y')
737
+ needs_netconfig= self.__needs_netconfig_request(node_addr)
654
738
  else:
655
- self.D(f"Node <{node_addr}> is offline thus NOT sending net-config request", color='y')
739
+ self.D(f"<NC> Node <{short_addr}> is OFFLINE thus NOT sending net-config request")
656
740
  # endif node seen for the first time
657
-
658
- self._dct_can_send_to_node[node_addr] = can_send
659
741
  return needs_netconfig
660
742
 
661
743
 
662
- def __process_node_pipelines(self, node_addr, pipelines):
744
+ def __process_node_pipelines(
745
+ self,
746
+ node_addr : str,
747
+ pipelines : list,
748
+ plugins_statuses : list
749
+ ):
663
750
  """
664
- Given a list of pipeline configurations, create or update the pipelines for a node.
751
+ Given a list of pipeline configurations, create or update the pipelines for a node
752
+ including the liveness of the plugins required for app monitoring
665
753
  """
666
754
  new_pipelines = []
667
755
  if node_addr not in self._dct_online_nodes_pipelines:
668
756
  self._dct_online_nodes_pipelines[node_addr] = {}
669
757
  for config in pipelines:
670
758
  pipeline_name = config[PAYLOAD_DATA.NAME]
671
- pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr].get(pipeline_name, None)
759
+ pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr].get(
760
+ pipeline_name, None
761
+ )
672
762
  if pipeline is not None:
673
- pipeline._sync_configuration_with_remote({k.upper(): v for k, v in config.items()})
763
+ pipeline._sync_configuration_with_remote(
764
+ config={k.upper(): v for k, v in config.items()},
765
+ plugins_statuses=plugins_statuses,
766
+ )
674
767
  else:
675
- self._dct_online_nodes_pipelines[node_addr][pipeline_name] = self.__create_pipeline_from_config(
676
- node_addr, config)
677
- new_pipelines.append(self._dct_online_nodes_pipelines[node_addr][pipeline_name])
768
+ pipeline : Pipeline = self.__create_pipeline_from_config(
769
+ node_addr=node_addr, config=config, plugins_statuses=plugins_statuses
770
+ )
771
+ self._dct_online_nodes_pipelines[node_addr][pipeline_name] = pipeline
772
+ new_pipelines.append(pipeline)
678
773
  return new_pipelines
679
774
 
680
775
  def __on_heartbeat(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance):
@@ -685,12 +780,16 @@ class GenericSession(BaseDecentrAIObject):
685
780
  ----------
686
781
  dict_msg : dict
687
782
  The message received from the communication server
783
+
688
784
  msg_node_addr : str
689
785
  The address of the Naeural Edge Protocol edge node that sent the message.
786
+
690
787
  msg_pipeline : str
691
788
  The name of the pipeline that sent the message.
789
+
692
790
  msg_signature : str
693
791
  The signature of the plugin that sent the message.
792
+
694
793
  msg_instance : str
695
794
  The name of the instance that sent the message.
696
795
  """
@@ -714,21 +813,24 @@ class GenericSession(BaseDecentrAIObject):
714
813
  )
715
814
 
716
815
  msg_active_configs = dict_msg.get(HB.CONFIG_STREAMS)
816
+ whitelist = dict_msg.get(HB.EE_WHITELIST, [])
817
+ is_allowed = self.bc_engine.contains_current_address(whitelist)
717
818
  if msg_active_configs is None:
718
819
  msg_active_configs = []
719
820
  # at this point we dont return if no active configs are present
720
821
  # as the protocol should NOT send a heartbeat with active configs to
721
822
  # the entire network, only to the interested parties via net-config
722
-
723
- self.D("<HB> Received {} with {} pipelines".format(
724
- msg_node_addr, len(msg_active_configs)), verbosity=2
823
+ short_addr = self._shorten_addr(msg_node_addr)
824
+ self.D("<HB> Received {} with {} pipelines (wl: {}, allowed: {})".format(
825
+ short_addr, len(msg_active_configs), len(whitelist), is_allowed
826
+ ), verbosity=2
725
827
  )
726
828
 
727
829
  if len(msg_active_configs) > 0:
728
830
  # this is for legacy and custom implementation where heartbeats still contain
729
831
  # the pipeline configuration.
730
832
  pipeline_names = [x.get(PAYLOAD_DATA.NAME, None) for x in msg_active_configs]
731
- self.D(f'<HB> Processing pipelines from <{msg_node_addr}>:{pipeline_names}', color='y')
833
+ self.D(f'<HB> Processing pipelines from <{short_addr}>:{pipeline_names}', color='y')
732
834
  self.__process_node_pipelines(msg_node_addr, msg_active_configs)
733
835
 
734
836
  # TODO: move this call in `__on_message_default_callback`
@@ -780,7 +882,7 @@ class GenericSession(BaseDecentrAIObject):
780
882
  self.D("Received notification {} from <{}/{}>: {}"
781
883
  .format(
782
884
  notification_type,
783
- msg_node_addr,
885
+ self._shorten_addr(msg_node_addr),
784
886
  msg_pipeline,
785
887
  notification),
786
888
  color=color,
@@ -829,6 +931,8 @@ class GenericSession(BaseDecentrAIObject):
829
931
  online_addresses = []
830
932
  all_addresses = []
831
933
  lst_netconfig_request = []
934
+ short_addr = self._shorten_addr(sender_addr)
935
+ self.D(f"<NM> Processing {len(current_network)} from <{short_addr}> `{ee_id}`")
832
936
  for _ , node_data in current_network.items():
833
937
  needs_netconfig = False
834
938
  node_addr = node_data.get(PAYLOAD_DATA.NETMON_ADDRESS, None)
@@ -846,15 +950,22 @@ class GenericSession(BaseDecentrAIObject):
846
950
  if needs_netconfig:
847
951
  lst_netconfig_request.append(node_addr)
848
952
  # end for each node in network map
849
- self.Pd(f"Net mon from <{sender_addr}> `{ee_id}`: {len(online_addresses)}/{len(all_addresses)}")
850
- if len(lst_netconfig_request) > 0:
851
- self.__request_pipelines_from_net_config_monitor(lst_netconfig_request)
953
+ self.Pd(f"<NM> <{short_addr}> `{ee_id}`: {len(online_addresses)} online of total {len(all_addresses)} nodes")
954
+ first_request = len(self._dct_netconfig_pipelines_requests) == 0
955
+ if len(lst_netconfig_request) > 0 or first_request:
956
+ str_msg = "First request for" if first_request else "Requesting"
957
+ msg = f"<NC> {str_msg} pipelines from at least {len(lst_netconfig_request)} nodes"
958
+ if first_request:
959
+ self.P(msg, color='y')
960
+ else:
961
+ self.Pd(msg, verbosity=2)
962
+ self.__request_pipelines_from_net_config_monitor()
852
963
  # end if needs netconfig
853
964
  nr_peers = sum(self._dct_can_send_to_node.values())
854
965
  if nr_peers > 0 and not self.__at_least_one_node_peered:
855
966
  self.__at_least_one_node_peered = True
856
967
  self.P(
857
- f"Received {PLUGIN_SIGNATURES.NET_MON_01} from {sender_addr}, so far {nr_peers} peers that allow me: {json.dumps(self._dct_can_send_to_node, indent=2)}",
968
+ f"<NM> Received {PLUGIN_SIGNATURES.NET_MON_01} from {sender_addr}, so far {nr_peers} peers that allow me: {json.dumps(self._dct_can_send_to_node, indent=2)}",
858
969
  color='g'
859
970
  )
860
971
  # end for each node in network map
@@ -878,7 +989,7 @@ class GenericSession(BaseDecentrAIObject):
878
989
  sender_addr = dict_msg.get(PAYLOAD_DATA.EE_SENDER, None)
879
990
  short_sender_addr = sender_addr[:8] + '...' + sender_addr[-4:]
880
991
  if self.client_address == sender_addr:
881
- self.D("<NETCFG> Ignoring message from self", color='d')
992
+ self.D("<NC> Ignoring message from self", color='d')
882
993
  return
883
994
  receiver = dict_msg.get(PAYLOAD_DATA.EE_DESTINATION, None)
884
995
  if not isinstance(receiver, list):
@@ -888,26 +999,32 @@ class GenericSession(BaseDecentrAIObject):
888
999
  op = dict_msg.get(NET_CONFIG.NET_CONFIG_DATA, {}).get(NET_CONFIG.OPERATION, "UNKNOWN")
889
1000
  # drop any incoming request as we are not a net-config provider just a consumer
890
1001
  if op == NET_CONFIG.REQUEST_COMMAND:
891
- self.P(f"<NETCFG> Dropping request from <{short_sender_addr}> `{ee_id}`", color='d')
1002
+ self.Pd(f"<NC> Dropping request from <{short_sender_addr}> `{ee_id}`")
892
1003
  return
893
1004
 
894
1005
  # check if I am allowed to see this payload
895
1006
  if not self.bc_engine.contains_current_address(receiver):
896
- self.P(f"<NETCFG> Received `{op}` from <{short_sender_addr}> `{ee_id}` but I am not in the receiver list: {receiver}", color='d')
1007
+ self.P(f"<NC> Received `{op}` from <{short_sender_addr}> `{ee_id}` but I am not in the receiver list: {receiver}", color='d')
897
1008
  return
898
1009
 
899
1010
  # encryption check. By now all should be decrypted
900
1011
  is_encrypted = dict_msg.get(PAYLOAD_DATA.EE_IS_ENCRYPTED, False)
901
1012
  if not is_encrypted:
902
- self.P(f"<NETCFG> Received from <{short_sender_addr}> `{ee_id}` but it is not encrypted", color='r')
1013
+ self.P(f"<NC> Received from <{short_sender_addr}> `{ee_id}` but it is not encrypted", color='r')
903
1014
  return
904
1015
  net_config_data = dict_msg.get(NET_CONFIG.NET_CONFIG_DATA, {})
905
- received_pipelines = net_config_data.get('PIPELINES', [])
906
- self.D(f"<NETCFG> Received {len(received_pipelines)} pipelines from <{sender_addr}> `{ee_id}`")
907
- new_pipelines = self.__process_node_pipelines(sender_addr, received_pipelines)
1016
+ received_pipelines = net_config_data.get(NET_CONFIG.PIPELINES, [])
1017
+ received_plugins = net_config_data.get(NET_CONFIG.PLUGINS_STATUSES, [])
1018
+ self.D(f"<NC> Received {len(received_pipelines)} pipelines from <{sender_addr}> `{ee_id}`")
1019
+ if self._verbosity > 2:
1020
+ self.D(f"<NC> {ee_id} Netconfig data:\n{json.dumps(net_config_data, indent=2)}")
1021
+ new_pipelines = self.__process_node_pipelines(
1022
+ node_addr=sender_addr, pipelines=received_pipelines,
1023
+ plugins_statuses=received_plugins
1024
+ )
908
1025
  pipeline_names = [x.name for x in new_pipelines]
909
1026
  if len(new_pipelines) > 0:
910
- self.P(f'<NETCFG> Received NEW pipelines from <{sender_addr}> `{ee_id}`:{pipeline_names}', color='y')
1027
+ self.P(f'<NC> Received NEW pipelines from <{sender_addr}> `{ee_id}`:{pipeline_names}', color='y')
911
1028
  return True
912
1029
 
913
1030
 
@@ -1329,8 +1446,8 @@ class GenericSession(BaseDecentrAIObject):
1329
1446
  if True:
1330
1447
 
1331
1448
  def __load_user_config(self, dotenv_path):
1332
- # if the ~/.naeural/config file exists, load the credentials from there else try to load them from .env
1333
- if not load_user_defined_config(verbose=not self.log.silent):
1449
+ # if the ~/.ratio1/config file exists, load the credentials from there else try to load them from .env
1450
+ if not load_user_defined_config(verbose=not self.silent):
1334
1451
  # this method will search for the credentials in the environment variables
1335
1452
  # the path to env file, if not specified, will be search in the following order:
1336
1453
  # 1. current working directory
@@ -1338,14 +1455,33 @@ class GenericSession(BaseDecentrAIObject):
1338
1455
  load_dotenv(dotenv_path=dotenv_path, verbose=False)
1339
1456
  if not self.silent:
1340
1457
  keys = [k for k in os.environ if k.startswith("EE_")]
1341
- print("Loaded credentials from environment variables: {keys}", flush=True)
1458
+ if not self.silent:
1459
+ log_with_color(f"Loaded credentials from environment variables: {keys}", color='y')
1342
1460
  self.__user_config_loaded = False
1343
1461
  else:
1344
1462
  if not self.silent:
1345
1463
  keys = [k for k in os.environ if k.startswith("EE_")]
1346
- print(f"Loaded credentials from `{get_user_config_file()}`: {keys}.", flush=True)
1464
+ if not self.silent:
1465
+ log_with_color(f"Loaded credentials from `{get_user_config_file()}`: {keys}.", color='y')
1347
1466
  self.__user_config_loaded = True
1348
1467
  # endif config loading from ~ or ./.env
1468
+
1469
+ if self.name is None:
1470
+ from naeural_client.logging.logger_mixins.utils_mixin import _UtilsMixin
1471
+ random_name = _UtilsMixin.get_random_name()
1472
+ default = EE_SDK_ALIAS_DEFAULT + '-' + random_name
1473
+ self.name = os.environ.get(EE_SDK_ALIAS_ENV_KEY, default)
1474
+ if EE_SDK_ALIAS_ENV_KEY not in os.environ:
1475
+ if not self.silent:
1476
+ log_with_color(f"Using default SDK alias: {self.name}. Writing the user config file...", color='y')
1477
+ set_client_alias(self.name)
1478
+ #end with
1479
+ else:
1480
+ if not self.silent:
1481
+ log_with_color(f"SDK Alias (from env): {self.name}.", color='y')
1482
+ #end if
1483
+ #end name is None
1484
+ return self.__user_config_loaded
1349
1485
 
1350
1486
  def __fill_config(self, host, port, user, pwd, secured):
1351
1487
  """
@@ -1758,7 +1894,12 @@ class GenericSession(BaseDecentrAIObject):
1758
1894
  self.__open_transactions.append(transaction)
1759
1895
  return transaction
1760
1896
 
1761
- def __create_pipeline_from_config(self, node_addr, config):
1897
+ def __create_pipeline_from_config(
1898
+ self,
1899
+ node_addr : str,
1900
+ config : dict,
1901
+ plugins_statuses : list = None,
1902
+ ):
1762
1903
  pipeline_config = {k.lower(): v for k, v in config.items()}
1763
1904
  name = pipeline_config.pop('name', None)
1764
1905
  plugins = pipeline_config.pop('plugins', None)
@@ -1771,6 +1912,7 @@ class GenericSession(BaseDecentrAIObject):
1771
1912
  name=name,
1772
1913
  plugins=plugins,
1773
1914
  existing_config=pipeline_config,
1915
+ plugins_statuses=plugins_statuses,
1774
1916
  )
1775
1917
 
1776
1918
  return pipeline
@@ -2288,9 +2430,9 @@ class GenericSession(BaseDecentrAIObject):
2288
2430
  bool
2289
2431
  True if the node is online, False otherwise.
2290
2432
  """
2291
-
2433
+ short_addr = self._shorten_addr(node)
2292
2434
  if verbose:
2293
- self.P("Waiting for node '{}' to appear online...".format(node))
2435
+ self.Pd("Waiting for node '{}' to appear online...".format(short_addr))
2294
2436
 
2295
2437
  _start = tm()
2296
2438
  found = self.check_node_online(node)
@@ -2301,9 +2443,9 @@ class GenericSession(BaseDecentrAIObject):
2301
2443
 
2302
2444
  if verbose:
2303
2445
  if found:
2304
- self.P("Node '{}' is online.".format(node))
2446
+ self.P("Node '{}' is online.".format(short_addr))
2305
2447
  else:
2306
- self.P("Node '{}' did not appear online in {:.1f}s.".format(node, tm() - _start), color='r')
2448
+ self.P("Node '{}' did not appear online in {:.1f}s.".format(short_addr, tm() - _start), color='r')
2307
2449
  return found
2308
2450
 
2309
2451
  def wait_for_node_configs(
@@ -2328,8 +2470,8 @@ class GenericSession(BaseDecentrAIObject):
2328
2470
  bool
2329
2471
  True if the node has its configurations loaded, False otherwise.
2330
2472
  """
2331
-
2332
- self.P("Waiting for node '{}' to have its configurations loaded...".format(node))
2473
+ short_addr = self._shorten_addr(node)
2474
+ self.P("Waiting for node '{}' to have its configurations loaded...".format(short_addr))
2333
2475
 
2334
2476
  _start = tm()
2335
2477
  found = self.check_node_config_received(node)
@@ -2339,7 +2481,7 @@ class GenericSession(BaseDecentrAIObject):
2339
2481
  sleep(0.1)
2340
2482
  found = self.check_node_config_received(node)
2341
2483
  if not found and not additional_request_sent and (tm() - _start) > request_time_thr and attempt_additional_requests:
2342
- self.P("Re-requesting configurations of node '{}'...".format(node), show=True)
2484
+ self.P("Re-requesting configurations of node '{}'...".format(short_addr), show=True)
2343
2485
  node_addr = self.__get_node_address(node)
2344
2486
  self.__request_pipelines_from_net_config_monitor(node_addr)
2345
2487
  additional_request_sent = True
@@ -2347,9 +2489,9 @@ class GenericSession(BaseDecentrAIObject):
2347
2489
 
2348
2490
  if verbose:
2349
2491
  if found:
2350
- self.P(f"Received configurations of node '{node}'.")
2492
+ self.P(f"Received configurations of node '{short_addr}'.")
2351
2493
  else:
2352
- self.P(f"Node '{node}' did not send configs in {(tm() - _start)}. Client might not be authorized!", color='r')
2494
+ self.P(f"Node '{short_addr}' did not send configs in {(tm() - _start)}. Client might not be authorized!", color='r')
2353
2495
  return found
2354
2496
 
2355
2497
  def check_node_config_received(self, node):
@@ -2624,6 +2766,7 @@ class GenericSession(BaseDecentrAIObject):
2624
2766
  name,
2625
2767
  signature=PLUGIN_SIGNATURES.TELEGRAM_BASIC_BOT_01,
2626
2768
  message_handler=None,
2769
+ processor_handler=None,
2627
2770
  telegram_bot_token=None,
2628
2771
  telegram_bot_token_env_key=ENVIRONMENT.TELEGRAM_BOT_TOKEN_ENV_KEY,
2629
2772
  telegram_bot_name=None,
@@ -2648,6 +2791,10 @@ class GenericSession(BaseDecentrAIObject):
2648
2791
  message_handler : callable, optional
2649
2792
  The message handler function that will be called when a message is received. Defaults to None.
2650
2793
 
2794
+ processor_handler : callable, optional
2795
+ The processor handler function that will be called in a processing loop within the
2796
+ Telegram bot plugin in parallel with the message handler. Defaults to None.
2797
+
2651
2798
  telegram_bot_token : str, optional
2652
2799
  The Telegram bot token. Defaults to None.
2653
2800
 
@@ -2687,6 +2834,11 @@ class GenericSession(BaseDecentrAIObject):
2687
2834
  )
2688
2835
 
2689
2836
  func_name, func_args, func_base64_code = pipeline._get_method_data(message_handler)
2837
+
2838
+ proc_func_args, proc_func_base64_code =[], None
2839
+ if processor_handler is not None:
2840
+ _, proc_func_args, proc_func_base64_code = pipeline._get_method_data(processor_handler)
2841
+
2690
2842
  if len(func_args) != 2:
2691
2843
  raise ValueError("The message handler function must have exactly 3 arguments: `plugin`, `message` and `user`.")
2692
2844
 
@@ -2700,6 +2852,8 @@ class GenericSession(BaseDecentrAIObject):
2700
2852
  message_handler=func_base64_code,
2701
2853
  message_handler_args=func_args, # mandatory message and user
2702
2854
  message_handler_name=func_name, # not mandatory
2855
+ processor_handler=proc_func_base64_code, # not mandatory
2856
+ processor_handler_args=proc_func_args, # not mandatory
2703
2857
  **kwargs
2704
2858
  )
2705
2859
  return pipeline, instance
@@ -3133,3 +3287,132 @@ class GenericSession(BaseDecentrAIObject):
3133
3287
  if df_only:
3134
3288
  return dct_result[SESSION_CT.NETSTATS_REPORT]
3135
3289
  return dct_result
3290
+
3291
+
3292
+ def get_nodes_apps(
3293
+ self,
3294
+ node=None,
3295
+ owner=None,
3296
+ show_full=False,
3297
+ as_json=False,
3298
+ show_errors=False,
3299
+ as_df=False
3300
+ ):
3301
+ """
3302
+ Get the workload status of a node.
3303
+
3304
+ Parameters
3305
+ ----------
3306
+
3307
+ node : str, optional
3308
+ The address or name of the Naeural Edge Protocol edge node. Defaults to None.
3309
+
3310
+ owner : str, optional
3311
+ The owner of the apps to filter. Defaults to None.
3312
+
3313
+ show_full : bool, optional
3314
+ If True, will show the full configuration of the apps. Defaults to False.
3315
+
3316
+ as_json : bool, optional
3317
+ If True, will return the result as a JSON. Defaults to False.
3318
+
3319
+ show_errors : bool, optional
3320
+ If True, will show the errors. Defaults to False.
3321
+
3322
+ as_df : bool, optional
3323
+ If True, will return the result as a Pandas DataFrame. Defaults to False.
3324
+
3325
+
3326
+ Returns
3327
+ -------
3328
+
3329
+ list
3330
+ A list of dictionaries containing the workload status
3331
+ of the specified node.
3332
+
3333
+
3334
+ """
3335
+ lst_plugin_instance_data = []
3336
+ if node is None:
3337
+ nodes = self.get_active_nodes()
3338
+ else:
3339
+ nodes = [node]
3340
+ found_nodes = []
3341
+ for node in nodes:
3342
+ short_addr = self._shorten_addr(node)
3343
+ # 2. Wait for node to appear online
3344
+ node_found = self.wait_for_node(node)
3345
+ if node_found:
3346
+ found_nodes.append(node)
3347
+
3348
+ # 3. Check if the node is peered with the client
3349
+ is_allowed = self.is_peered(node)
3350
+ if not is_allowed:
3351
+ if show_errors:
3352
+ log_with_color(f"Node {short_addr} is not peered with this client. Skipping..", color='r')
3353
+ continue
3354
+
3355
+ # 4. Wait for node to send the configuration.
3356
+ self.wait_for_node_configs(node)
3357
+ apps = self.get_active_pipelines(node)
3358
+ if apps is None:
3359
+ if show_errors:
3360
+ log_with_color(f"No apps found on node {short_addr}. Client might not be authorized", color='r')
3361
+ continue
3362
+
3363
+ # 5. Maybe exclude admin application.
3364
+ if not show_full:
3365
+ apps = {k: v for k, v in apps.items() if str(k).lower() != 'admin_pipeline'}
3366
+
3367
+ # 6. Show the apps
3368
+ if as_json:
3369
+ # Will print a big JSON with all the app configurations.
3370
+ lst_plugin_instance_data.append({k: v.get_full_config() for k, v in apps.items()})
3371
+ else:
3372
+ for pipeline_name, pipeline in apps.items():
3373
+ pipeline_owner = pipeline.config.get("INITIATOR_ADDR")
3374
+ if owner is not None and owner != pipeline_owner:
3375
+ continue
3376
+ pipeline_alias = pipeline.config.get("INITIATOR_ID")
3377
+ for instance in pipeline.lst_plugin_instances:
3378
+ instance_status = instance.get_status()
3379
+ if len(instance_status) == 0:
3380
+ # this instance is only present in config but is NOT loaded so ignore it
3381
+ continue
3382
+ start_time = instance_status.get('INIT_TIMESTAMP')
3383
+ last_probe = instance_status.get('EXEC_TIMESTAMP')
3384
+ last_data = instance_status.get('LAST_PAYLOAD_TIME')
3385
+ dates = [start_time, last_probe, last_data]
3386
+ for i in range(len(dates)):
3387
+ if isinstance(dates[i], str):
3388
+ if dates[i].startswith('1970'):
3389
+ dates[i] = 'Never'
3390
+ elif '.' in dates[i]:
3391
+ dates[i] = dates[i].split('.')[0]
3392
+ lst_plugin_instance_data.append({
3393
+ 'Node' : node,
3394
+ 'Owner' : pipeline_owner,
3395
+ 'Alias' : pipeline_alias,
3396
+ 'App': pipeline_name,
3397
+ 'Plugin': instance.signature,
3398
+ 'Id': instance.instance_id,
3399
+ 'Start' : dates[0],
3400
+ 'Probe' : dates[1],
3401
+ 'Data' : dates[2],
3402
+ })
3403
+ # endfor instances in app
3404
+ # endfor apps
3405
+ # endif as_json or as dict-for-df
3406
+ # endfor nodes
3407
+ if len(found_nodes) == 0:
3408
+ log_with_color(f'Node(s) {nodes} not found. Please check the configuration.', color='r')
3409
+ return
3410
+ if as_df:
3411
+ df = pd.DataFrame(lst_plugin_instance_data)
3412
+ if not (df.empty or df.shape[0] == 0):
3413
+ df['Node'] = df['Node'].apply(lambda x: self._shorten_addr(x))
3414
+ df['Owner'] = df['Owner'].apply(lambda x: self._shorten_addr(x))
3415
+ # end if not empty
3416
+ return df
3417
+ return lst_plugin_instance_data
3418
+