naeural-client 2.6.14__py3-none-any.whl → 2.6.16__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 CHANGED
@@ -1,4 +1,4 @@
1
- __VER__ = "2.6.14"
1
+ __VER__ = "2.6.16"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  with open("pyproject.toml", "rt") as fd:
@@ -23,7 +23,7 @@ from ..bc import DefaultBlockEngine, _DotDict
23
23
  from ..const import (
24
24
  COMMANDS, ENVIRONMENT, HB, PAYLOAD_DATA, STATUS_TYPE,
25
25
  PLUGIN_SIGNATURES, DEFAULT_PIPELINES,
26
- BLOCKCHAIN_CONFIG, SESSION_CT
26
+ BLOCKCHAIN_CONFIG, SESSION_CT, NET_CONFIG
27
27
  )
28
28
  from ..const import comms as comm_ct
29
29
  from ..io_formatter import IOFormatterWrapper
@@ -366,24 +366,33 @@ class GenericSession(BaseDecentrAIObject):
366
366
  """
367
367
  # check if payload is encrypted
368
368
  if dict_msg.get(PAYLOAD_DATA.EE_IS_ENCRYPTED, False):
369
- encrypted_data = dict_msg.get(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
370
- sender_addr = dict_msg.get(comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR, None)
369
+ destination = dict_msg.get(PAYLOAD_DATA.EE_DESTINATION, [])
370
+ if not isinstance(destination, list):
371
+ destination = [destination]
372
+ if self.bc_engine.contains_current_address(destination):
371
373
 
372
- str_data = self.bc_engine.decrypt(encrypted_data, sender_addr)
374
+ encrypted_data = dict_msg.get(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
375
+ sender_addr = dict_msg.get(comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR, None)
373
376
 
374
- if str_data is None:
375
- self.D("Cannot decrypt message, dropping..\n{}".format(str_data), verbosity=2)
376
- return None
377
+ str_data = self.bc_engine.decrypt(encrypted_data, sender_addr)
377
378
 
378
- try:
379
- dict_data = json.loads(str_data)
380
- except Exception as e:
381
- self.P("Error while decrypting message: {}".format(e), color='r', verbosity=1)
382
- self.D("Message: {}".format(str_data), verbosity=2)
383
- return None
379
+ if str_data is None:
380
+ self.D("Cannot decrypt message, dropping..\n{}".format(str_data), verbosity=2)
381
+ return None
384
382
 
385
- dict_msg = {**dict_data, **dict_msg}
386
- dict_msg.pop(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
383
+ try:
384
+ dict_data = json.loads(str_data)
385
+ except Exception as e:
386
+ self.P("Error while decrypting message: {}".format(e), color='r', verbosity=1)
387
+ self.D("Message: {}".format(str_data), verbosity=2)
388
+ return None
389
+
390
+ dict_msg = {**dict_data, **dict_msg}
391
+ dict_msg.pop(PAYLOAD_DATA.EE_ENCRYPTED_DATA, None)
392
+ else:
393
+ payload_path = dict_msg.get(PAYLOAD_DATA.EE_PAYLOAD_PATH, None)
394
+ self.D(f"Message {payload_path} is encrypted but not for this address.", verbosity=2)
395
+ # endif message for us
387
396
  # end if encrypted
388
397
 
389
398
  formatter = self.formatter_wrapper \
@@ -498,6 +507,56 @@ class GenericSession(BaseDecentrAIObject):
498
507
  self._dct_can_send_to_node[node_addr] = not node_secured or client_is_allowed or self.bc_engine.address == node_addr
499
508
  return
500
509
 
510
+ def send_encrypted_payload(self, node_addr, payload, **kwargs):
511
+ """
512
+ TODO: move in BlockChainEngine
513
+ Send an encrypted payload to a node.
514
+
515
+ Parameters
516
+ ----------
517
+ node_addr : str
518
+ The address of the edge node that will receive the message.
519
+ payload : dict
520
+ The payload dict to be sent.
521
+ **kwargs : dict
522
+ Additional data to be sent to __prepare_message.
523
+ """
524
+ msg_to_send = self.__prepare_message(
525
+ msg_data=payload,
526
+ encrypt_message=True,
527
+ destination=node_addr,
528
+ **kwargs
529
+ )
530
+ self.bc_engine.sign(msg_to_send)
531
+ if not self.silent:
532
+ self.P(f'Sending encrypted payload to <{node_addr}>', color='y')
533
+ self._send_payload(msg_to_send)
534
+ return
535
+
536
+ def __request_pipelines_from_net_config_monitor(self, node_addr):
537
+ """
538
+ Request the pipelines for a node from the net-config monitor plugin instance.
539
+ Parameters
540
+ ----------
541
+ node_addr : str
542
+ The address of the edge node that sent the message.
543
+
544
+ """
545
+ payload = {
546
+ NET_CONFIG.NET_CONFIG_DATA: {
547
+ NET_CONFIG.OPERATION: NET_CONFIG.REQUEST_COMMAND,
548
+ NET_CONFIG.DESTINATION: node_addr,
549
+ }
550
+ }
551
+ additional_data = {
552
+ PAYLOAD_DATA.EE_PAYLOAD_PATH: [self.bc_engine.address, DEFAULT_PIPELINES.ADMIN_PIPELINE, PLUGIN_SIGNATURES.NET_CONFIG_MONITOR, None]
553
+ }
554
+ self.send_encrypted_payload(
555
+ node_addr=node_addr, payload=payload,
556
+ additional_data=additional_data
557
+ )
558
+ return
559
+
501
560
  def __track_allowed_node_by_netmon(self, node_addr, dict_msg):
502
561
  """
503
562
  Track if this session is allowed to send messages to node using net-mon data
@@ -505,7 +564,7 @@ class GenericSession(BaseDecentrAIObject):
505
564
  Parameters
506
565
  ----------
507
566
  node_addr : str
508
- The address of the Naeural Edge Protocol edge node that sent the message.
567
+ The address of the edge node that sent the message.
509
568
 
510
569
  dict_msg : dict
511
570
  The message received from the communication server as a heartbeat of the object from netconfig
@@ -514,8 +573,12 @@ class GenericSession(BaseDecentrAIObject):
514
573
  node_secured = dict_msg.get(PAYLOAD_DATA.NETMON_NODE_SECURED, False)
515
574
 
516
575
  client_is_allowed = self.bc_engine.contains_current_address(node_whitelist)
576
+ can_send = not node_secured or client_is_allowed or self.bc_engine.address == node_addr
577
+ if node_addr not in self._dct_can_send_to_node and can_send:
578
+ self.__request_pipelines_from_net_config_monitor(node_addr)
579
+ # endif node seen for the first time
517
580
 
518
- self._dct_can_send_to_node[node_addr] = not node_secured or client_is_allowed or self.bc_engine.address == node_addr
581
+ self._dct_can_send_to_node[node_addr] = can_send
519
582
  return
520
583
 
521
584
 
@@ -523,6 +586,9 @@ class GenericSession(BaseDecentrAIObject):
523
586
  """
524
587
  Given a list of pipeline configurations, create or update the pipelines for a node.
525
588
  """
589
+ new_pipelines = []
590
+ if node_addr not in self._dct_online_nodes_pipelines:
591
+ self._dct_online_nodes_pipelines[node_addr] = {}
526
592
  for config in pipelines:
527
593
  pipeline_name = config[PAYLOAD_DATA.NAME]
528
594
  pipeline: Pipeline = self._dct_online_nodes_pipelines[node_addr].get(pipeline_name, None)
@@ -531,7 +597,8 @@ class GenericSession(BaseDecentrAIObject):
531
597
  else:
532
598
  self._dct_online_nodes_pipelines[node_addr][pipeline_name] = self.__create_pipeline_from_config(
533
599
  node_addr, config)
534
- return
600
+ new_pipelines.append(self._dct_online_nodes_pipelines[node_addr][pipeline_name])
601
+ return new_pipelines
535
602
 
536
603
  def __on_heartbeat(self, dict_msg: dict, msg_node_addr, msg_pipeline, msg_signature, msg_instance):
537
604
  """
@@ -571,14 +638,11 @@ class GenericSession(BaseDecentrAIObject):
571
638
  # as the protocol should NOT send a heartbeat with active configs to
572
639
  # the entire network, only to the interested parties via net-config
573
640
 
574
- # default action
575
- if msg_node_addr not in self._dct_online_nodes_pipelines:
576
- # this is ok here although we dont get the pipelines from the heartbeat
577
- self._dct_online_nodes_pipelines[msg_node_addr] = {}
578
-
579
641
  if len(msg_active_configs) > 0:
580
642
  # this is for legacy and custom implementation where heartbeats still contain
581
643
  # the pipeline configuration.
644
+ pipeline_names = [x.get(PAYLOAD_DATA.NAME, None) for x in msg_active_configs]
645
+ self.D(f'<HB>Received pipelines from <{msg_node_addr}>:{pipeline_names}', color='y')
582
646
  self.__process_node_pipelines(msg_node_addr, msg_active_configs)
583
647
 
584
648
  # TODO: move this call in `__on_message_default_callback`
@@ -699,8 +763,7 @@ class GenericSession(BaseDecentrAIObject):
699
763
  # end if current_network is valid
700
764
  # end if NET_MON_01
701
765
  return
702
-
703
-
766
+
704
767
  def __maybe_process_net_config(
705
768
  self,
706
769
  dict_msg: dict,
@@ -708,7 +771,39 @@ class GenericSession(BaseDecentrAIObject):
708
771
  msg_signature : str,
709
772
  sender_addr: str,
710
773
  ):
711
- return
774
+ # TODO: bleo if session is in debug mode then for each net-config show what pipelines have
775
+ # been received
776
+ REQUIRED_PIPELINE = DEFAULT_PIPELINES.ADMIN_PIPELINE
777
+ REQUIRED_SIGNATURE = PLUGIN_SIGNATURES.NET_CONFIG_MONITOR
778
+ if msg_pipeline.lower() == REQUIRED_PIPELINE.lower() and msg_signature.upper() == REQUIRED_SIGNATURE.upper():
779
+ # extract data
780
+ sender_addr = dict_msg.get(PAYLOAD_DATA.EE_SENDER, None)
781
+ receiver = dict_msg.get(PAYLOAD_DATA.EE_DESTINATION, None)
782
+ if not isinstance(receiver, list):
783
+ receiver = [receiver]
784
+ path = dict_msg.get(PAYLOAD_DATA.EE_PAYLOAD_PATH, [None, None, None, None])
785
+ ee_id = dict_msg.get(PAYLOAD_DATA.EE_ID, None)
786
+
787
+ # check if I am allowed to see this payload
788
+ if not self.bc_engine.contains_current_address(receiver):
789
+ self.P(f"Received net-config from <{sender_addr}> `{ee_id}` but I am not in the receiver list: {receiver}", color='d')
790
+ return
791
+
792
+ # decrypt payload
793
+ is_encrypted = dict_msg.get(PAYLOAD_DATA.EE_IS_ENCRYPTED, False)
794
+ if not is_encrypted:
795
+ self.P(f"Received net-config from <{sender_addr}> `{ee_id}` but it is not encrypted", color='r')
796
+ return
797
+ net_config_data = dict_msg.get(NET_CONFIG.NET_CONFIG_DATA, {})
798
+ received_pipelines = net_config_data.get('PIPELINES', [])
799
+ self.D(f"Received {len(received_pipelines)} pipelines from <{sender_addr}>")
800
+ new_pipelines = self.__process_node_pipelines(sender_addr, received_pipelines)
801
+ pipeline_names = [x.name for x in new_pipelines]
802
+ self.P(f'<NETCFG>Received pipelines from <{sender_addr}>:{pipeline_names}', color='y')
803
+ self.D(f'[DEBUG][NETCFG]Received pipelines from <{sender_addr}>:\n{new_pipelines}', color='y')
804
+
805
+ # load with same method as a hb
806
+ return True
712
807
 
713
808
 
714
809
  # TODO: maybe convert dict_msg to Payload object
@@ -908,15 +1003,38 @@ class GenericSession(BaseDecentrAIObject):
908
1003
  """
909
1004
  raise NotImplementedError
910
1005
 
911
- def _send_payload(self, to, payload):
1006
+ def _send_raw_message(self, to, msg, communicator='default'):
912
1007
  """
913
- Send a payload to a node.
1008
+ Send a message to a node.
914
1009
 
915
1010
  Parameters
916
1011
  ----------
917
1012
  to : str
918
- The name of the Naeural Edge Protocol edge node that will receive the payload.
919
- payload : dict
1013
+ The name of the Naeural Edge Protocol edge node that will receive the message.
1014
+ msg : dict or str
1015
+ The message to send.
1016
+ """
1017
+ raise NotImplementedError
1018
+
1019
+ def _send_command(self, to, command):
1020
+ """
1021
+ Send a command to a node.
1022
+
1023
+ Parametersc
1024
+ ----------
1025
+ to : str
1026
+ The name of the Naeural Edge Protocol edge node that will receive the command.
1027
+ command : str or dict
1028
+ The command to send.
1029
+ """
1030
+ raise NotImplementedError
1031
+
1032
+ def _send_payload(self, payload):
1033
+ """
1034
+ Send a payload to a node.
1035
+ Parameters
1036
+ ----------
1037
+ payload : dict or str
920
1038
  The payload to send.
921
1039
  """
922
1040
  raise NotImplementedError
@@ -1241,6 +1359,68 @@ class GenericSession(BaseDecentrAIObject):
1241
1359
  result = aliases.get(node, None)
1242
1360
  return result
1243
1361
 
1362
+ def __prepare_message(
1363
+ self, msg_data, encrypt_message: bool = True,
1364
+ destination: str = None, destination_id: str = None,
1365
+ session_id: str = None, additional_data: dict = None
1366
+ ):
1367
+ """
1368
+ Prepare and maybe encrypt a message for sending.
1369
+ Parameters
1370
+ ----------
1371
+ msg_data : dict
1372
+ The message to send.
1373
+ encrypt_message : bool
1374
+ If True, will encrypt the message.
1375
+ destination : str, optional
1376
+ The destination address, by default None
1377
+ destination_id : str, optional
1378
+ The destination id, by default None
1379
+ additional_data : dict, optional
1380
+ Additional data to send, by default None
1381
+ This has to be dict!
1382
+
1383
+ Returns
1384
+ -------
1385
+ msg_to_send : dict
1386
+ The message to send.
1387
+ """
1388
+ if destination is None and destination_id is not None:
1389
+ # Initial code `str_enc_data = self.bc_engine.encrypt(str_data, destination_id)` could not work under any
1390
+ # circumstances due to the fact that encrypt requires the public key of the receiver not the alias
1391
+ # of the receiver. The code below is a workaround to encrypt the message
1392
+ # TODO: furthermore the code will be migrated to the use of the address of the worker
1393
+ destination = self.get_addr_by_name(destination_id)
1394
+ assert destination is not None, f"Unknown address for id: {destination_id}"
1395
+ # endif only destination_id provided
1396
+
1397
+ # This part is duplicated with the creation of payloads
1398
+ if encrypt_message and destination is not None:
1399
+ str_data = json.dumps(msg_data)
1400
+ str_enc_data = self.bc_engine.encrypt(str_data, destination)
1401
+ msg_data = {
1402
+ comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED: True,
1403
+ comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA: str_enc_data,
1404
+ }
1405
+ else:
1406
+ msg_data[comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED] = False
1407
+ if encrypt_message:
1408
+ msg_data[comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA] = "Error! No receiver address found!"
1409
+ # endif encrypt_message and destination available
1410
+ msg_to_send = {
1411
+ **msg_data,
1412
+ PAYLOAD_DATA.EE_DESTINATION: destination,
1413
+ comm_ct.COMM_SEND_MESSAGE.K_EE_ID: destination_id,
1414
+ comm_ct.COMM_SEND_MESSAGE.K_SESSION_ID: session_id or self.name,
1415
+ comm_ct.COMM_SEND_MESSAGE.K_INITIATOR_ID: self.name,
1416
+ comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR: self.bc_engine.address,
1417
+ comm_ct.COMM_SEND_MESSAGE.K_TIME: dt.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
1418
+ }
1419
+ if additional_data is not None and isinstance(additional_data, dict):
1420
+ msg_to_send.update(additional_data)
1421
+ # endif additional_data provided
1422
+ return msg_to_send
1423
+
1244
1424
  def _send_command_to_box(self, command, worker, payload, show_command=True, session_id=None, **kwargs):
1245
1425
  """
1246
1426
  Send a command to a node.
@@ -1273,37 +1453,12 @@ class GenericSession(BaseDecentrAIObject):
1273
1453
  comm_ct.COMM_SEND_MESSAGE.K_PAYLOAD: payload,
1274
1454
  }
1275
1455
 
1276
- # This part is duplicated with the creation of payloads
1277
- encrypt_payload = self.encrypt_comms
1278
- if encrypt_payload and worker is not None:
1279
- str_data = json.dumps(critical_data)
1280
-
1281
- # Initial code `str_enc_data = self.bc_engine.encrypt(str_data, worker)` could not work under any
1282
- # circumstances due to the fact that encrypt requires the public key of the receiver not the alias
1283
- # of the receiver. The code below is a workaround to encrypt the message
1284
- # TODO: furthermore the code will be migrated to the use of the address of the worker
1285
- worker_addr = self.get_addr_by_name(worker)
1286
- assert worker_addr is not None, f"Unknown worker address: {worker} - {worker_addr}"
1287
-
1288
- str_enc_data = self.bc_engine.encrypt(str_data, worker_addr)
1289
- critical_data = {
1290
- comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED: True,
1291
- comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA: str_enc_data,
1292
- }
1293
- else:
1294
- critical_data[comm_ct.COMM_SEND_MESSAGE.K_EE_IS_ENCRYPTED] = False
1295
- if encrypt_payload:
1296
- critical_data[comm_ct.COMM_SEND_MESSAGE.K_EE_ENCRYPTED_DATA] = "Error! No receiver address found!"
1297
-
1298
- # endif
1299
- msg_to_send = {
1300
- **critical_data,
1301
- comm_ct.COMM_SEND_MESSAGE.K_EE_ID: worker,
1302
- comm_ct.COMM_SEND_MESSAGE.K_SESSION_ID: session_id or self.name,
1303
- comm_ct.COMM_SEND_MESSAGE.K_INITIATOR_ID: self.name,
1304
- comm_ct.COMM_SEND_MESSAGE.K_SENDER_ADDR: self.bc_engine.address,
1305
- comm_ct.COMM_SEND_MESSAGE.K_TIME: dt.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
1306
- }
1456
+ msg_to_send = self.__prepare_message(
1457
+ msg_data=critical_data,
1458
+ encrypt_message=self.encrypt_comms,
1459
+ destination_id=worker,
1460
+ session_id=session_id,
1461
+ )
1307
1462
  self.bc_engine.sign(msg_to_send, use_digest=True)
1308
1463
  if show_command:
1309
1464
  self.P(
@@ -1311,7 +1466,7 @@ class GenericSession(BaseDecentrAIObject):
1311
1466
  color='y',
1312
1467
  verbosity=1
1313
1468
  )
1314
- self._send_payload(worker, msg_to_send)
1469
+ self._send_command(worker, msg_to_send)
1315
1470
  return
1316
1471
 
1317
1472
  def _send_command_create_pipeline(self, worker, pipeline_config, **kwargs):
@@ -2470,6 +2625,35 @@ class GenericSession(BaseDecentrAIObject):
2470
2625
  def client_address(self):
2471
2626
  return self.get_client_address()
2472
2627
 
2628
+ def __wait_for_supervisors_net_mon_data(
2629
+ self,
2630
+ supervisor=None,
2631
+ timeout=10,
2632
+ min_supervisors=2
2633
+ ):
2634
+ # the following loop will wait for the desired number of supervisors to appear online
2635
+ # for the current session
2636
+ start = tm()
2637
+ result = False
2638
+ while (tm() - start) < timeout:
2639
+ if supervisor is not None:
2640
+ if supervisor in self.__current_network_statuses:
2641
+ result = True
2642
+ break
2643
+ elif len(self.__current_network_statuses) >= min_supervisors:
2644
+ result = True
2645
+ break
2646
+ sleep(0.1)
2647
+ elapsed = tm() - start
2648
+ # end while
2649
+ # done waiting for supervisors
2650
+ return result, elapsed
2651
+
2652
+
2653
+ def get_all_nodes_pipelines(self):
2654
+ # TODO: Bleo inject this function in __on_hb and maybe_process_net_config and dump result
2655
+ return self._dct_online_nodes_pipelines
2656
+
2473
2657
  def get_network_known_nodes(
2474
2658
  self,
2475
2659
  timeout=10,
@@ -2540,19 +2724,11 @@ class GenericSession(BaseDecentrAIObject):
2540
2724
  for k in mapping:
2541
2725
  res[k] = []
2542
2726
 
2543
- # the following loop will wait for the desired number of supervisors to appear online
2544
- # for the current session
2545
- start = tm()
2546
- while (tm() - start) < timeout:
2547
- if supervisor is not None:
2548
- if supervisor in self.__current_network_statuses:
2549
- break
2550
- elif len(self.__current_network_statuses) >= min_supervisors:
2551
- break
2552
- sleep(0.1)
2553
- elapsed = tm() - start
2554
- # end while
2555
- # done waiting for supervisors
2727
+ result, elapsed = self.__wait_for_supervisors_net_mon_data(
2728
+ supervisor=supervisor,
2729
+ timeout=timeout,
2730
+ min_supervisors=min_supervisors,
2731
+ )
2556
2732
  best_super = 'ERROR'
2557
2733
  best_super_alias = 'ERROR'
2558
2734
 
naeural_client/bc/base.py CHANGED
@@ -1314,4 +1314,4 @@ class BaseBlockEngine:
1314
1314
  else:
1315
1315
  self.P(f"dAuth URL is not invalid: {url}", color='r')
1316
1316
  #end if url is valid
1317
- return dct_env
1317
+ return dct_env
@@ -10,6 +10,7 @@ class PLUGIN_SIGNATURES:
10
10
  CHAIN_DIST_CUSTOM_JOB_01 = 'PROCESS_REAL_TIME_COLLECTED_DATA_CUSTOM_EXEC_CHAIN_DIST'
11
11
  TELEGRAM_BASIC_BOT_01 = 'TELEGRAM_BASIC_BOT_01'
12
12
  TELEGRAM_CONVERSATIONAL_BOT_01 = 'TELEGRAM_CONVERSATIONAL_BOT_01'
13
+ NET_CONFIG_MONITOR = 'NET_CONFIG_MONITOR'
13
14
  # INSERT_NEW_PLUGIN_HERE
14
15
 
15
16
 
@@ -18,6 +18,9 @@ DAUTH_VARS = [DAUTH_NONCE, BCctbase.SIGN, BCctbase.SENDER, BCctbase.HASH]
18
18
 
19
19
  ETH_ENABLED_ENV_KEY = 'EE_ETH_ENABLED'
20
20
 
21
+ EE_EPOCH_INTERVALS_KEY = 'EE_EPOCH_INTERVALS'
22
+ EE_EPOCH_INTERVAL_SECONDS_KEY = 'EE_EPOCH_INTERVAL_SECONDS'
23
+
21
24
 
22
25
  class LocalInfo:
23
26
  LOCAL_INFO_FILE = 'local_info.json'
@@ -10,7 +10,7 @@ class MqttSession(GenericSession):
10
10
  self._default_communicator = MQTTWrapper(
11
11
  log=self.log,
12
12
  config=self._config,
13
- send_channel_name=comm_ct.COMMUNICATION_CONFIG_CHANNEL,
13
+ send_channel_name=comm_ct.COMMUNICATION_PAYLOADS_CHANNEL,
14
14
  recv_channel_name=comm_ct.COMMUNICATION_PAYLOADS_CHANNEL,
15
15
  comm_type=comm_ct.COMMUNICATION_DEFAULT,
16
16
  recv_buff=self._payload_messages,
@@ -21,6 +21,7 @@ class MqttSession(GenericSession):
21
21
  self._heartbeats_communicator = MQTTWrapper(
22
22
  log=self.log,
23
23
  config=self._config,
24
+ send_channel_name=comm_ct.COMMUNICATION_CONFIG_CHANNEL,
24
25
  recv_channel_name=comm_ct.COMMUNICATION_CTRL_CHANNEL,
25
26
  comm_type=comm_ct.COMMUNICATION_HEARTBEATS,
26
27
  recv_buff=self._hb_messages,
@@ -37,6 +38,11 @@ class MqttSession(GenericSession):
37
38
  connection_name=self.name,
38
39
  verbosity=self._verbosity,
39
40
  )
41
+ self.__communicators = {
42
+ 'default': self._default_communicator,
43
+ 'heartbeats': self._heartbeats_communicator,
44
+ 'notifications': self._notifications_communicator,
45
+ }
40
46
  return super(MqttSession, self).startup()
41
47
 
42
48
  @property
@@ -64,9 +70,18 @@ class MqttSession(GenericSession):
64
70
  self._notifications_communicator.release()
65
71
  return
66
72
 
67
- def _send_payload(self, to, msg):
73
+ def _send_raw_message(self, to, msg, communicator='default'):
68
74
  payload = json.dumps(msg)
75
+ communicator_obj = self.__communicators.get(communicator, self._default_communicator)
76
+ communicator_obj._send_to = to
77
+ communicator_obj.send(payload)
78
+ return
69
79
 
70
- self._default_communicator._send_to = to
71
- self._default_communicator.send(payload)
80
+ def _send_payload(self, payload):
81
+ # `to` parameter will be added after migrating to segregated payloads.
82
+ self._send_raw_message(to=None, msg=payload, communicator='default')
72
83
  return
84
+
85
+ def _send_command(self, to, command):
86
+ self._send_raw_message(to, command, communicator='heartbeats')
87
+ return
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: naeural_client
3
+ Version: 2.6.16
4
+ Summary: `naeural_client` is the Python SDK required for client app development for the Naeural Edge Protocol Edge Protocol framework
5
+ Project-URL: Homepage, https://github.com/NaeuralEdgeProtocol/naeural_client
6
+ Project-URL: Bug Tracker, https://github.com/NaeuralEdgeProtocol/naeural_client/issues
7
+ Author-email: Andrei Ionut Damian <andrei.damian@me.com>, Cristan Bleotiu <cristibleotiu@gmail.com>, Stefan Saraev <saraevstefan@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.8
13
+ Requires-Dist: cryptography>=39.0.0
14
+ Requires-Dist: numpy
15
+ Requires-Dist: paho-mqtt
16
+ Requires-Dist: pandas
17
+ Requires-Dist: pika
18
+ Requires-Dist: pyaml
19
+ Requires-Dist: pyopenssl>=23.0.0
20
+ Requires-Dist: python-dateutil
21
+ Requires-Dist: web3
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Ratio1 SDK
25
+
26
+ Welcome to the **Ratio1 SDK** repository, formerly known as the **naeural_client SDK**. The Ratio1 SDK is a crucial component of the Ratio1 ecosystem, designed to facilitate interactions, development, and deployment of jobs within the Ratio1 network. By enabling low-code development, the SDK allows developers to build and deploy end-to-end AI (and beyond) cooperative application pipelines seamlessly within the Ratio1 Edge Nodes ecosystem.
27
+
28
+ ## Overview
29
+
30
+ The **Ratio1 SDK** is engineered to enhance the Ratio1 protocol and ecosystem, aiming to improve the functionality and performance of the Ratio1 Edge Node through dedicated research and community contributions. This SDK serves as an essential tool for developers looking to integrate their applications with the Ratio1 network, enabling them to leverage the decentralized, secure, and privacy-preserving capabilities of Ratio1 Edge Nodes.
31
+
32
+ Key functionalities of the Ratio1 SDK include:
33
+
34
+ - **Job Interactions**: Facilitate the development and management of computation tasks within the Ratio1 network.
35
+ - **Development Tools**: Provide low-code solutions for creating and deploying AI-driven application pipelines.
36
+ - **Ecosystem Integration**: Seamlessly integrate with Ratio1 Edge Nodes to utilize their computational resources effectively.
37
+ - **Collaboration and Deployment**: Enable cooperative application development and deployment across multiple edge nodes within the Ratio1 ecosystem.
38
+
39
+ Unlike the Ratio1 Core Packages, which are intended solely for protocol and ecosystem enhancements and are not meant for standalone installation, the Ratio1 SDK is designed for both client-side development and sending workloads to Ratio1 Edge Nodes, making it an indispensable tool for developers within the ecosystem.
40
+
41
+ ## Dependencies
42
+
43
+ The Ratio1 SDK relies on several key packages to function effectively. These dependencies are automatically managed when installing the SDK via pip:
44
+
45
+ - `pika`
46
+ - `paho-mqtt`
47
+ - `numpy`
48
+ - `pyopenssl>=23.0.0`
49
+ - `cryptography>=39.0.0`
50
+ - `python-dateutil`
51
+ - `pyaml`
52
+
53
+ ## Installation
54
+
55
+ Installing the Ratio1 SDK is straightforward and is intended for development and integration into your projects. Use the following pip commands to install the SDK:
56
+
57
+ ### Standard Installation
58
+
59
+ To install the Ratio1 SDK, run:
60
+
61
+ ```shell
62
+ pip install ratio1_sdk --upgrade
63
+ ```
64
+
65
+ ### Development Installation
66
+
67
+ For development purposes, you can clone the repository and set up the SDK in an editable mode:
68
+
69
+ ```shell
70
+ git clone https://github.com/Ratio1/ratio1_sdk
71
+ cd ratio1_sdk
72
+ pip install -e .
73
+ ```
74
+
75
+ This allows you to make modifications to the SDK and have them reflected immediately without reinstalling.
76
+
77
+ ## Documentation
78
+
79
+ Comprehensive documentation for the Ratio1 SDK is currently a work in progress. Minimal documentation is available here, with detailed code examples located in the `tutorials` folder within the project's repository. We encourage developers to explore these examples to understand the SDK's capabilities and integration methods.
80
+
81
+ ## Quick Start Guides
82
+
83
+ Starting with version 2.6+, the Ratio1 SDK automatically performs self-configuration using **dAuth**—the Ratio1 decentralized self-authentication system. To begin integrating with the Ratio1 network, follow these steps:
84
+
85
+ ### 1. Start a Local Edge Node
86
+
87
+ Launch a local Ratio1 Edge Node using Docker:
88
+
89
+ ```bash
90
+ docker run -d --name=local_node ratio1/edge_node:develop
91
+ ```
92
+
93
+ After a few seconds, the node will be online. Retrieve the node's address by running:
94
+
95
+ ```bash
96
+ docker exec local_node get_node_info
97
+ ```
98
+
99
+ The output will resemble:
100
+
101
+ ```json
102
+ {
103
+ "address": "0xai_AtMvIwaEPi5M8cnkdbaZ3tbUhCzKbGKEYuZ1xFtCjT_6",
104
+ "alias": "6dd74472642e",
105
+ "eth_address": "0x98FE7c0d8CeC2E97B932D2bDC1bb73B395C9Dfd7"
106
+ }
107
+ ```
108
+
109
+ ### 2. Develop and Deploy Jobs
110
+
111
+ Use the SDK to develop and send workloads to the Edge Nodes. Below are examples of both local and remote execution.
112
+
113
+ ## Examples
114
+
115
+ ### Local Execution
116
+
117
+ This example demonstrates how to find all 168 prime numbers in the interval 1 - 1000 using local execution. The code leverages multiple threads to perform prime number generation efficiently.
118
+
119
+ ```python
120
+ import numpy as np
121
+ from concurrent.futures import ThreadPoolExecutor
122
+
123
+ def local_brute_force_prime_number_generator():
124
+ def is_prime(n):
125
+ if n <= 1:
126
+ return False
127
+ for i in range(2, int(np.sqrt(n)) + 1):
128
+ if n % i == 0:
129
+ return False
130
+ return True
131
+
132
+ random_numbers = np.random.randint(1, 1000, 20)
133
+
134
+ thread_pool = ThreadPoolExecutor(max_workers=4)
135
+ are_primes = list(thread_pool.map(is_prime, random_numbers))
136
+
137
+ prime_numbers = []
138
+ for i in range(len(random_numbers)):
139
+ if are_primes[i]:
140
+ prime_numbers.append(random_numbers[i])
141
+
142
+ return prime_numbers
143
+
144
+ if __name__ == "__main__":
145
+ found_so_far = []
146
+ print_step = 0
147
+
148
+ while len(found_so_far) < 168:
149
+ # Compute a batch of prime numbers
150
+ prime_numbers = local_brute_force_prime_number_generator()
151
+
152
+ # Keep only the new prime numbers
153
+ for prime_number in prime_numbers:
154
+ if prime_number not in found_so_far:
155
+ found_so_far.append(prime_number)
156
+
157
+ # Show progress
158
+ if print_step % 50 == 0:
159
+ print("Found so far: {}: {}\n".format(len(found_so_far), sorted(found_so_far)))
160
+
161
+ print_step += 1
162
+
163
+ # Show final result
164
+ print("Found so far: {}: {}\n".format(len(found_so_far), sorted(found_so_far)))
165
+ ```
166
+
167
+ ### Remote Execution
168
+
169
+ To accelerate prime number discovery, this example demonstrates deploying the task across multiple edge nodes within the Ratio1 network. Minimal code changes are required to transition from local to remote execution.
170
+
171
+ #### 1. Modify the Prime Number Generator
172
+
173
+ ```python
174
+ from ratio1_sdk import CustomPluginTemplate
175
+
176
+ def remote_brute_force_prime_number_generator(plugin: CustomPluginTemplate):
177
+ def is_prime(n):
178
+ if n <= 1:
179
+ return False
180
+ for i in range(2, int(plugin.np.sqrt(n)) + 1):
181
+ if n % i == 0:
182
+ return False
183
+ return True
184
+
185
+ random_numbers = plugin.np.random.randint(1, 1000, 20)
186
+ are_primes = plugin.threadapi_map(is_prime, random_numbers, n_threads=4)
187
+
188
+ prime_numbers = []
189
+ for i in range(len(random_numbers)):
190
+ if are_primes[i]:
191
+ prime_numbers.append(random_numbers[i])
192
+
193
+ return prime_numbers
194
+ ```
195
+
196
+ #### 2. Connect to the Network and Select a Node
197
+
198
+ ```python
199
+ from ratio1_sdk import Session
200
+ from time import sleep
201
+
202
+ def on_heartbeat(session: Session, node: str, heartbeat: dict):
203
+ session.P("{} is online".format(node))
204
+ return
205
+
206
+ if __name__ == '__main__':
207
+ session = Session(
208
+ on_heartbeat=on_heartbeat
209
+ )
210
+
211
+ # Run the program for 15 seconds to detect online nodes
212
+ sleep(15)
213
+
214
+ # Retrieve and select an online node
215
+ node = "0xai_A8SY7lEqBtf5XaGyB6ipdk5C30vSf3HK4xELp3iplwLe" # naeural-1
216
+ ```
217
+
218
+ #### 3. Deploy the Distributed Job
219
+
220
+ ```python
221
+ from ratio1_sdk import DistributedCustomCodePresets as Presets
222
+
223
+ _, _ = session.create_chain_dist_custom_job(
224
+ node=node,
225
+ main_node_process_real_time_collected_data=Presets.PROCESS_REAL_TIME_COLLECTED_DATA__KEEP_UNIQUES_IN_AGGREGATED_COLLECTED_DATA,
226
+ main_node_finish_condition=Presets.FINISH_CONDITION___AGGREGATED_DATA_MORE_THAN_X,
227
+ main_node_finish_condition_kwargs={"X": 167},
228
+ main_node_aggregate_collected_data=Presets.AGGREGATE_COLLECTED_DATA___AGGREGATE_COLLECTED_DATA,
229
+ nr_remote_worker_nodes=2,
230
+ worker_node_code=remote_brute_force_prime_number_generator,
231
+ on_data=locally_process_partial_results,
232
+ deploy=True
233
+ )
234
+ ```
235
+
236
+ #### 4. Close the Session Upon Completion
237
+
238
+ ```python
239
+ # Wait until the finished flag is set to True
240
+ session.run(wait=lambda: not finished, close_pipelines=True)
241
+ ```
242
+
243
+ ## Project Financing Disclaimer
244
+
245
+ This project incorporates open-source components developed with the support of financing grants **SMIS 143488** and **SMIS 156084**, provided by the Romanian Competitiveness Operational Programme. We extend our sincere gratitude for this support, which has been instrumental in advancing our work and enabling us to share these resources with the community.
246
+
247
+ The content and information within this repository are solely the responsibility of the authors and do not necessarily reflect the views of the funding agencies. The grants have specifically supported certain aspects of this open-source project, facilitating broader dissemination and collaborative development.
248
+
249
+ For any inquiries regarding the funding and its impact on this project, please contact the authors directly.
250
+
251
+ ## License
252
+
253
+ This project is licensed under the **Apache 2.0 License**. For more details, please refer to the [LICENSE](LICENSE) file.
254
+
255
+ ## Contact
256
+
257
+ For more information, visit our website at [https://ratio1.ai](https://ratio1.ai) or reach out to us via email at [support@ratio1.ai](mailto:support@ratio1.ai).
258
+
259
+ ## Citation
260
+
261
+ If you use the Ratio1 SDK in your research or projects, please cite it as follows:
262
+
263
+ ```bibtex
264
+ @misc{Ratio1SDK,
265
+ author = {Ratio1.AI},
266
+ title = {Ratio1 SDK},
267
+ year = {2024-2025},
268
+ howpublished = {\url{https://github.com/NaeuralEdgeProtocol/naeural_client}},
269
+ }
270
+ ```
271
+
272
+ ```bibtex
273
+ @misc{Ratio1EdgeNode,
274
+ author = {Ratio1.AI},
275
+ title = {Ratio1: Edge Node},
276
+ year = {2024-2025},
277
+ howpublished = {\url{https://github.com/NaeuralEdgeProtocol/edge_node}},
278
+ }
279
+ ```
@@ -1,10 +1,10 @@
1
1
  naeural_client/__init__.py,sha256=YimqgDbjLuywsf8zCWE0EaUXH4MBUrqLxt0TDV558hQ,632
2
- naeural_client/_ver.py,sha256=suKXv38GGYBJ0RcB_6qcC-hc1VP14y80oKWh1Ov0_Xg,331
2
+ naeural_client/_ver.py,sha256=eM7t0xRbVJaaGjrh0O9tmQF7K_usWv_3wk3hVZ27_Qs,331
3
3
  naeural_client/base_decentra_object.py,sha256=C4iwZTkhKNBS4VHlJs5DfElRYLo4Q9l1V1DNVSk1fyQ,4412
4
4
  naeural_client/plugins_manager_mixin.py,sha256=X1JdGLDz0gN1rPnTN_5mJXR8JmqoBFQISJXmPR9yvCo,11106
5
5
  naeural_client/base/__init__.py,sha256=hACh83_cIv7-PwYMM3bQm2IBmNqiHw-3PAfDfAEKz9A,259
6
6
  naeural_client/base/distributed_custom_code_presets.py,sha256=cvz5R88P6Z5V61Ce1vHVVh8bOkgXd6gve_vdESDNAsg,2544
7
- naeural_client/base/generic_session.py,sha256=p7NZoMZJEW-2Zb0CrJyXIgtlpqZcRW9If6p3R_EfzzE,97509
7
+ naeural_client/base/generic_session.py,sha256=Vw08ywfW03aN0_5kmXOF5-VkRthl7FwAXqJu3M3NDMs,104275
8
8
  naeural_client/base/instance.py,sha256=kcZJmjLBtx8Bjj_ysIOx1JmLA-qSpG7E28j5rq6IYus,20444
9
9
  naeural_client/base/pipeline.py,sha256=b4uNHrEIOlAtw4PGUx20dxwBhDck5__SrVXaHcSi8ZA,58251
10
10
  naeural_client/base/plugin_template.py,sha256=qGaXByd_JZFpjvH9GXNbT7KaitRxIJB6-1IhbKrZjq4,138123
@@ -14,7 +14,7 @@ naeural_client/base/webapp_pipeline.py,sha256=QmPLVmhP0CPdi0YuvbZEH4APYz2Amtw3gy
14
14
  naeural_client/base/payload/__init__.py,sha256=y8fBI8tG2ObNfaXFWjyWZXwu878FRYj_I8GIbHT4GKE,29
15
15
  naeural_client/base/payload/payload.py,sha256=x-au7l67Z_vfn_4R2C_pjZCaFuUVXHngJiGOfIAYVdE,2690
16
16
  naeural_client/bc/__init__.py,sha256=FQj23D1PrY06NUOARiKQi4cdj0-VxnoYgYDEht8lpr8,158
17
- naeural_client/bc/base.py,sha256=hVdk_Vxi94jMy6OJfbYVrbQgMCNNi3BgVOj_NCw5cKc,36159
17
+ naeural_client/bc/base.py,sha256=KC-wtymP9tEvXGG6wfiRirgshjW7_y58ylWjbe_lUWo,36160
18
18
  naeural_client/bc/chain.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  naeural_client/bc/ec.py,sha256=qI8l7YqiS4MNftlx-tF7IZUswrSeQc7KMn5OZ0fEaJs,23370
20
20
  naeural_client/certs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -31,8 +31,8 @@ naeural_client/comm/amqp_wrapper.py,sha256=hzj6ih07DnLQy2VSfA88giDIFHaCp9uSdGLTA
31
31
  naeural_client/comm/mqtt_wrapper.py,sha256=Ig3bFZkCbWd4y_Whn2PPa91Z3aLgNbNPau6Tn5yLPZ8,16167
32
32
  naeural_client/const/README.md,sha256=6OHesr-f5NBuuJGryEoi_TCu2XdlhfQYlDKx_IJoXeg,177
33
33
  naeural_client/const/__init__.py,sha256=MM6Zib6i7M2qWcMkLtLx14zqU-lE-u2uPHjNvbh2jAM,478
34
- naeural_client/const/apps.py,sha256=gIONTZUkqPveu3DwelyJWpbFMeIR9l6DlaNg-xEfK1A,611
35
- naeural_client/const/base.py,sha256=Ld6WuQeqm5FbARL_-xs3gXxpa46ypksHimAk1g4qanw,4467
34
+ naeural_client/const/apps.py,sha256=ePBiJXLuPfFOKuw-LJrT9OWbaodU7QApfDurIPNDoB4,655
35
+ naeural_client/const/base.py,sha256=1ScfnfNd5xeWo7tkkDgQR95vliTpcax025Naj0Lqw38,4574
36
36
  naeural_client/const/comms.py,sha256=La6JXWHexH8CfcBCKyT4fCIoeaoZlcm7KtZ57ab4ZgU,2201
37
37
  naeural_client/const/environment.py,sha256=iytmTDgbOjvORPwHQmc0K0r-xJx7dnnzNnqAJJiFCDA,870
38
38
  naeural_client/const/formatter.py,sha256=AW3bWlqf39uaqV4BBUuW95qKYfF2OkkU4f9hy3kSVhM,200
@@ -47,7 +47,7 @@ naeural_client/default/instance/net_mon_01_plugin.py,sha256=u85i2AiYHkLJnam0wOx-
47
47
  naeural_client/default/instance/telegram_basic_bot_01_plugin.py,sha256=P_W2K_ng7svbhoIoYGvkGxa4v9UUoxuOAAgtoM36wDo,161
48
48
  naeural_client/default/instance/telegram_conversational_bot_01_plugin.py,sha256=XkyE-uNbtFnTSGA1nOf-XNmUipAUjhg0xXV1glRb7gc,179
49
49
  naeural_client/default/instance/view_scene_01_plugin.py,sha256=5kMhd23kL5AYCdOJzrdCqi2ohoQNvmpv8oE6hWQtUWk,720
50
- naeural_client/default/session/mqtt_session.py,sha256=dpQcBhhVZDo458v0IqJMZb1CsTn-TxXhYjNlyJp9Rp8,2414
50
+ naeural_client/default/session/mqtt_session.py,sha256=QdL0LPCqV3Fw0VSNgnsXWKJoxnkl17BtSYvEmhNU_zM,3079
51
51
  naeural_client/io_formatter/__init__.py,sha256=_wy7c-Z9kgb26jN7uNTDq88G7xZ3wI_ObuQd3QWNPkQ,85
52
52
  naeural_client/io_formatter/io_formatter_manager.py,sha256=MiZ70cGVD6hR99QiEk9wvJ_vADxpI-y2bTb0ITlMNI0,3451
53
53
  naeural_client/io_formatter/base/__init__.py,sha256=VmLHY35NCRR46lgjhcrwtFxmJIVOjX2ADV9wOXt9CKA,41
@@ -81,8 +81,8 @@ naeural_client/utils/__init__.py,sha256=mAnke3-MeRzz3nhQvhuHqLnpaaCSmDxicd7Ck9uw
81
81
  naeural_client/utils/comm_utils.py,sha256=4cS9llRr_pK_3rNgDcRMCQwYPO0kcNU7AdWy_LtMyCY,1072
82
82
  naeural_client/utils/config.py,sha256=v7xHikr6Z5Sbvf3opYeMhYzGWD2pe0HlRwa-aGJzUh8,6323
83
83
  naeural_client/utils/dotenv.py,sha256=_AgSo35n7EnQv5yDyu7C7i0kHragLJoCGydHjvOkrYY,2008
84
- naeural_client-2.6.14.dist-info/METADATA,sha256=tI-zL6Ld5JvnTFP9JDWK8sfx788VwVGABqbmCfubxUQ,13176
85
- naeural_client-2.6.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
86
- naeural_client-2.6.14.dist-info/entry_points.txt,sha256=PNdyotDaQBAslZREx5luVyj0kqpQnwNACwkFNTPIHU4,55
87
- naeural_client-2.6.14.dist-info/licenses/LICENSE,sha256=cvOsJVslde4oIaTCadabXnPqZmzcBO2f2zwXZRmJEbE,11311
88
- naeural_client-2.6.14.dist-info/RECORD,,
84
+ naeural_client-2.6.16.dist-info/METADATA,sha256=PaCUsM0cAtvH9ZUTzfwNxZTumPu4np7e3inODMj9WLA,10362
85
+ naeural_client-2.6.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
86
+ naeural_client-2.6.16.dist-info/entry_points.txt,sha256=PNdyotDaQBAslZREx5luVyj0kqpQnwNACwkFNTPIHU4,55
87
+ naeural_client-2.6.16.dist-info/licenses/LICENSE,sha256=cvOsJVslde4oIaTCadabXnPqZmzcBO2f2zwXZRmJEbE,11311
88
+ naeural_client-2.6.16.dist-info/RECORD,,
@@ -1,358 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: naeural_client
3
- Version: 2.6.14
4
- Summary: `naeural_client` is the Python SDK required for client app development for the Naeural Edge Protocol Edge Protocol framework
5
- Project-URL: Homepage, https://github.com/NaeuralEdgeProtocol/naeural_client
6
- Project-URL: Bug Tracker, https://github.com/NaeuralEdgeProtocol/naeural_client/issues
7
- Author-email: Andrei Ionut Damian <andrei.damian@me.com>, Cristan Bleotiu <cristibleotiu@gmail.com>, Stefan Saraev <saraevstefan@gmail.com>
8
- License-File: LICENSE
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.8
13
- Requires-Dist: cryptography>=39.0.0
14
- Requires-Dist: numpy
15
- Requires-Dist: paho-mqtt
16
- Requires-Dist: pandas
17
- Requires-Dist: pika
18
- Requires-Dist: pyaml
19
- Requires-Dist: pyopenssl>=23.0.0
20
- Requires-Dist: python-dateutil
21
- Requires-Dist: web3
22
- Description-Content-Type: text/markdown
23
-
24
- # Ratio1 SDK (naeural_client SDK)
25
-
26
- This is the Python SDK package that allows interactions, development and deployment of jobs in Ratio1 ecosystem formely known as Naeural Edge Protocol network. The SDK enables low-code development and deployment of end-to-end AI (and not only) cooperative application pipelines within the Ratio1 Edge Nodes ecosystem.
27
-
28
- ## Dependencies
29
-
30
- This packet depends and will automatically install the following packets: `pika`, `paho-mqtt`, `numpy`, `pyopenssl>=23.0.0`, `cryptography>=39.0.0`, `python-dateutil`, `pyaml`.
31
-
32
- ## Installation
33
-
34
- ```shell
35
- pip install naeural_client --upgrade
36
- ```
37
-
38
- ### Development installation
39
-
40
- ```shell
41
- git clone https://github.com/NaeuralEdgeProtocol/naeural_client
42
- pip install -e .
43
- ```
44
-
45
- ## Documentation
46
-
47
- Minimal documentation will be presented here. The complete documentation is
48
- Work in Progress.
49
-
50
- Code examples are located in the `tutorials` folder in the project's repository.
51
-
52
- ## Quick start guides
53
-
54
- Starting with version 2.6+ the SDK will automatically perform self-configuration using the dAuth - the Ratio1 decentralized self-authentication system.
55
-
56
- In order to start a local edge node you just need to run:
57
-
58
- ```bash
59
- docker run -d --name=local_node naeural/edge_node:develop
60
- ```
61
- after a few seconds the node will be online and you can get the node's address by running:
62
-
63
- ```bash
64
- docker exec local_node get_node_info
65
- ```
66
-
67
- The output will be similar to:
68
-
69
- ```json
70
- {
71
- "address": "0xai_AtMvIwaEPi5M8cnkdbaZ3tbUhCzKbGKEYuZ1xFtCjT_6",
72
- "alias": "6dd74472642e",
73
- "eth_address": "0x98FE7c0d8CeC2E97B932D2bDC1bb73B395C9Dfd7"
74
- }
75
- ```
76
-
77
-
78
- ## Some Examples
79
-
80
- ### Local Execution
81
-
82
- We want to find all $168$ prime numbers in the interval $1$ - $1000$. For this we can run the following code on our local machine.
83
-
84
- This code has segments running on multiple threads using a ThreadPool.
85
-
86
- ```python
87
- import numpy as np
88
- from concurrent.futures import ThreadPoolExecutor
89
-
90
-
91
- def local_brute_force_prime_number_generator():
92
- def is_prime(n):
93
- if n <= 1:
94
- return False
95
- for i in range(2, int(np.sqrt(n)) + 1):
96
- if n % i == 0:
97
- return False
98
- return True
99
-
100
- random_numbers = np.random.randint(1, 1000, 20)
101
-
102
- thread_pool = ThreadPoolExecutor(max_workers=4)
103
- are_primes = list(thread_pool.map(is_prime, random_numbers))
104
-
105
- prime_numbers = []
106
- for i in range(len(random_numbers)):
107
- if are_primes[i]:
108
- prime_numbers.append(random_numbers[i])
109
-
110
- return prime_numbers
111
-
112
-
113
- if __name__ == "__main__":
114
- found_so_far = []
115
-
116
- print_step = 0
117
-
118
- while len(found_so_far) < 168:
119
- # compute a batch of prime numbers
120
- prime_numbers = local_brute_force_prime_number_generator()
121
-
122
- # keep only the new prime numbers
123
- for prime_number in prime_numbers:
124
- if prime_number not in found_so_far:
125
- found_so_far.append(prime_number)
126
- # end for
127
-
128
- # show progress
129
- if print_step % 50 == 0:
130
- print("Found so far: {}: {}\n".format(len(found_so_far), sorted(found_so_far)))
131
-
132
- print_step += 1
133
- # end while
134
-
135
- # show final result
136
- print("Found so far: {}: {}\n".format(len(found_so_far), sorted(found_so_far)))
137
- ```
138
-
139
- We can see that we have a `local_brute_force_prime_number_generator` method which will generate a random sample of $20$ numbers that will be checked if they are prime or not.
140
-
141
- The rest of the code handles how the numbers generated with this method are kept.
142
- Because we want to find $168$ unique numbers, we append to the list of found primes only the numbers that are not present yet.
143
-
144
- At the end, we want to show a list of all the numbers found.
145
-
146
- ### Remote Execution
147
-
148
- For this example we would like to use multiple edge nodes to find the prime numbers faster.
149
-
150
- To execute this code on our network, a series of changes must be made to the `local_brute_force_prime_number_generator` method.
151
- These changes are the only ones a developer has to do to deploy his own custom code on the network.
152
-
153
- For this, we will create a new method, `remote_brute_force_prime_number_generator`, which will use the exposed edge node API methods.
154
-
155
- ```python
156
- from naeural_client import CustomPluginTemplate
157
-
158
- # through the `plugin` object we get access to the edge node API
159
- # the CustomPluginTemplate class acts as a documentation for all the available methods and attributes
160
- # since we do not allow imports in the custom code due to security reasons, the `plugin` object
161
- # exposes common modules to the user
162
- def remote_brute_force_prime_number_generator(plugin: CustomPluginTemplate):
163
- def is_prime(n):
164
- if n <= 1:
165
- return False
166
- # we use the `plugin.np` instead of the `np` module
167
- for i in range(2, int(plugin.np.sqrt(n)) + 1):
168
- if n % i == 0:
169
- return False
170
- return True
171
-
172
- # we use the `plugin.np` instead of the `np` module
173
- random_numbers = plugin.np.random.randint(1, 1000, 20)
174
-
175
- # we use the `plugin.threadapi_map` instead of the `ThreadPoolExecutor.map`
176
- are_primes = plugin.threadapi_map(is_prime, random_numbers, n_threads=4)
177
-
178
- prime_numbers = []
179
- for i in range(len(random_numbers)):
180
- if are_primes[i]:
181
- prime_numbers.append(random_numbers[i])
182
-
183
- return prime_numbers
184
- ```
185
-
186
- This are all the changes we have to do to deploy this code in the network.
187
-
188
- Now lets connect to the network and see what nodes are online.
189
- We will use the `on_heartbeat` callback to print the nodes.
190
-
191
- ```python
192
- from naeural_client import Session
193
- from time import sleep
194
-
195
- def on_heartbeat(session: Session, node: str, heartbeat: dict):
196
- # the `.P` method is used to print messages in the console and store them in the log file
197
- session.P("{} is online".format(node))
198
- return
199
-
200
-
201
- if __name__ == '__main__':
202
- # create a session
203
- # the network credentials are read from the .env file automatically
204
- session = Session(
205
- on_heartbeat=on_heartbeat
206
- )
207
-
208
- # run the program for 15 seconds to show all the nodes that are online
209
- sleep(15)
210
-
211
- ```
212
-
213
- Next we will select an online node. This node will be our entrypoint in the network.
214
-
215
- The available nodes in our test net are:
216
-
217
- ```
218
- 0xai_A8SY7lEqBtf5XaGyB6ipdk5C30vSf3HK4xELp3iplwLe naeural-1
219
- 0xai_Amfnbt3N-qg2-qGtywZIPQBTVlAnoADVRmSAsdDhlQ-6 naeural-2
220
- 0xai_ApltAljEgWk3g8x2QcSa0sS3hT1P4dyCchd04zFSMy5e naeural-3
221
- ```
222
-
223
- We will send a task to this node. Since we want to distribute the task of finding prime numbers to multiple nodes, this selected node will handle distribution of tasks and collection of the results.
224
-
225
- ```python
226
- node = "0xai_A8SY7lEqBtf5XaGyB6ipdk5C30vSf3HK4xELp3iplwLe" # naeural-1
227
-
228
- # we usually wait for the node to be online before sending the task
229
- # but in this case we are sure that the node is online because we
230
- # have received heartbeats from it during the sleep period
231
-
232
- # session.wait_for_node(node)
233
- ```
234
-
235
- Our selected node will periodically output partial results with the prime numbers found so far by the worker nodes. We want to consume these results.
236
-
237
- Thus, we need to implement a callback method that will handle this.
238
-
239
- ```python
240
- from naeural_client import Pipeline
241
-
242
- # a flag used to close the session when the task is finished
243
- finished = False
244
-
245
- def locally_process_partial_results(pipeline: Pipeline, full_payload):
246
- global finished
247
- found_so_far = full_payload.get("DATA")
248
-
249
- if found_so_far:
250
- pipeline.P("Found so far: {}: {}\n\n".format(len(found_so_far), sorted(found_so_far)))
251
-
252
- progress = full_payload.get("PROGRESS")
253
- if progress == 100:
254
- pipeline.P("FINISHED\n\n")
255
- finished = True
256
-
257
- return
258
- ```
259
-
260
- Now we are ready to deploy our job to the network.
261
-
262
- ```python
263
- from naeural_client import DistributedCustomCodePresets as Presets
264
-
265
- _, _ = session.create_chain_dist_custom_job(
266
- # this is the main node, our entrypoint
267
- node=node,
268
-
269
- # this function is executed on the main node
270
- # this handles what we want to do with primes found by a worker node after an iteration
271
- # we want to store only the unique prime numbers
272
- # we cam either write a custom code to pass here or we can use a preset
273
- main_node_process_real_time_collected_data=Presets.PROCESS_REAL_TIME_COLLECTED_DATA__KEEP_UNIQUES_IN_AGGREGATED_COLLECTED_DATA,
274
-
275
- # this function is executed on the main node
276
- # this handles the finish condition of our distributed job
277
- # we want to finish when we have found 168 prime numbers
278
- # so more than 167 prime numbers
279
- # we cam either write a custom code to pass here or we can use a preset
280
- main_node_finish_condition=Presets.FINISH_CONDITION___AGGREGATED_DATA_MORE_THAN_X,
281
- main_node_finish_condition_kwargs={
282
- "X": 167
283
- },
284
-
285
- # this function is executed on the main node
286
- # this handles the final processing of the results
287
- # this function prepares data for the final result of the distributed job
288
- # we want to aggregate all the prime numbers found by the worker nodes in a single list
289
- # we cam either write a custom code to pass here or we can use a preset
290
- main_node_aggregate_collected_data=Presets.AGGREGATE_COLLECTED_DATA___AGGREGATE_COLLECTED_DATA,
291
-
292
- # how many worker nodes we want to use for this task
293
- nr_remote_worker_nodes=2,
294
-
295
- # this is the function that will be executed on the worker nodes
296
- # this function generates prime numbers using brute force
297
- # we simply pass the function reference
298
- worker_node_code=remote_brute_force_prime_number_generator,
299
-
300
- # this is the function that will be executed on the client
301
- # this is the callback function that processes the partial results
302
- # in our case we want to print the partial results
303
- on_data=locally_process_partial_results,
304
-
305
- # we want to deploy the job immediately
306
- deploy=True
307
- )
308
- ```
309
-
310
- Last but not least, we want to close the session when the distributed job finished.
311
-
312
- ```python
313
- # we wait until the finished flag is set to True
314
- # we want to release the resources allocated on the selected node when the job is finished
315
- session.run(wait=lambda: not finished, close_pipelines=True)
316
- ```
317
-
318
-
319
- # Project Financing Disclaimer
320
-
321
- This project includes open-source components that have been developed with the support of financing grants SMIS 143488 and SMIS 156084, provided by the Romanian Competitiveness Operational Programme. We are grateful for this support, which has enabled us to advance our work and share these resources with the community.
322
-
323
- The content and information provided within this repository are solely the responsibility of the authors and do not necessarily reflect the views of the funding agencies. The funding received under these grants has been instrumental in supporting specific parts of this open source project, allowing for broader dissemination and collaborative development.
324
-
325
- For any inquiries related to the funding and its impact on this project, please contact the authors directly.
326
-
327
-
328
- # Citation
329
-
330
- ```bibtex
331
- @misc{naeural_client,
332
- author = {Stefan Saraev, Andrei Damian},
333
- title = {naeural_client: Python SDK for Naeural Edge Protocol Edge Protocol},
334
- year = {2024},
335
- howpublished = {\url{https://github.com/Naeural Edge ProtocolEdgeProtocol/naeural_client}},
336
- }
337
- ```
338
-
339
- ```bibtex
340
- @misc{project_funding_acknowledgment1,
341
- author = {Damian, Bleotiu, Saraev, Constantinescu},
342
- title = {SOLIS – Sistem Omogen multi-Locație cu funcționalități Inteligente și Sustenabile”
343
- SMIS 143488},
344
- howpublished = {\url{https://github.com/Naeural Edge ProtocolEdgeProtocol/}},
345
- note = {This project includes open-source components developed with support from the Romanian Competitiveness Operational Programme under grants SMIS 143488. The content is solely the responsibility of the authors and does not necessarily reflect the views of the funding agencies.},
346
- year = {2021-2022}
347
- }
348
- ```
349
-
350
- ```bibtex
351
- @misc{project_funding_acknowledgment2,
352
- author = {Damian, Bleotiu, Saraev, Constantinescu, Milik, Lupaescu},
353
- title = {ReDeN – Rețea Descentralizată Neurală SMIS 156084},
354
- howpublished = {\url{https://github.com/Naeural Edge ProtocolEdgeProtocol/}},
355
- note = {This project includes open-source components developed with support from the Romanian Competitiveness Operational Programme under grants SMIS 143488. The content is solely the responsibility of the authors and does not necessarily reflect the views of the funding agencies.},
356
- year = {2023-2024}
357
- }
358
- ```