naeural-client 3.1.4__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.
- naeural_client/_ver.py +1 -1
- naeural_client/base/generic_session.py +356 -73
- naeural_client/base/instance.py +38 -0
- naeural_client/base/pipeline.py +82 -6
- naeural_client/base_decentra_object.py +4 -2
- naeural_client/bc/base.py +12 -8
- naeural_client/bc/evm.py +10 -1
- naeural_client/cli/cli.py +1 -0
- naeural_client/cli/cli_commands.py +19 -2
- naeural_client/cli/nodes.py +100 -8
- naeural_client/cli/oracles.py +6 -0
- naeural_client/comm/mqtt_wrapper.py +4 -2
- naeural_client/const/base.py +2 -0
- naeural_client/const/payload.py +7 -1
- naeural_client/utils/config.py +64 -75
- naeural_client/utils/oracle_sync/oracle_tester.py +1 -1
- {naeural_client-3.1.4.dist-info → naeural_client-3.2.0.dist-info}/METADATA +4 -4
- {naeural_client-3.1.4.dist-info → naeural_client-3.2.0.dist-info}/RECORD +21 -21
- {naeural_client-3.1.4.dist-info → naeural_client-3.2.0.dist-info}/WHEEL +0 -0
- {naeural_client-3.1.4.dist-info → naeural_client-3.2.0.dist-info}/entry_points.txt +0 -0
- {naeural_client-3.1.4.dist-info → naeural_client-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,
|
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=
|
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
|
-
|
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=
|
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
|
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
|
-
|
607
|
-
|
608
|
-
]
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
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
|
-
|
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
|
-
|
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 <{
|
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(
|
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(
|
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(
|
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
|
-
|
676
|
-
node_addr, config
|
677
|
-
|
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
|
-
|
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 <{
|
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"
|
850
|
-
|
851
|
-
|
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("<
|
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.
|
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"<
|
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"<
|
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(
|
906
|
-
|
907
|
-
|
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'<
|
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 ~/.
|
1333
|
-
if not load_user_defined_config(verbose=not self.
|
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
|
-
|
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
|
-
|
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(
|
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.
|
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(
|
2446
|
+
self.P("Node '{}' is online.".format(short_addr))
|
2305
2447
|
else:
|
2306
|
-
self.P("Node '{}' did not appear online in {:.1f}s.".format(
|
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(
|
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(
|
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 '{
|
2492
|
+
self.P(f"Received configurations of node '{short_addr}'.")
|
2351
2493
|
else:
|
2352
|
-
self.P(f"Node '{
|
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
|
+
|