neuronum 5.7.0__py3-none-any.whl → 5.8.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.

Potentially problematic release.


This version of neuronum might be problematic. Click here for more details.

cli/main.py CHANGED
@@ -10,6 +10,8 @@ import click
10
10
  import questionary
11
11
  from pathlib import Path
12
12
  import requests
13
+ import psutil
14
+ from datetime import datetime
13
15
 
14
16
 
15
17
  @click.group()
@@ -251,9 +253,13 @@ def delete_cell():
251
253
  @click.option('--stream', multiple=True, default=None, help="Optional stream ID for stream.")
252
254
  @click.option('--app', is_flag=True, help="Generate a Node with app template")
253
255
  def init_node(sync, stream, app):
254
- asyncio.run(async_init_node(sync, stream, app))
256
+ descr = click.prompt("Node description: Type up to 25 characters").strip()
257
+ if descr and len(descr) > 25:
258
+ click.echo("Description too long. Max 25 characters allowed.")
259
+ return
260
+ asyncio.run(async_init_node(sync, stream, app, descr))
255
261
 
256
- async def async_init_node(sync, stream, app):
262
+ async def async_init_node(sync, stream, app, descr):
257
263
  credentials_folder_path = Path.home() / ".neuronum"
258
264
  env_path = credentials_folder_path / ".env"
259
265
 
@@ -278,11 +284,16 @@ async def async_init_node(sync, stream, app):
278
284
  return
279
285
 
280
286
  url = f"https://{network}/api/init_node"
281
- node_payload = {"host": host, "password": password, "synapse": synapse}
287
+ node = {
288
+ "host": host,
289
+ "password": password,
290
+ "synapse": synapse,
291
+ "descr": descr,
292
+ }
282
293
 
283
294
  async with aiohttp.ClientSession() as session:
284
295
  try:
285
- async with session.post(url, json=node_payload) as response:
296
+ async with session.post(url, json=node) as response:
286
297
  response.raise_for_status()
287
298
  data = await response.json()
288
299
  nodeID = data["nodeID"]
@@ -565,13 +576,38 @@ asyncio.run(main())
565
576
  @click.command()
566
577
  @click.option('--d', is_flag=True, help="Start node in detached mode")
567
578
  def start_node(d):
579
+ pid_file = Path.cwd() / "status.txt"
580
+ system_name = platform.system()
581
+ active_pids = []
582
+
583
+ start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
584
+
585
+ if pid_file.exists():
586
+ try:
587
+ with open(pid_file, "r") as f:
588
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
589
+ for pid in pids:
590
+ if system_name == "Windows":
591
+ if psutil.pid_exists(pid):
592
+ active_pids.append(pid)
593
+ else:
594
+ try:
595
+ os.kill(pid, 0)
596
+ active_pids.append(pid)
597
+ except OSError:
598
+ continue
599
+ except Exception as e:
600
+ click.echo(f"Failed to read PID file: {e}")
601
+
602
+ if active_pids:
603
+ click.echo(f"Node is already running. Active PIDs: {', '.join(map(str, active_pids))}")
604
+ return
605
+
568
606
  click.echo("Starting Node...")
569
607
 
570
608
  project_path = Path.cwd()
571
609
  script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
572
-
573
610
  processes = []
574
- system_name = platform.system()
575
611
 
576
612
  for script in script_files:
577
613
  script_path = project_path / script
@@ -587,7 +623,10 @@ def start_node(d):
587
623
  start_new_session=True
588
624
  )
589
625
  else:
590
- process = subprocess.Popen(["python", str(script_path)], start_new_session=True)
626
+ process = subprocess.Popen(
627
+ [python_cmd, str(script_path)],
628
+ start_new_session=True
629
+ )
591
630
 
592
631
  processes.append(process.pid)
593
632
 
@@ -595,55 +634,172 @@ def start_node(d):
595
634
  click.echo("Error: No valid node script found. Ensure the node is set up correctly.")
596
635
  return
597
636
 
598
- with open("node_pid.txt", "w") as f:
637
+ with open(pid_file, "w") as f:
638
+ f.write(f"Started at: {start_time}\n")
599
639
  f.write("\n".join(map(str, processes)))
600
640
 
601
- click.echo("Node started successfully!")
641
+ click.echo(f"Node started successfully with PIDs: {', '.join(map(str, processes))}")
602
642
 
603
643
 
604
644
  @click.command()
605
- def stop_node():
606
- asyncio.run(async_stop_node())
645
+ def check_node():
646
+ click.echo("Checking Node status...")
607
647
 
608
- async def async_stop_node():
609
- click.echo("Stopping Node...")
648
+ env_data = {}
649
+ try:
650
+ with open(".env", "r") as f:
651
+ for line in f:
652
+ if "=" in line:
653
+ key, value = line.strip().split("=", 1)
654
+ env_data[key] = value
655
+ nodeID = env_data.get("NODE", "")
656
+ except FileNotFoundError:
657
+ click.echo("Error: .env with credentials not found")
658
+ return
659
+ except Exception as e:
660
+ click.echo(f"Error reading .env file: {e}")
661
+ return
662
+
663
+ pid_file = Path.cwd() / "status.txt"
610
664
 
611
- node_pid_path = Path("node_pid.txt")
665
+ if not pid_file.exists():
666
+ click.echo(f"Node {nodeID} is not running. Status file missing.")
667
+ return
612
668
 
613
669
  try:
614
- with open("node_pid.txt", "r") as f:
615
- pids = [int(pid.strip()) for pid in f.readlines()]
670
+ with open(pid_file, "r") as f:
671
+ lines = f.readlines()
672
+ timestamp_line = next((line for line in lines if line.startswith("Started at:")), None)
673
+ pids = [int(line.strip()) for line in lines if line.strip().isdigit()]
674
+
675
+ if timestamp_line:
676
+ click.echo(timestamp_line.strip())
677
+ start_time = datetime.strptime(timestamp_line.split(":", 1)[1].strip(), "%Y-%m-%d %H:%M:%S")
678
+ now = datetime.now()
679
+ uptime = now - start_time
680
+ click.echo(f"Uptime: {str(uptime).split('.')[0]}")
681
+ except Exception as e:
682
+ click.echo(f"Failed to read PID file: {e}")
683
+ return
616
684
 
617
- system_name = platform.system()
685
+ system_name = platform.system()
686
+ running_pids = []
618
687
 
619
- for pid in pids:
688
+ for pid in pids:
689
+ if system_name == "Windows":
690
+ if psutil.pid_exists(pid):
691
+ running_pids.append(pid)
692
+ else:
620
693
  try:
694
+ os.kill(pid, 0)
695
+ running_pids.append(pid)
696
+ except OSError:
697
+ continue
698
+
699
+ if running_pids:
700
+ for pid in running_pids:
701
+ proc = psutil.Process(pid)
702
+ mem = proc.memory_info().rss / (1024 * 1024) # in MB
703
+ cpu = proc.cpu_percent(interval=0.1)
704
+ click.echo(f"PID {pid} → Memory: {mem:.2f} MB | CPU: {cpu:.1f}%")
705
+ click.echo(f"Node {nodeID} is running. Active PIDs: {', '.join(map(str, running_pids))}")
706
+ else:
707
+ click.echo(f"Node {nodeID} is not running.")
708
+
709
+
710
+ @click.command()
711
+ @click.option('--d', is_flag=True, help="Restart node in detached mode")
712
+ def restart_node(d):
713
+ pid_file = Path.cwd() / "status.txt"
714
+ system_name = platform.system()
715
+
716
+ start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
717
+
718
+ env_data = {}
719
+ try:
720
+ with open(".env", "r") as f:
721
+ for line in f:
722
+ key, value = line.strip().split("=")
723
+ env_data[key] = value
724
+
725
+ nodeID = env_data.get("NODE", "")
726
+
727
+ except FileNotFoundError:
728
+ print("Error: .env with credentials not found")
729
+ return
730
+ except Exception as e:
731
+ print(f"Error reading .env file: {e}")
732
+ return
733
+
734
+ if pid_file.exists():
735
+ try:
736
+ with open(pid_file, "r") as f:
737
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
738
+
739
+ for pid in pids:
621
740
  if system_name == "Windows":
622
- await asyncio.to_thread(subprocess.run, ["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
741
+ if psutil.pid_exists(pid):
742
+ proc = psutil.Process(pid)
743
+ proc.terminate()
623
744
  else:
624
- await asyncio.to_thread(os.kill, pid, 9)
625
- except ProcessLookupError:
626
- click.echo(f"Warning: Process {pid} already stopped or does not exist.")
745
+ try:
746
+ os.kill(pid, 15)
747
+ except OSError:
748
+ continue
627
749
 
628
- await asyncio.to_thread(os.remove, node_pid_path)
629
- click.echo("Node stopped successfully!")
750
+ pid_file.unlink()
630
751
 
631
- except FileNotFoundError:
632
- click.echo("Error: No active node process found.")
633
- except subprocess.CalledProcessError:
634
- click.echo("Error: Unable to stop some node processes.")
752
+ click.echo(f"Terminated existing {nodeID} processes: {', '.join(map(str, pids))}")
753
+
754
+ except Exception as e:
755
+ click.echo(f"Failed to terminate processes: {e}")
756
+ return
757
+ else:
758
+ click.echo(f"Node {nodeID} is not running")
759
+
760
+ click.echo(f"Starting Node {nodeID}...")
761
+ project_path = Path.cwd()
762
+ script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
763
+ processes = []
764
+
765
+ python_cmd = "pythonw" if system_name == "Windows" else "python"
766
+
767
+ for script in script_files:
768
+ script_path = project_path / script
769
+ if script_path.exists():
770
+ if d:
771
+ process = subprocess.Popen(
772
+ ["nohup", python_cmd, str(script_path), "&"] if system_name != "Windows"
773
+ else [python_cmd, str(script_path)],
774
+ stdout=subprocess.DEVNULL,
775
+ stderr=subprocess.DEVNULL,
776
+ start_new_session=True
777
+ )
778
+ else:
779
+ process = subprocess.Popen([python_cmd, str(script_path)], start_new_session=True)
780
+
781
+ processes.append(process.pid)
782
+
783
+ if not processes:
784
+ click.echo("Error: No valid node script found.")
785
+ return
786
+
787
+ with open(pid_file, "w") as f:
788
+ f.write(f"Started at: {start_time}\n")
789
+ f.write("\n".join(map(str, processes)))
790
+
791
+ click.echo(f"Node {nodeID} started with new PIDs: {', '.join(map(str, processes))}")
635
792
 
636
793
 
637
794
  @click.command()
638
- def connect_node():
639
- node_type = questionary.select(
640
- "Choose Node type:",
641
- choices=["public", "private"]
642
- ).ask()
643
- descr = click.prompt("Node description (max. 25 characters)")
644
- asyncio.run(async_connect_node(descr, node_type))
795
+ def stop_node():
796
+ asyncio.run(async_stop_node())
797
+
798
+ async def async_stop_node():
799
+ click.echo("Stopping Node...")
800
+
801
+ node_pid_path = Path("status.txt")
645
802
 
646
- async def async_connect_node(descr, node_type):
647
803
  env_data = {}
648
804
  try:
649
805
  with open(".env", "r") as f:
@@ -652,10 +808,6 @@ async def async_connect_node(descr, node_type):
652
808
  env_data[key] = value
653
809
 
654
810
  nodeID = env_data.get("NODE", "")
655
- host = env_data.get("HOST", "")
656
- password = env_data.get("PASSWORD", "")
657
- network = env_data.get("NETWORK", "")
658
- synapse = env_data.get("SYNAPSE", "")
659
811
 
660
812
  except FileNotFoundError:
661
813
  print("Error: .env with credentials not found")
@@ -665,52 +817,46 @@ async def async_connect_node(descr, node_type):
665
817
  return
666
818
 
667
819
  try:
668
- with open("NODE.md", "r") as f:
669
- nodemd_file = f.read()
820
+ with open("status.txt", "r") as f:
821
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
670
822
 
671
- except FileNotFoundError:
672
- print("Error: NODE.md file not found")
673
- return
674
- except Exception as e:
675
- print(f"Error reading NODE.md file: {e}")
676
- return
823
+ system_name = platform.system()
677
824
 
678
- url = f"https://{network}/api/connect_node/{node_type}"
825
+ for pid in pids:
826
+ try:
827
+ if system_name == "Windows":
828
+ await asyncio.to_thread(subprocess.run, ["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
829
+ else:
830
+ await asyncio.to_thread(os.kill, pid, 9)
831
+ except ProcessLookupError:
832
+ click.echo(f"Warning: Process {pid} already stopped or does not exist.")
679
833
 
680
- node = {
681
- "nodeID": nodeID,
682
- "descr": descr,
683
- "host": host,
684
- "password": password,
685
- "synapse": synapse,
686
- "nodemd_file": nodemd_file
687
- }
834
+ await asyncio.to_thread(os.remove, node_pid_path)
835
+ click.echo(f"Node {nodeID} stopped successfully!")
688
836
 
689
- async with aiohttp.ClientSession() as session:
690
- try:
691
- async with session.post(url, json=node) as response:
692
- response.raise_for_status()
693
- data = await response.json()
694
- nodeID = data["nodeID"]
695
- node_url = data["node_url"]
696
- except aiohttp.ClientError as e:
697
- click.echo(f"Error sending request: {e}")
698
- return
699
-
700
- if nodeID == "Node does not exist":
701
- click.echo(f"Neuronum Node not found! Make sure you initialized your Node correctly")
702
- else:
703
- if node_type == "public":
704
- click.echo(f"Public Neuronum Node '{nodeID}' connected! Visit: {node_url}")
705
- else:
706
- click.echo(f"Private Neuronum Node '{nodeID}' connected!")
837
+ except FileNotFoundError:
838
+ click.echo("Error: No active node process found.")
839
+ except subprocess.CalledProcessError:
840
+ click.echo("Error: Unable to stop some node processes.")
707
841
 
708
842
 
709
843
  @click.command()
710
844
  def update_node():
711
- asyncio.run(async_update_node())
845
+ node_type = questionary.select(
846
+ "Update Node type:",
847
+ choices=["public", "private"]
848
+ ).ask()
849
+ descr = click.prompt(
850
+ "Update Node description: Type up to 25 characters, or press Enter to leave it unchanged",
851
+ default="None",
852
+ show_default=False
853
+ ).strip()
854
+ if descr and len(descr) > 25:
855
+ click.echo("Description too long. Max 25 characters allowed.")
856
+ return
857
+ asyncio.run(async_update_node(node_type, descr))
712
858
 
713
- async def async_update_node():
859
+ async def async_update_node(node_type: str, descr: str) -> None:
714
860
  env_data = {}
715
861
 
716
862
  try:
@@ -744,17 +890,19 @@ async def async_update_node():
744
890
  return
745
891
 
746
892
  url = f"https://{network}/api/update_node"
747
- node_payload = {
893
+ node = {
748
894
  "nodeID": nodeID,
749
895
  "host": host,
750
896
  "password": password,
751
897
  "synapse": synapse,
752
- "nodemd_file": nodemd_file
898
+ "node_type": node_type,
899
+ "nodemd_file": nodemd_file,
900
+ "descr": descr,
753
901
  }
754
902
 
755
903
  async with aiohttp.ClientSession() as session:
756
904
  try:
757
- async with session.post(url, json=node_payload) as response:
905
+ async with session.post(url, json=node) as response:
758
906
  response.raise_for_status()
759
907
  data = await response.json()
760
908
  nodeID = data["nodeID"]
@@ -782,54 +930,10 @@ async def async_update_node():
782
930
  await asyncio.to_thread(Path("streams.json").write_text, json.dumps(stx, indent=4))
783
931
  await asyncio.to_thread(Path("nodes.json").write_text, json.dumps(nodes, indent=4))
784
932
 
785
- click.echo(f"Neuronum Node '{nodeID}' updated! Visit: {node_url}")
786
-
787
-
788
- @click.command()
789
- def disconnect_node():
790
- asyncio.run(async_disconnect_node())
791
-
792
- async def async_disconnect_node():
793
- env_data = {}
794
-
795
- try:
796
- with open(".env", "r") as f:
797
- for line in f:
798
- key, value = line.strip().split("=")
799
- env_data[key] = value
800
-
801
- nodeID = env_data.get("NODE", "")
802
- host = env_data.get("HOST", "")
803
- password = env_data.get("PASSWORD", "")
804
- network = env_data.get("NETWORK", "")
805
- synapse = env_data.get("SYNAPSE", "")
806
-
807
- except FileNotFoundError:
808
- click.echo("Error: .env with credentials not found")
809
- return
810
- except Exception as e:
811
- click.echo(f"Error reading .env file: {e}")
812
- return
813
-
814
- url = f"https://{network}/api/disconnect_node"
815
- node_payload = {
816
- "nodeID": nodeID,
817
- "host": host,
818
- "password": password,
819
- "synapse": synapse
820
- }
821
-
822
- async with aiohttp.ClientSession() as session:
823
- try:
824
- async with session.post(url, json=node_payload) as response:
825
- response.raise_for_status()
826
- data = await response.json()
827
- nodeID = data["nodeID"]
828
- except aiohttp.ClientError as e:
829
- click.echo(f"Error sending request: {e}")
830
- return
831
-
832
- click.echo(f"Neuronum Node '{nodeID}' disconnected!")
933
+ if node_type == "public":
934
+ click.echo(f"Neuronum Node '{nodeID}' updated! Visit: {node_url}")
935
+ else:
936
+ click.echo(f"Neuronum Node '{nodeID}' updated!")
833
937
 
834
938
 
835
939
  @click.command()
@@ -1004,16 +1108,15 @@ async def async_sync(stx):
1004
1108
 
1005
1109
 
1006
1110
  cli.add_command(create_cell)
1007
- cli.add_command(connect_cell)
1008
1111
  cli.add_command(view_cell)
1009
1112
  cli.add_command(disconnect_cell)
1010
1113
  cli.add_command(delete_cell)
1011
1114
  cli.add_command(init_node)
1012
1115
  cli.add_command(start_node)
1116
+ cli.add_command(restart_node)
1013
1117
  cli.add_command(stop_node)
1014
- cli.add_command(connect_node)
1118
+ cli.add_command(check_node)
1015
1119
  cli.add_command(update_node)
1016
- cli.add_command(disconnect_node)
1017
1120
  cli.add_command(delete_node)
1018
1121
  cli.add_command(activate)
1019
1122
  cli.add_command(load)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuronum
3
- Version: 5.7.0
3
+ Version: 5.8.0
4
4
  Summary: Official client library to interact with the Neuronum Network
5
5
  Home-page: https://neuronum.net
6
6
  Author: Neuronum Cybernetics
@@ -18,6 +18,7 @@ Requires-Dist: click
18
18
  Requires-Dist: questionary
19
19
  Requires-Dist: python-dotenv
20
20
  Requires-Dist: requests
21
+ Requires-Dist: psutil
21
22
  Dynamic: author
22
23
  Dynamic: author-email
23
24
  Dynamic: classifier
@@ -54,10 +55,11 @@ Dynamic: summary
54
55
  ------------------
55
56
 
56
57
  ### **A Getting Started into the Neuronum Network**
57
- - Learn about Neuronum
58
- - Connect to the Network
59
- - Build a Neuronum Node
60
- - Interact with your Node
58
+ In this brief getting started guide, you will:
59
+ - [Learn about Neuronum](#about-neuronum)
60
+ - [Connect to the Network](#connect-to-neuronum)
61
+ - [Build a Neuronum Node](#build-on-neuronum)
62
+ - [Interact with your Node](#interact-with-neuronum)
61
63
 
62
64
  ------------------
63
65
 
@@ -101,10 +103,13 @@ neuronum connect-cell # connect Cell
101
103
  ------------------
102
104
 
103
105
 
104
- ### **Build On Neuronum** **[(Build with Node Examples)](https://github.com/neuronumcybernetics/neuronum/tree/main/features/nodes)**
106
+ ### **Build On Neuronum**
107
+ [View Node Examples](https://github.com/neuronumcybernetics/neuronum/tree/main/features/nodes)
108
+
109
+
105
110
  Initialize a Node (app template):
106
111
  ```sh
107
- neuronum init-node --app # initialize a Node with app template -> creates a folder named node_<node_id> containing all relevant files
112
+ neuronum init-node --app # initialize a Node with app template
108
113
  ```
109
114
 
110
115
  Change into Node folder
@@ -0,0 +1,10 @@
1
+ cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli/main.py,sha256=33d7kyfPeSNJD_lHP5CuFQ34sKM7Mv-kfQIGAbu2DQk,35181
3
+ neuronum/__init__.py,sha256=Drsm263_w3_VWgl1YsKLUr8WwVodqV3TSjqpxLjyq_M,46
4
+ neuronum/neuronum.py,sha256=L8Oz1qcTyOiYMTGfiSKgN5Nu9jcl25jFeJOh0XItPjw,17201
5
+ neuronum-5.8.0.dist-info/licenses/LICENSE.md,sha256=zGst0rjgnp6oFuRVwFwB1Ql4sDXt_nw9xbUR49Gf99A,2008
6
+ neuronum-5.8.0.dist-info/METADATA,sha256=oSNPrPnllCkMzCGcMJjN6cYbcFONcKj2f6fExlfH2AQ,5374
7
+ neuronum-5.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ neuronum-5.8.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
+ neuronum-5.8.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
+ neuronum-5.8.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cli/main.py,sha256=nishUFMtkKJqpCnmSTats30kymNYDIfknKLoesiztVs,31371
3
- neuronum/__init__.py,sha256=Drsm263_w3_VWgl1YsKLUr8WwVodqV3TSjqpxLjyq_M,46
4
- neuronum/neuronum.py,sha256=L8Oz1qcTyOiYMTGfiSKgN5Nu9jcl25jFeJOh0XItPjw,17201
5
- neuronum-5.7.0.dist-info/licenses/LICENSE.md,sha256=zGst0rjgnp6oFuRVwFwB1Ql4sDXt_nw9xbUR49Gf99A,2008
6
- neuronum-5.7.0.dist-info/METADATA,sha256=M7Q5CKsUO2uHGbPXQk43zq2FYFDPfWDnV29rZEW7A-8,5288
7
- neuronum-5.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- neuronum-5.7.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
- neuronum-5.7.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
- neuronum-5.7.0.dist-info/RECORD,,