neuronum 5.9.0__py3-none-any.whl → 6.0.1__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
@@ -1,1143 +1,1137 @@
1
- import subprocess
2
- import os
3
- import neuronum
4
- import json
5
- import platform
6
- import glob
7
- import asyncio
8
- import aiohttp
9
- import click
10
- import questionary
11
- from pathlib import Path
12
- import requests
13
- import psutil
14
- from datetime import datetime
15
-
16
-
17
- @click.group()
18
- def cli():
19
- """Neuronum CLI Tool"""
20
-
21
-
22
- @click.command()
23
- def create_cell():
24
- cell_type = questionary.select(
25
- "Choose Cell type:",
26
- choices=["business", "community"]
27
- ).ask()
28
-
29
- network = questionary.select(
30
- "Choose Network:",
31
- choices=["neuronum.net"]
32
- ).ask()
33
-
34
- if cell_type == "business":
35
- click.echo("Visit https://neuronum.net/createcell to create your Neuronum Business Cell")
36
-
37
- if cell_type == "community":
38
-
39
- email = click.prompt("Enter email")
40
- password = click.prompt("Enter password", hide_input=True)
41
- repeat_password = click.prompt("Repeat password", hide_input=True)
42
-
43
- if password != repeat_password:
44
- click.echo("Passwords do not match!")
45
- return
46
-
47
- url = f"https://{network}/api/create_cell/{cell_type}"
48
-
49
- create_cell = {"email": email, "password": password}
50
-
51
- try:
52
- response = requests.post(url, json=create_cell)
53
- response.raise_for_status()
54
- status = response.json()["status"]
55
-
56
- except requests.exceptions.RequestException as e:
57
- click.echo(f"Error sending request: {e}")
58
- return
59
-
60
- if status == True:
61
- host = response.json()["host"]
62
- cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
63
-
64
- url = f"https://{network}/api/verify_email"
65
-
66
- verify_email = {"host": host, "email": email, "cellkey": cellkey}
67
-
68
- try:
69
- response = requests.post(url, json=verify_email)
70
- response.raise_for_status()
71
- status = response.json()["status"]
72
-
73
- except requests.exceptions.RequestException as e:
74
- click.echo(f"Error sending request: {e}")
75
- return
76
-
77
- if status == True:
78
- synapse = response.json()["synapse"]
79
- credentials_folder_path = Path.home() / ".neuronum"
80
- credentials_folder_path.mkdir(parents=True, exist_ok=True)
81
-
82
- env_path = credentials_folder_path / ".env"
83
- env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
84
-
85
- click.echo(f"Welcome to Neuronum! Community Cell '{host}' created and connected!")
86
-
87
- if status == False:
88
- click.echo(f"Error:'{email}' already assigned!")
89
-
90
-
91
- @click.command()
92
- def connect_cell():
93
- email = click.prompt("Enter your Email")
94
- password = click.prompt("Enter password", hide_input=True)
95
-
96
- network = questionary.select(
97
- "Choose Network:",
98
- choices=["neuronum.net"]
99
- ).ask()
100
-
101
- url = f"https://{network}/api/connect_cell"
102
- payload = {"email": email, "password": password}
103
-
104
- try:
105
- response = requests.post(url, json=payload)
106
- response.raise_for_status()
107
- status = response.json()["status"]
108
- host = response.json()["host"]
109
- except requests.exceptions.RequestException as e:
110
- click.echo(f"Error connecting: {e}")
111
- return
112
-
113
- if status == True:
114
- cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
115
- url = f"https://{network}/api/verify_email"
116
- verify_email = {"host": host, "email": email, "cellkey": cellkey}
117
-
118
- try:
119
- response = requests.post(url, json=verify_email)
120
- response.raise_for_status()
121
- status = response.json()["status"]
122
- synapse = response.json()["synapse"]
123
-
124
- except requests.exceptions.RequestException as e:
125
- click.echo(f"Error sending request: {e}")
126
- return
127
-
128
- if status == True:
129
- credentials_folder_path = Path.home() / ".neuronum"
130
- credentials_folder_path.mkdir(parents=True, exist_ok=True)
131
-
132
- env_path = credentials_folder_path / f".env"
133
- env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
134
-
135
- click.echo(f"Cell '{host}' connected!")
136
- else:
137
- click.echo(f"Connection failed!")
138
-
139
-
140
- @click.command()
141
- def view_cell():
142
- credentials_folder_path = Path.home() / ".neuronum"
143
- env_path = credentials_folder_path / ".env"
144
-
145
- env_data = {}
146
-
147
- try:
148
- with open(env_path, "r") as f:
149
- for line in f:
150
- key, value = line.strip().split("=")
151
- env_data[key] = value
152
-
153
- host = env_data.get("HOST", "")
154
-
155
- except FileNotFoundError:
156
- click.echo("Error: No credentials found. Please connect to a cell first.")
157
- return
158
- except Exception as e:
159
- click.echo(f"Error reading .env file: {e}")
160
- return
161
-
162
- if host:
163
- click.echo(f"Connected Cell: '{host}'")
164
- else:
165
- click.echo("No active cell connection found.")
166
-
167
-
168
- @click.command()
169
- def disconnect_cell():
170
- credentials_folder_path = Path.home() / ".neuronum"
171
- env_path = credentials_folder_path / ".env"
172
-
173
- env_data = {}
174
-
175
- try:
176
- with open(env_path, "r") as f:
177
- for line in f:
178
- key, value = line.strip().split("=")
179
- env_data[key] = value
180
-
181
- host = env_data.get("HOST", "")
182
-
183
- except FileNotFoundError:
184
- click.echo("Error: .env with credentials not found")
185
- return
186
- except Exception as e:
187
- click.echo(f"Error reading .env file: {e}")
188
- return
189
-
190
- if env_path.exists():
191
- if click.confirm(f"Are you sure you want to disconnect Cell '{host}'?", default=True):
192
- os.remove(env_path)
193
- click.echo(f"'{host}' disconnected!")
194
- else:
195
- click.echo("Disconnect canceled.")
196
- else:
197
- click.echo(f"No Neuronum Cell connected!")
198
-
199
-
200
- @click.command()
201
- def delete_cell():
202
- credentials_folder_path = Path.home() / ".neuronum"
203
- env_path = credentials_folder_path / ".env"
204
-
205
- env_data = {}
206
-
207
- try:
208
- with open(env_path, "r") as f:
209
- for line in f:
210
- key, value = line.strip().split("=")
211
- env_data[key] = value
212
-
213
- host = env_data.get("HOST", "")
214
- password = env_data.get("PASSWORD", "")
215
- network = env_data.get("NETWORK", "")
216
- synapse = env_data.get("SYNAPSE", "")
217
-
218
- except FileNotFoundError:
219
- click.echo("Error: No cell connected. Connect Cell first to delete")
220
- return
221
- except Exception as e:
222
- click.echo(f"Error reading .env file: {e}")
223
- return
224
-
225
- confirm = click.confirm(f" Are you sure you want to delete '{host}'?", default=True)
226
- if not confirm:
227
- click.echo("Deletion canceled.")
228
- return
229
-
230
- url = f"https://{network}/api/delete_cell"
231
- payload = {"host": host, "password": password, "synapse": synapse}
232
-
233
- try:
234
- response = requests.delete(url, json=payload)
235
- response.raise_for_status()
236
- status = response.json()["status"]
237
- except requests.exceptions.RequestException as e:
238
- click.echo(f"Error deleting cell: {e}")
239
- return
240
-
241
- if status == True:
242
- env_path = credentials_folder_path / f"{host}.env"
243
- if env_path.exists():
244
- os.remove(env_path)
245
- click.echo("Credentials deleted successfully!")
246
- click.echo(f"Neuronum Cell '{host}' has been deleted!")
247
- else:
248
- click.echo(f"Neuronum Cell '{host}' deletion failed!")
249
-
250
-
251
- @click.command()
252
- @click.option('--sync', multiple=True, default=None, help="Optional stream IDs for sync.")
253
- @click.option('--stream', multiple=True, default=None, help="Optional stream ID for stream.")
254
- @click.option('--app', is_flag=True, help="Generate a Node with app template")
255
- def 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))
261
-
262
- async def async_init_node(sync, stream, app, descr):
263
- credentials_folder_path = Path.home() / ".neuronum"
264
- env_path = credentials_folder_path / ".env"
265
-
266
- env_data = {}
267
-
268
- try:
269
- with open(env_path, "r") as f:
270
- for line in f:
271
- key, value = line.strip().split("=")
272
- env_data[key] = value
273
-
274
- host = env_data.get("HOST", "")
275
- password = env_data.get("PASSWORD", "")
276
- network = env_data.get("NETWORK", "")
277
- synapse = env_data.get("SYNAPSE", "")
278
-
279
- except FileNotFoundError:
280
- click.echo("No cell connected. Connect your cell with command neuronum connect-cell")
281
- return
282
- except Exception as e:
283
- click.echo(f"Error reading .env file: {e}")
284
- return
285
-
286
- url = f"https://{network}/api/init_node"
287
- node = {
288
- "host": host,
289
- "password": password,
290
- "synapse": synapse,
291
- "descr": descr,
292
- }
293
-
294
- async with aiohttp.ClientSession() as session:
295
- try:
296
- async with session.post(url, json=node) as response:
297
- response.raise_for_status()
298
- data = await response.json()
299
- nodeID = data["nodeID"]
300
- except aiohttp.ClientError as e:
301
- click.echo(f"Error sending request: {e}")
302
- return
303
-
304
- node_filename = "node_" + nodeID.replace("::node", "")
305
- project_path = Path(node_filename)
306
- project_path.mkdir(exist_ok=True)
307
-
308
- cell = neuronum.Cell(
309
- host=host,
310
- password=password,
311
- network=network,
312
- synapse=synapse
313
- )
314
-
315
- cells = await cell.list_cells()
316
- tx = await cell.list_tx()
317
- ctx = await cell.list_ctx()
318
- stx = await cell.list_stx()
319
- nodes = await cell.list_nodes()
320
-
321
- await asyncio.to_thread((project_path / "cells.json").write_text, json.dumps(cells, indent=4))
322
- await asyncio.to_thread((project_path / "transmitters.json").write_text, json.dumps(tx, indent=4))
323
- await asyncio.to_thread((project_path / "circuits.json").write_text, json.dumps(ctx, indent=4))
324
- await asyncio.to_thread((project_path / "streams.json").write_text, json.dumps(stx, indent=4))
325
- await asyncio.to_thread((project_path / "nodes.json").write_text, json.dumps(nodes, indent=4))
326
-
327
- env_path = project_path / ".env"
328
- await asyncio.to_thread(env_path.write_text, f"NODE={nodeID}\nHOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
329
-
330
- nodemd_path = project_path / "NODE.md"
331
- await asyncio.to_thread(nodemd_path.write_text, """### NODE.md: How to interact with this Node
332
-
333
- ```json
334
- {
335
- "gateways": [
336
- {
337
- "type": "stream",
338
- "id": "id::stx",
339
- "link": "https://neuronum.net/stream/id::stx",
340
- "info": "stream info"
341
- },
342
- {
343
- "type": "transmitter",
344
- "id": "id::tx",
345
- "link": "https://neuronum.net/tx/id::tx",
346
- "info": "transmitter info"
347
- },
348
- {
349
- "type": "circuit",
350
- "id": "id::ctx",
351
- "link": "https://neuronum.net/circuit/id::ctx",
352
- "info": "circuit info"
353
- }
354
- ]
355
- }
356
- ```"""
357
- )
358
-
359
- stx = sync[0] if sync else (stream[0] if stream else host.replace("::cell", "::stx"))
360
-
361
- if sync:
362
- for stx in sync:
363
- sync_path = project_path / f"sync_{stx.replace('::stx', '')}.py"
364
- sync_path.write_text(f"""\
365
- import asyncio
366
- import neuronum
367
- import os
368
- from dotenv import load_dotenv
369
-
370
- load_dotenv()
371
- host = os.getenv("HOST")
372
- password = os.getenv("PASSWORD")
373
- network = os.getenv("NETWORK")
374
- synapse = os.getenv("SYNAPSE")
375
-
376
- cell = neuronum.Cell(
377
- host=host,
378
- password=password,
379
- network=network,
380
- synapse=synapse
381
- )
382
-
383
- async def main():
384
- STX = "{stx}"
385
- async for operation in cell.sync(STX):
386
- label = operation.get("label")
387
- data = operation.get("data")
388
- ts = operation.get("time")
389
- stxID = operation.get("stxID")
390
- operator = operation.get("operator")
391
- print(label, data, ts, stxID, operator)
392
-
393
- asyncio.run(main())
394
- """)
395
-
396
-
397
- if stream:
398
- for stx in stream:
399
- stream_path = project_path / f"stream_{stx.replace('::stx', '')}.py"
400
- stream_path.write_text(f"""\
401
- import asyncio
402
- import neuronum
403
- import os
404
- from dotenv import load_dotenv
405
- import time
406
-
407
- load_dotenv()
408
- host = os.getenv("HOST")
409
- password = os.getenv("PASSWORD")
410
- network = os.getenv("NETWORK")
411
- synapse = os.getenv("SYNAPSE")
412
-
413
- cell = neuronum.Cell(
414
- host=host,
415
- password=password,
416
- network=network,
417
- synapse=synapse
418
- )
419
-
420
- async def main():
421
- STX = "{stx}"
422
- label = "your_label"
423
-
424
- while True:
425
- data = {{
426
- "key1": "value1",
427
- "key2": "value2",
428
- "key3": "value3",
429
- }}
430
- await cell.stream(label, data, STX)
431
- time.sleep(5)
432
-
433
- asyncio.run(main())
434
- """)
435
-
436
- if not sync and not stream and not app:
437
- sync_path = project_path / f"sync_{stx.replace('::stx', '')}.py"
438
- sync_path.write_text(f"""\
439
- import asyncio
440
- import neuronum
441
- import os
442
- from dotenv import load_dotenv
443
-
444
- load_dotenv()
445
- host = os.getenv("HOST")
446
- password = os.getenv("PASSWORD")
447
- network = os.getenv("NETWORK")
448
- synapse = os.getenv("SYNAPSE")
449
-
450
- cell = neuronum.Cell(
451
- host=host,
452
- password=password,
453
- network=network,
454
- synapse=synapse
455
- )
456
-
457
- async def main():
458
- async for operation in cell.sync():
459
- message = operation.get("data").get("message")
460
- print(message)
461
-
462
- asyncio.run(main())
463
- """)
464
-
465
- stream_path = project_path / f"stream_{stx.replace('::stx', '')}.py"
466
- stream_path.write_text(f"""\
467
- import asyncio
468
- import neuronum
469
- import os
470
- from dotenv import load_dotenv
471
- import time
472
-
473
- load_dotenv()
474
- host = os.getenv("HOST")
475
- password = os.getenv("PASSWORD")
476
- network = os.getenv("NETWORK")
477
- synapse = os.getenv("SYNAPSE")
478
-
479
- cell = neuronum.Cell(
480
- host=host,
481
- password=password,
482
- network=network,
483
- synapse=synapse
484
- )
485
-
486
- async def main():
487
- label = "Welcome to Neuronum"
488
-
489
- while True:
490
- data = {{
491
- "message": "Hello, Neuronum!"
492
- }}
493
- await cell.stream(label, data)
494
- time.sleep(5)
495
-
496
- asyncio.run(main())
497
- """)
498
-
499
- if app and nodeID:
500
-
501
- descr = f"{nodeID} App"
502
- partners = ["private"]
503
- stxID = await cell.create_stx(descr, partners)
504
-
505
-
506
- descr = f"Greet {nodeID}"
507
- key_values = {
508
- "say": "hello",
509
- }
510
- STX = stxID
511
- label = "say:hello"
512
- partners = ["private"]
513
- txID = await cell.create_tx(descr, key_values, STX, label, partners)
514
-
515
-
516
- app_path = project_path / "app.py"
517
- app_path.write_text(f"""\
518
- import asyncio
519
- import neuronum
520
- import os
521
- from dotenv import load_dotenv
522
-
523
- load_dotenv()
524
- host = os.getenv("HOST")
525
- password = os.getenv("PASSWORD")
526
- network = os.getenv("NETWORK")
527
- synapse = os.getenv("SYNAPSE")
528
-
529
- cell = neuronum.Cell(
530
- host=host,
531
- password=password,
532
- network=network,
533
- synapse=synapse
534
- )
535
-
536
- async def main():
537
- STX = "{stxID}"
538
- async for operation in cell.sync(STX):
539
- txID = operation.get("txID")
540
- client = operation.get("operator")
541
-
542
- if txID == "{txID}":
543
- data = {{
544
- "json": f"Hello {{client}} from {nodeID}",
545
- "html": f\"\"\"
546
- <!DOCTYPE html>
547
- <html>
548
- <head>
549
- <meta charset="UTF-8">
550
- <title>Greeting Node</title>
551
- </head>
552
- <body>
553
- <div class="card">
554
- <h1>Hello, {{client}}</h1>
555
- <p>Greetings from <span class="node">{nodeID}</span></p>
556
- </div>
557
- </body>
558
- </html>
559
- \"\"\"
560
-
561
- }}
562
- await cell.tx_response(txID, client, data)
563
-
564
- asyncio.run(main())
565
- """)
566
-
567
- click.echo(f"Neuronum Node '{nodeID}' initialized!")
568
-
569
-
570
- @click.command()
571
- @click.option('--d', is_flag=True, help="Start node in detached mode")
572
- def start_node(d):
573
- pid_file = Path.cwd() / "status.txt"
574
- system_name = platform.system()
575
- active_pids = []
576
-
577
- start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
578
-
579
- if pid_file.exists():
580
- try:
581
- with open(pid_file, "r") as f:
582
- pids = [int(line.strip()) for line in f if line.strip().isdigit()]
583
- for pid in pids:
584
- if system_name == "Windows":
585
- if psutil.pid_exists(pid):
586
- active_pids.append(pid)
587
- else:
588
- try:
589
- os.kill(pid, 0)
590
- active_pids.append(pid)
591
- except OSError:
592
- continue
593
- except Exception as e:
594
- click.echo(f"Failed to read PID file: {e}")
595
-
596
- if active_pids:
597
- click.echo(f"Node is already running. Active PIDs: {', '.join(map(str, active_pids))}")
598
- return
599
-
600
- click.echo("Starting Node...")
601
-
602
- project_path = Path.cwd()
603
- script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
604
- processes = []
605
-
606
- for script in script_files:
607
- script_path = project_path / script
608
- if script_path.exists():
609
- python_cmd = "pythonw" if system_name == "Windows" else "python"
610
-
611
- if d:
612
- process = subprocess.Popen(
613
- ["nohup", python_cmd, str(script_path), "&"] if system_name != "Windows"
614
- else [python_cmd, str(script_path)],
615
- stdout=subprocess.DEVNULL,
616
- stderr=subprocess.DEVNULL,
617
- start_new_session=True
618
- )
619
- else:
620
- process = subprocess.Popen(
621
- ["python", str(script_path)],
622
- stdout = None,
623
- stderr = None
624
- )
625
-
626
- processes.append(process.pid)
627
-
628
- if not processes:
629
- click.echo("Error: No valid node script found. Ensure the node is set up correctly.")
630
- return
631
-
632
- with open(pid_file, "w") as f:
633
- f.write(f"Started at: {start_time}\n")
634
- f.write("\n".join(map(str, processes)))
635
-
636
- click.echo(f"Node started successfully with PIDs: {', '.join(map(str, processes))}")
637
-
638
-
639
- @click.command()
640
- def check_node():
641
- click.echo("Checking Node status...")
642
-
643
- env_data = {}
644
- try:
645
- with open(".env", "r") as f:
646
- for line in f:
647
- if "=" in line:
648
- key, value = line.strip().split("=", 1)
649
- env_data[key] = value
650
- nodeID = env_data.get("NODE", "")
651
- except FileNotFoundError:
652
- click.echo("Error: .env with credentials not found")
653
- return
654
- except Exception as e:
655
- click.echo(f"Error reading .env file: {e}")
656
- return
657
-
658
- pid_file = Path.cwd() / "status.txt"
659
-
660
- if not pid_file.exists():
661
- click.echo(f"Node {nodeID} is not running. Status file missing.")
662
- return
663
-
664
- try:
665
- with open(pid_file, "r") as f:
666
- lines = f.readlines()
667
- timestamp_line = next((line for line in lines if line.startswith("Started at:")), None)
668
- pids = [int(line.strip()) for line in lines if line.strip().isdigit()]
669
-
670
- if timestamp_line:
671
- click.echo(timestamp_line.strip())
672
- start_time = datetime.strptime(timestamp_line.split(":", 1)[1].strip(), "%Y-%m-%d %H:%M:%S")
673
- now = datetime.now()
674
- uptime = now - start_time
675
- click.echo(f"Uptime: {str(uptime).split('.')[0]}")
676
- except Exception as e:
677
- click.echo(f"Failed to read PID file: {e}")
678
- return
679
-
680
- system_name = platform.system()
681
- running_pids = []
682
-
683
- for pid in pids:
684
- if system_name == "Windows":
685
- if psutil.pid_exists(pid):
686
- running_pids.append(pid)
687
- else:
688
- try:
689
- os.kill(pid, 0)
690
- running_pids.append(pid)
691
- except OSError:
692
- continue
693
-
694
- if running_pids:
695
- for pid in running_pids:
696
- proc = psutil.Process(pid)
697
- mem = proc.memory_info().rss / (1024 * 1024) # in MB
698
- cpu = proc.cpu_percent(interval=0.1)
699
- click.echo(f"PID {pid} → Memory: {mem:.2f} MB | CPU: {cpu:.1f}%")
700
- click.echo(f"Node {nodeID} is running. Active PIDs: {', '.join(map(str, running_pids))}")
701
- else:
702
- click.echo(f"Node {nodeID} is not running.")
703
-
704
-
705
- @click.command()
706
- @click.option('--d', is_flag=True, help="Restart node in detached mode")
707
- def restart_node(d):
708
- pid_file = Path.cwd() / "status.txt"
709
- system_name = platform.system()
710
-
711
- start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
712
-
713
- env_data = {}
714
- try:
715
- with open(".env", "r") as f:
716
- for line in f:
717
- key, value = line.strip().split("=")
718
- env_data[key] = value
719
-
720
- nodeID = env_data.get("NODE", "")
721
-
722
- except FileNotFoundError:
723
- print("Error: .env with credentials not found")
724
- return
725
- except Exception as e:
726
- print(f"Error reading .env file: {e}")
727
- return
728
-
729
- if pid_file.exists():
730
- try:
731
- with open(pid_file, "r") as f:
732
- pids = [int(line.strip()) for line in f if line.strip().isdigit()]
733
-
734
- for pid in pids:
735
- if system_name == "Windows":
736
- if psutil.pid_exists(pid):
737
- proc = psutil.Process(pid)
738
- proc.terminate()
739
- else:
740
- try:
741
- os.kill(pid, 15)
742
- except OSError:
743
- continue
744
-
745
- pid_file.unlink()
746
-
747
- click.echo(f"Terminated existing {nodeID} processes: {', '.join(map(str, pids))}")
748
-
749
- except Exception as e:
750
- click.echo(f"Failed to terminate processes: {e}")
751
- return
752
- else:
753
- click.echo(f"Node {nodeID} is not running")
754
-
755
- click.echo(f"Starting Node {nodeID}...")
756
- project_path = Path.cwd()
757
- script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
758
- processes = []
759
-
760
- python_cmd = "pythonw" if system_name == "Windows" else "python"
761
-
762
- for script in script_files:
763
- script_path = project_path / script
764
- if script_path.exists():
765
- if d:
766
- process = subprocess.Popen(
767
- ["nohup", python_cmd, str(script_path), "&"] if system_name != "Windows"
768
- else [python_cmd, str(script_path)],
769
- stdout=subprocess.DEVNULL,
770
- stderr=subprocess.DEVNULL,
771
- start_new_session=True
772
- )
773
- else:
774
- process = subprocess.Popen(
775
- ["python", str(script_path)],
776
- stdout = None,
777
- stderr = None
778
- )
779
-
780
- processes.append(process.pid)
781
-
782
- if not processes:
783
- click.echo("Error: No valid node script found.")
784
- return
785
-
786
- with open(pid_file, "w") as f:
787
- f.write(f"Started at: {start_time}\n")
788
- f.write("\n".join(map(str, processes)))
789
-
790
- click.echo(f"Node {nodeID} started with new PIDs: {', '.join(map(str, processes))}")
791
-
792
-
793
- @click.command()
794
- def stop_node():
795
- asyncio.run(async_stop_node())
796
-
797
- async def async_stop_node():
798
- click.echo("Stopping Node...")
799
-
800
- node_pid_path = Path("status.txt")
801
-
802
- env_data = {}
803
- try:
804
- with open(".env", "r") as f:
805
- for line in f:
806
- key, value = line.strip().split("=")
807
- env_data[key] = value
808
-
809
- nodeID = env_data.get("NODE", "")
810
-
811
- except FileNotFoundError:
812
- print("Error: .env with credentials not found")
813
- return
814
- except Exception as e:
815
- print(f"Error reading .env file: {e}")
816
- return
817
-
818
- try:
819
- with open("status.txt", "r") as f:
820
- pids = [int(line.strip()) for line in f if line.strip().isdigit()]
821
-
822
- system_name = platform.system()
823
-
824
- for pid in pids:
825
- try:
826
- if system_name == "Windows":
827
- await asyncio.to_thread(subprocess.run, ["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
828
- else:
829
- await asyncio.to_thread(os.kill, pid, 9)
830
- except ProcessLookupError:
831
- click.echo(f"Warning: Process {pid} already stopped or does not exist.")
832
-
833
- await asyncio.to_thread(os.remove, node_pid_path)
834
- click.echo(f"Node {nodeID} stopped successfully!")
835
-
836
- except FileNotFoundError:
837
- click.echo("Error: No active node process found.")
838
- except subprocess.CalledProcessError:
839
- click.echo("Error: Unable to stop some node processes.")
840
-
841
-
842
- @click.command()
843
- def update_node():
844
- click.echo("Update your Node")
845
- node_type = questionary.select(
846
- "Who can view your Node?:",
847
- choices=["public", "private", "partners"]
848
- ).ask()
849
- partners = "None"
850
- if node_type == "partners":
851
- prompt_msg = (
852
- "Enter the list of partners who can view this Node.\n"
853
- "Format: partner::cell, partner::cell, partner::cell\n"
854
- "Press Enter to leave the list unchanged"
855
- )
856
- partners = click.prompt(
857
- prompt_msg,
858
- default="None",
859
- show_default=False
860
- ).strip()
861
- descr = click.prompt(
862
- "Update Node description: Type up to 25 characters, or press Enter to leave it unchanged",
863
- default="None",
864
- show_default=False
865
- ).strip()
866
- if descr and len(descr) > 25:
867
- click.echo("Description too long. Max 25 characters allowed.")
868
- return
869
- asyncio.run(async_update_node(node_type, descr, partners))
870
-
871
- async def async_update_node(node_type: str, descr: str, partners:str) -> None:
872
- env_data = {}
873
-
874
- try:
875
- with open(".env", "r") as f:
876
- for line in f:
877
- key, value = line.strip().split("=")
878
- env_data[key] = value
879
-
880
- nodeID = env_data.get("NODE", "")
881
- host = env_data.get("HOST", "")
882
- password = env_data.get("PASSWORD", "")
883
- network = env_data.get("NETWORK", "")
884
- synapse = env_data.get("SYNAPSE", "")
885
-
886
- except FileNotFoundError:
887
- click.echo("Error: .env with credentials not found")
888
- return
889
- except Exception as e:
890
- click.echo(f"Error reading .env file: {e}")
891
- return
892
-
893
- try:
894
- with open("NODE.md", "r") as f:
895
- nodemd_file = f.read()
896
-
897
- except FileNotFoundError:
898
- click.echo("Error: NODE.md file not found")
899
- return
900
- except Exception as e:
901
- click.echo(f"Error reading NODE.md file: {e}")
902
- return
903
-
904
- if node_type == "partners":
905
- node_type = partners
906
-
907
- url = f"https://{network}/api/update_node"
908
- node = {
909
- "nodeID": nodeID,
910
- "host": host,
911
- "password": password,
912
- "synapse": synapse,
913
- "node_type": node_type,
914
- "nodemd_file": nodemd_file,
915
- "descr": descr,
916
- }
917
-
918
- async with aiohttp.ClientSession() as session:
919
- try:
920
- async with session.post(url, json=node) as response:
921
- response.raise_for_status()
922
- data = await response.json()
923
- nodeID = data["nodeID"]
924
- node_url = data["node_url"]
925
- except aiohttp.ClientError as e:
926
- click.echo(f"Error sending request: {e}")
927
- return
928
-
929
- cell = neuronum.Cell(
930
- host=host,
931
- password=password,
932
- network=network,
933
- synapse=synapse
934
- )
935
-
936
- cells = await cell.list_cells()
937
- tx = await cell.list_tx()
938
- ctx = await cell.list_ctx()
939
- stx = await cell.list_stx()
940
- nodes = await cell.list_nodes()
941
-
942
- await asyncio.to_thread(Path("cells.json").write_text, json.dumps(cells, indent=4))
943
- await asyncio.to_thread(Path("transmitters.json").write_text, json.dumps(tx, indent=4))
944
- await asyncio.to_thread(Path("circuits.json").write_text, json.dumps(ctx, indent=4))
945
- await asyncio.to_thread(Path("streams.json").write_text, json.dumps(stx, indent=4))
946
- await asyncio.to_thread(Path("nodes.json").write_text, json.dumps(nodes, indent=4))
947
-
948
- if node_type == "public":
949
- click.echo(f"Neuronum Node '{nodeID}' updated! Visit: {node_url}")
950
- else:
951
- click.echo(f"Neuronum Node '{nodeID}' updated!")
952
-
953
-
954
- @click.command()
955
- def delete_node():
956
- asyncio.run(async_delete_node())
957
-
958
- async def async_delete_node():
959
- env_data = {}
960
-
961
- try:
962
- with open(".env", "r") as f:
963
- for line in f:
964
- key, value = line.strip().split("=")
965
- env_data[key] = value
966
-
967
- nodeID = env_data.get("NODE", "")
968
- host = env_data.get("HOST", "")
969
- password = env_data.get("PASSWORD", "")
970
- network = env_data.get("NETWORK", "")
971
- synapse = env_data.get("SYNAPSE", "")
972
-
973
- except FileNotFoundError:
974
- click.echo("Error: .env with credentials not found")
975
- return
976
- except Exception as e:
977
- click.echo(f"Error reading .env file: {e}")
978
- return
979
-
980
- url = f"https://{network}/api/delete_node"
981
- node_payload = {
982
- "nodeID": nodeID,
983
- "host": host,
984
- "password": password,
985
- "synapse": synapse
986
- }
987
-
988
- async with aiohttp.ClientSession() as session:
989
- try:
990
- async with session.post(url, json=node_payload) as response:
991
- response.raise_for_status()
992
- data = await response.json()
993
- nodeID = data["nodeID"]
994
- except aiohttp.ClientError as e:
995
- click.echo(f"Error sending request: {e}")
996
- return
997
-
998
- click.echo(f"Neuronum Node '{nodeID}' deleted!")
999
-
1000
-
1001
- @click.command()
1002
- @click.option('--tx', required=True, help="Transmitter ID")
1003
- @click.argument('kvpairs', nargs=-1)
1004
- def activate(tx, kvpairs):
1005
- try:
1006
- data = dict(pair.split(':', 1) for pair in kvpairs)
1007
- except ValueError:
1008
- click.echo("Invalid input. Use key:value pairs.")
1009
- return
1010
-
1011
- asyncio.run(async_activate(tx, data))
1012
-
1013
- async def async_activate(tx, data):
1014
- credentials_folder_path = Path.home() / ".neuronum"
1015
- env_path = credentials_folder_path / ".env"
1016
- env_data = {}
1017
-
1018
- try:
1019
- with open(env_path, "r") as f:
1020
- for line in f:
1021
- key, value = line.strip().split("=")
1022
- env_data[key] = value
1023
- except FileNotFoundError:
1024
- click.echo("No cell connected. Try: neuronum connect-cell")
1025
- return
1026
- except Exception as e:
1027
- click.echo(f"Error reading .env: {e}")
1028
- return
1029
-
1030
- cell = neuronum.Cell(
1031
- host=env_data.get("HOST", ""),
1032
- password=env_data.get("PASSWORD", ""),
1033
- network=env_data.get("NETWORK", ""),
1034
- synapse=env_data.get("SYNAPSE", "")
1035
- )
1036
-
1037
- tx_response = await cell.activate_tx(tx, data)
1038
- click.echo(tx_response)
1039
-
1040
-
1041
- @click.command()
1042
- @click.option('--ctx', required=True, help="Circuit ID")
1043
- @click.argument('label', nargs=-1)
1044
- def load(ctx, label):
1045
- if len(label) > 1 and all(Path(x).exists() for x in label):
1046
- label = "*"
1047
- else:
1048
- label = " ".join(label)
1049
-
1050
- asyncio.run(async_load(ctx, label))
1051
-
1052
-
1053
- async def async_load(ctx, label):
1054
- credentials_folder_path = Path.home() / ".neuronum"
1055
- env_path = credentials_folder_path / ".env"
1056
- env_data = {}
1057
-
1058
- try:
1059
- with open(env_path, "r") as f:
1060
- for line in f:
1061
- key, value = line.strip().split("=")
1062
- env_data[key] = value
1063
- except FileNotFoundError:
1064
- click.echo("No cell connected. Try: neuronum connect-cell")
1065
- return
1066
- except Exception as e:
1067
- click.echo(f"Error reading .env: {e}")
1068
- return
1069
-
1070
- cell = neuronum.Cell(
1071
- host=env_data.get("HOST", ""),
1072
- password=env_data.get("PASSWORD", ""),
1073
- network=env_data.get("NETWORK", ""),
1074
- synapse=env_data.get("SYNAPSE", "")
1075
- )
1076
-
1077
- data = await cell.load(label, ctx)
1078
- click.echo(data)
1079
-
1080
-
1081
- @click.command()
1082
- @click.option('--stx', default=None, help="Stream ID (optional)")
1083
- def sync(stx):
1084
- asyncio.run(async_sync(stx))
1085
-
1086
-
1087
- async def async_sync(stx):
1088
- credentials_folder_path = Path.home() / ".neuronum"
1089
- env_path = credentials_folder_path / ".env"
1090
- env_data = {}
1091
-
1092
- try:
1093
- with open(env_path, "r") as f:
1094
- for line in f:
1095
- key, value = line.strip().split("=")
1096
- env_data[key] = value
1097
- except FileNotFoundError:
1098
- click.echo("No cell connected. Try: neuronum connect-cell")
1099
- return
1100
- except Exception as e:
1101
- click.echo(f"Error reading .env: {e}")
1102
- return
1103
-
1104
- cell = neuronum.Cell(
1105
- host=env_data.get("HOST", ""),
1106
- password=env_data.get("PASSWORD", ""),
1107
- network=env_data.get("NETWORK", ""),
1108
- synapse=env_data.get("SYNAPSE", "")
1109
- )
1110
-
1111
- if stx:
1112
- print(f"Listening to Stream '{stx}'! Close connection with CTRL+C")
1113
- else:
1114
- print(f"Listening to '{cell.host}' private Stream! Close connection with CTRL+C")
1115
- async for operation in cell.sync() if stx is None else cell.sync(stx):
1116
- label = operation.get("label")
1117
- data = operation.get("data")
1118
- ts = operation.get("time")
1119
- stxID = operation.get("stxID")
1120
- operator = operation.get("operator")
1121
- txID = operation.get("txID")
1122
- print(label, data, ts, operator, txID, stxID)
1123
-
1124
-
1125
- cli.add_command(create_cell)
1126
- cli.add_command(connect_cell)
1127
- cli.add_command(view_cell)
1128
- cli.add_command(disconnect_cell)
1129
- cli.add_command(delete_cell)
1130
- cli.add_command(init_node)
1131
- cli.add_command(start_node)
1132
- cli.add_command(restart_node)
1133
- cli.add_command(stop_node)
1134
- cli.add_command(check_node)
1135
- cli.add_command(update_node)
1136
- cli.add_command(delete_node)
1137
- cli.add_command(activate)
1138
- cli.add_command(load)
1139
- cli.add_command(sync)
1140
-
1141
-
1142
- if __name__ == "__main__":
1143
- cli()
1
+ import subprocess
2
+ import os
3
+ import neuronum
4
+ import platform
5
+ import glob
6
+ import asyncio
7
+ import aiohttp
8
+ import click
9
+ import questionary
10
+ from pathlib import Path
11
+ import requests
12
+ import psutil
13
+ from datetime import datetime
14
+ import sys
15
+
16
+
17
+ @click.group()
18
+ def cli():
19
+ """Neuronum CLI Tool"""
20
+
21
+
22
+ @click.command()
23
+ def create_cell():
24
+ cell_type = questionary.select(
25
+ "Choose Cell type:",
26
+ choices=["business", "community"]
27
+ ).ask()
28
+
29
+ network = questionary.select(
30
+ "Choose Network:",
31
+ choices=["neuronum.net"]
32
+ ).ask()
33
+
34
+ if cell_type == "business":
35
+ click.echo("Visit https://neuronum.net/createcell to create your Neuronum Business Cell")
36
+
37
+ if cell_type == "community":
38
+
39
+ email = click.prompt("Enter email")
40
+ password = click.prompt("Enter password", hide_input=True)
41
+ repeat_password = click.prompt("Repeat password", hide_input=True)
42
+
43
+ if password != repeat_password:
44
+ click.echo("Passwords do not match!")
45
+ return
46
+
47
+ url = f"https://{network}/api/create_cell/{cell_type}"
48
+
49
+ create_cell = {"email": email, "password": password}
50
+
51
+ try:
52
+ response = requests.post(url, json=create_cell)
53
+ response.raise_for_status()
54
+ status = response.json()["status"]
55
+
56
+ except requests.exceptions.RequestException as e:
57
+ click.echo(f"Error sending request: {e}")
58
+ return
59
+
60
+ if status == True:
61
+ host = response.json()["host"]
62
+ cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
63
+
64
+ url = f"https://{network}/api/verify_email"
65
+
66
+ verify_email = {"host": host, "email": email, "cellkey": cellkey}
67
+
68
+ try:
69
+ response = requests.post(url, json=verify_email)
70
+ response.raise_for_status()
71
+ status = response.json()["status"]
72
+
73
+ except requests.exceptions.RequestException as e:
74
+ click.echo(f"Error sending request: {e}")
75
+ return
76
+
77
+ if status == True:
78
+ synapse = response.json()["synapse"]
79
+ credentials_folder_path = Path.home() / ".neuronum"
80
+ credentials_folder_path.mkdir(parents=True, exist_ok=True)
81
+
82
+ env_path = credentials_folder_path / ".env"
83
+ env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
84
+
85
+ click.echo(f"Welcome to Neuronum! Community Cell '{host}' created and connected!")
86
+
87
+ if status == False:
88
+ click.echo(f"Error:'{email}' already assigned!")
89
+
90
+
91
+ @click.command()
92
+ def connect_cell():
93
+ email = click.prompt("Enter your Email")
94
+ password = click.prompt("Enter password", hide_input=True)
95
+
96
+ network = questionary.select(
97
+ "Choose Network:",
98
+ choices=["neuronum.net"]
99
+ ).ask()
100
+
101
+ url = f"https://{network}/api/connect_cell"
102
+ payload = {"email": email, "password": password}
103
+
104
+ try:
105
+ response = requests.post(url, json=payload)
106
+ response.raise_for_status()
107
+ status = response.json()["status"]
108
+ host = response.json()["host"]
109
+ except requests.exceptions.RequestException as e:
110
+ click.echo(f"Error connecting: {e}")
111
+ return
112
+
113
+ if status == True:
114
+ cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
115
+ url = f"https://{network}/api/verify_email"
116
+ verify_email = {"host": host, "email": email, "cellkey": cellkey}
117
+
118
+ try:
119
+ response = requests.post(url, json=verify_email)
120
+ response.raise_for_status()
121
+ status = response.json()["status"]
122
+ synapse = response.json()["synapse"]
123
+
124
+ except requests.exceptions.RequestException as e:
125
+ click.echo(f"Error sending request: {e}")
126
+ return
127
+
128
+ if status == True:
129
+ credentials_folder_path = Path.home() / ".neuronum"
130
+ credentials_folder_path.mkdir(parents=True, exist_ok=True)
131
+
132
+ env_path = credentials_folder_path / f".env"
133
+ env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
134
+
135
+ click.echo(f"Cell '{host}' connected!")
136
+ else:
137
+ click.echo(f"Connection failed!")
138
+
139
+
140
+ @click.command()
141
+ def view_cell():
142
+ credentials_folder_path = Path.home() / ".neuronum"
143
+ env_path = credentials_folder_path / ".env"
144
+
145
+ env_data = {}
146
+
147
+ try:
148
+ with open(env_path, "r") as f:
149
+ for line in f:
150
+ key, value = line.strip().split("=")
151
+ env_data[key] = value
152
+
153
+ host = env_data.get("HOST", "")
154
+
155
+ except FileNotFoundError:
156
+ click.echo("Error: No credentials found. Please connect to a cell first.")
157
+ return
158
+ except Exception as e:
159
+ click.echo(f"Error reading .env file: {e}")
160
+ return
161
+
162
+ if host:
163
+ click.echo(f"Connected Cell: '{host}'")
164
+ else:
165
+ click.echo("No active cell connection found.")
166
+
167
+
168
+ @click.command()
169
+ def disconnect_cell():
170
+ credentials_folder_path = Path.home() / ".neuronum"
171
+ env_path = credentials_folder_path / ".env"
172
+
173
+ env_data = {}
174
+
175
+ try:
176
+ with open(env_path, "r") as f:
177
+ for line in f:
178
+ key, value = line.strip().split("=")
179
+ env_data[key] = value
180
+
181
+ host = env_data.get("HOST", "")
182
+
183
+ except FileNotFoundError:
184
+ click.echo("Error: .env with credentials not found")
185
+ return
186
+ except Exception as e:
187
+ click.echo(f"Error reading .env file: {e}")
188
+ return
189
+
190
+ if env_path.exists():
191
+ if click.confirm(f"Are you sure you want to disconnect Cell '{host}'?", default=True):
192
+ os.remove(env_path)
193
+ click.echo(f"'{host}' disconnected!")
194
+ else:
195
+ click.echo("Disconnect canceled.")
196
+ else:
197
+ click.echo(f"No Neuronum Cell connected!")
198
+
199
+
200
+ @click.command()
201
+ def delete_cell():
202
+ credentials_folder_path = Path.home() / ".neuronum"
203
+ env_path = credentials_folder_path / ".env"
204
+
205
+ env_data = {}
206
+
207
+ try:
208
+ with open(env_path, "r") as f:
209
+ for line in f:
210
+ key, value = line.strip().split("=")
211
+ env_data[key] = value
212
+
213
+ host = env_data.get("HOST", "")
214
+ password = env_data.get("PASSWORD", "")
215
+ network = env_data.get("NETWORK", "")
216
+ synapse = env_data.get("SYNAPSE", "")
217
+
218
+ except FileNotFoundError:
219
+ click.echo("Error: No cell connected. Connect Cell first to delete")
220
+ return
221
+ except Exception as e:
222
+ click.echo(f"Error reading .env file: {e}")
223
+ return
224
+
225
+ confirm = click.confirm(f" Are you sure you want to delete '{host}'?", default=True)
226
+ if not confirm:
227
+ click.echo("Deletion canceled.")
228
+ return
229
+
230
+ url = f"https://{network}/api/delete_cell"
231
+ payload = {"host": host, "password": password, "synapse": synapse}
232
+
233
+ try:
234
+ response = requests.delete(url, json=payload)
235
+ response.raise_for_status()
236
+ status = response.json()["status"]
237
+ except requests.exceptions.RequestException as e:
238
+ click.echo(f"Error deleting cell: {e}")
239
+ return
240
+
241
+ if status == True:
242
+ env_path = credentials_folder_path / f"{host}.env"
243
+ if env_path.exists():
244
+ os.remove(env_path)
245
+ click.echo("Credentials deleted successfully!")
246
+ click.echo(f"Neuronum Cell '{host}' has been deleted!")
247
+ else:
248
+ click.echo(f"Neuronum Cell '{host}' deletion failed!")
249
+
250
+
251
+ @click.command()
252
+ @click.option('--sync', multiple=True, default=None, help="Optional stream IDs for sync.")
253
+ @click.option('--stream', multiple=True, default=None, help="Optional stream ID for stream.")
254
+ @click.option('--app', is_flag=True, help="Generate a Node with app template")
255
+ def 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))
261
+
262
+ async def async_init_node(sync, stream, app, descr):
263
+ credentials_folder_path = Path.home() / ".neuronum"
264
+ env_path = credentials_folder_path / ".env"
265
+
266
+ env_data = {}
267
+
268
+ try:
269
+ with open(env_path, "r") as f:
270
+ for line in f:
271
+ key, value = line.strip().split("=")
272
+ env_data[key] = value
273
+
274
+ host = env_data.get("HOST", "")
275
+ password = env_data.get("PASSWORD", "")
276
+ network = env_data.get("NETWORK", "")
277
+ synapse = env_data.get("SYNAPSE", "")
278
+
279
+ cell = neuronum.Cell(
280
+ host=host,
281
+ password=password,
282
+ network=network,
283
+ synapse=synapse
284
+ )
285
+
286
+ except FileNotFoundError:
287
+ click.echo("No cell connected. Connect your cell with command neuronum connect-cell")
288
+ return
289
+ except Exception as e:
290
+ click.echo(f"Error reading .env file: {e}")
291
+ return
292
+
293
+ url = f"https://{network}/api/init_node"
294
+ node = {
295
+ "host": host,
296
+ "password": password,
297
+ "synapse": synapse,
298
+ "descr": descr,
299
+ }
300
+
301
+ async with aiohttp.ClientSession() as session:
302
+ try:
303
+ async with session.post(url, json=node) as response:
304
+ response.raise_for_status()
305
+ data = await response.json()
306
+ nodeID = data["nodeID"]
307
+ except aiohttp.ClientError as e:
308
+ click.echo(f"Error sending request: {e}")
309
+ return
310
+
311
+ node_filename = "node_" + nodeID.replace("::node", "")
312
+ project_path = Path(node_filename)
313
+ project_path.mkdir(exist_ok=True)
314
+
315
+ env_path = project_path / ".env"
316
+ await asyncio.to_thread(env_path.write_text, f"NODE={nodeID}\nHOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
317
+
318
+
319
+ config_path = project_path / "config.json"
320
+ await asyncio.to_thread(config_path.write_text, """{
321
+ "data_gateways": [
322
+ {
323
+ "type": "stream",
324
+ "id": "id::stx",
325
+ "info": "provide a detailed description about the stream"
326
+ },
327
+ {
328
+ "type": "transmitter",
329
+ "id": "id::tx",
330
+ "info": "provide a detailed description about the transmitter"
331
+ },
332
+ {
333
+ "type": "circuit",
334
+ "id": "id::ctx",
335
+ "info": "provide a detailed description about the circuit"
336
+ }
337
+ ]
338
+ }
339
+ """
340
+ )
341
+
342
+ nodemd_path = project_path / "NODE.md"
343
+ await asyncio.to_thread(nodemd_path.write_text, """### NODE.md: Create a detailed Markdown File on how to interact with this Node""")
344
+
345
+ stx = sync[0] if sync else (stream[0] if stream else host.replace("::cell", "::stx"))
346
+
347
+ if sync:
348
+ for stx in sync:
349
+ sync_path = project_path / f"sync_{stx.replace('::stx', '')}.py"
350
+ sync_path.write_text(f"""\
351
+ import asyncio
352
+ import neuronum
353
+ import os
354
+ from dotenv import load_dotenv
355
+
356
+ load_dotenv()
357
+ host = os.getenv("HOST")
358
+ password = os.getenv("PASSWORD")
359
+ network = os.getenv("NETWORK")
360
+ synapse = os.getenv("SYNAPSE")
361
+
362
+ cell = neuronum.Cell(
363
+ host=host,
364
+ password=password,
365
+ network=network,
366
+ synapse=synapse
367
+ )
368
+
369
+ async def main():
370
+ STX = "{stx}"
371
+ async for operation in cell.sync(STX):
372
+ label = operation.get("label")
373
+ data = operation.get("data")
374
+ ts = operation.get("time")
375
+ stxID = operation.get("stxID")
376
+ operator = operation.get("operator")
377
+ print(label, data, ts, stxID, operator)
378
+
379
+ asyncio.run(main())
380
+ """)
381
+
382
+
383
+ if stream:
384
+ for stx in stream:
385
+ stream_path = project_path / f"stream_{stx.replace('::stx', '')}.py"
386
+ stream_path.write_text(f"""\
387
+ import asyncio
388
+ import neuronum
389
+ import os
390
+ from dotenv import load_dotenv
391
+ import time
392
+
393
+ load_dotenv()
394
+ host = os.getenv("HOST")
395
+ password = os.getenv("PASSWORD")
396
+ network = os.getenv("NETWORK")
397
+ synapse = os.getenv("SYNAPSE")
398
+
399
+ cell = neuronum.Cell(
400
+ host=host,
401
+ password=password,
402
+ network=network,
403
+ synapse=synapse
404
+ )
405
+
406
+ async def main():
407
+ STX = "{stx}"
408
+ label = "your_label"
409
+
410
+ while True:
411
+ data = {{
412
+ "key1": "value1",
413
+ "key2": "value2",
414
+ "key3": "value3",
415
+ }}
416
+ await cell.stream(label, data, STX)
417
+ time.sleep(5)
418
+
419
+ asyncio.run(main())
420
+ """)
421
+
422
+ if not sync and not stream and not app:
423
+ sync_path = project_path / f"sync_{stx.replace('::stx', '')}.py"
424
+ sync_path.write_text(f"""\
425
+ import asyncio
426
+ import neuronum
427
+ import os
428
+ from dotenv import load_dotenv
429
+
430
+ load_dotenv()
431
+ host = os.getenv("HOST")
432
+ password = os.getenv("PASSWORD")
433
+ network = os.getenv("NETWORK")
434
+ synapse = os.getenv("SYNAPSE")
435
+
436
+ cell = neuronum.Cell(
437
+ host=host,
438
+ password=password,
439
+ network=network,
440
+ synapse=synapse
441
+ )
442
+
443
+ async def main():
444
+ async for operation in cell.sync():
445
+ message = operation.get("data").get("message")
446
+ print(message)
447
+
448
+ asyncio.run(main())
449
+ """)
450
+
451
+ stream_path = project_path / f"stream_{stx.replace('::stx', '')}.py"
452
+ stream_path.write_text(f"""\
453
+ import asyncio
454
+ import neuronum
455
+ import os
456
+ from dotenv import load_dotenv
457
+ import time
458
+
459
+ load_dotenv()
460
+ host = os.getenv("HOST")
461
+ password = os.getenv("PASSWORD")
462
+ network = os.getenv("NETWORK")
463
+ synapse = os.getenv("SYNAPSE")
464
+
465
+ cell = neuronum.Cell(
466
+ host=host,
467
+ password=password,
468
+ network=network,
469
+ synapse=synapse
470
+ )
471
+
472
+ async def main():
473
+ label = "Welcome to Neuronum"
474
+
475
+ while True:
476
+ data = {{
477
+ "message": "Hello, Neuronum!"
478
+ }}
479
+ await cell.stream(label, data)
480
+ time.sleep(5)
481
+
482
+ asyncio.run(main())
483
+ """)
484
+
485
+ if app and nodeID:
486
+
487
+ descr = f"{nodeID} App"
488
+ partners = ["private"]
489
+ stxID = await cell.create_stx(descr, partners)
490
+
491
+
492
+ descr = f"Greet {nodeID}"
493
+ key_values = {
494
+ "say": "hello",
495
+ }
496
+ STX = stxID
497
+ label = "say:hello"
498
+ partners = ["private"]
499
+ txID = await cell.create_tx(descr, key_values, STX, label, partners)
500
+
501
+
502
+ app_path = project_path / "app.py"
503
+ app_path.write_text(f"""\
504
+ import asyncio
505
+ import neuronum
506
+ import os
507
+ from dotenv import load_dotenv
508
+
509
+ load_dotenv()
510
+ host = os.getenv("HOST")
511
+ password = os.getenv("PASSWORD")
512
+ network = os.getenv("NETWORK")
513
+ synapse = os.getenv("SYNAPSE")
514
+
515
+ cell = neuronum.Cell(
516
+ host=host,
517
+ password=password,
518
+ network=network,
519
+ synapse=synapse
520
+ )
521
+
522
+ async def main():
523
+ STX = "{stxID}"
524
+ async for operation in cell.sync(STX):
525
+ txID = operation.get("txID")
526
+ client = operation.get("operator")
527
+
528
+ if txID == "{txID}":
529
+ data = {{
530
+ "json": f"Hello {{client}} from {nodeID}",
531
+ "html": f\"\"\"
532
+ <!DOCTYPE html>
533
+ <html>
534
+ <head>
535
+ <meta charset="UTF-8">
536
+ <title>Greeting Node</title>
537
+ </head>
538
+ <body>
539
+ <div class="card">
540
+ <h1>Hello, {{client}}</h1>
541
+ <p>Greetings from <span class="node">{nodeID}</span></p>
542
+ </div>
543
+ </body>
544
+ </html>
545
+ \"\"\"
546
+
547
+ }}
548
+ await cell.tx_response(txID, client, data)
549
+
550
+ asyncio.run(main())
551
+ """)
552
+
553
+ click.echo(f"Neuronum Node '{nodeID}' initialized!")
554
+
555
+
556
+ @click.command()
557
+ @click.option('--d', is_flag=True, help="Start node in detached mode")
558
+ def start_node(d):
559
+ pid_file = Path.cwd() / "status.txt"
560
+ system_name = platform.system()
561
+ active_pids = []
562
+
563
+ start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
564
+
565
+ if pid_file.exists():
566
+ try:
567
+ with open(pid_file, "r") as f:
568
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
569
+ for pid in pids:
570
+ if system_name == "Windows":
571
+ if psutil.pid_exists(pid):
572
+ active_pids.append(pid)
573
+ else:
574
+ try:
575
+ os.kill(pid, 0)
576
+ active_pids.append(pid)
577
+ except OSError:
578
+ continue
579
+ except Exception as e:
580
+ click.echo(f"Failed to read PID file: {e}")
581
+
582
+ if active_pids:
583
+ click.echo(f"Node is already running. Active PIDs: {', '.join(map(str, active_pids))}")
584
+ return
585
+
586
+ click.echo("Starting Node...")
587
+
588
+ project_path = Path.cwd()
589
+ script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
590
+ processes = []
591
+
592
+ for script in script_files:
593
+ script_path = project_path / script
594
+ if script_path.exists():
595
+ python_cmd = "pythonw" if system_name == "Windows" else "python"
596
+
597
+ if d:
598
+ process = subprocess.Popen(
599
+ ["nohup", python_cmd, str(script_path), "&"] if system_name != "Windows"
600
+ else [python_cmd, str(script_path)],
601
+ stdout=subprocess.DEVNULL,
602
+ stderr=subprocess.DEVNULL,
603
+ start_new_session=True
604
+ )
605
+ else:
606
+ process = subprocess.Popen(
607
+ [sys.executable, str(script_path)],
608
+ stdout=None,
609
+ stderr=None
610
+ )
611
+
612
+ processes.append(process.pid)
613
+
614
+ if not processes:
615
+ click.echo("Error: No valid node script found. Ensure the node is set up correctly.")
616
+ return
617
+
618
+ with open(pid_file, "w") as f:
619
+ f.write(f"Started at: {start_time}\n")
620
+ f.write("\n".join(map(str, processes)))
621
+
622
+ click.echo(f"Node started successfully with PIDs: {', '.join(map(str, processes))}")
623
+
624
+
625
+ @click.command()
626
+ def check_node():
627
+ click.echo("Checking Node status...")
628
+
629
+ env_data = {}
630
+ try:
631
+ with open(".env", "r") as f:
632
+ for line in f:
633
+ if "=" in line:
634
+ key, value = line.strip().split("=", 1)
635
+ env_data[key] = value
636
+ nodeID = env_data.get("NODE", "")
637
+ except FileNotFoundError:
638
+ click.echo("Error: .env with credentials not found")
639
+ return
640
+ except Exception as e:
641
+ click.echo(f"Error reading .env file: {e}")
642
+ return
643
+
644
+ pid_file = Path.cwd() / "status.txt"
645
+
646
+ if not pid_file.exists():
647
+ click.echo(f"Node {nodeID} is not running. Status file missing.")
648
+ return
649
+
650
+ try:
651
+ with open(pid_file, "r") as f:
652
+ lines = f.readlines()
653
+ timestamp_line = next((line for line in lines if line.startswith("Started at:")), None)
654
+ pids = [int(line.strip()) for line in lines if line.strip().isdigit()]
655
+
656
+ if timestamp_line:
657
+ click.echo(timestamp_line.strip())
658
+ start_time = datetime.strptime(timestamp_line.split(":", 1)[1].strip(), "%Y-%m-%d %H:%M:%S")
659
+ now = datetime.now()
660
+ uptime = now - start_time
661
+ click.echo(f"Uptime: {str(uptime).split('.')[0]}")
662
+ except Exception as e:
663
+ click.echo(f"Failed to read PID file: {e}")
664
+ return
665
+
666
+ system_name = platform.system()
667
+ running_pids = []
668
+
669
+ for pid in pids:
670
+ if system_name == "Windows":
671
+ if psutil.pid_exists(pid):
672
+ running_pids.append(pid)
673
+ else:
674
+ try:
675
+ os.kill(pid, 0)
676
+ running_pids.append(pid)
677
+ except OSError:
678
+ continue
679
+
680
+ if running_pids:
681
+ for pid in running_pids:
682
+ proc = psutil.Process(pid)
683
+ mem = proc.memory_info().rss / (1024 * 1024) # in MB
684
+ cpu = proc.cpu_percent(interval=0.1)
685
+ click.echo(f"PID {pid} → Memory: {mem:.2f} MB | CPU: {cpu:.1f}%")
686
+ click.echo(f"Node {nodeID} is running. Active PIDs: {', '.join(map(str, running_pids))}")
687
+ else:
688
+ click.echo(f"Node {nodeID} is not running.")
689
+
690
+
691
+ @click.command()
692
+ @click.option('--d', is_flag=True, help="Restart node in detached mode")
693
+ def restart_node(d):
694
+ pid_file = Path.cwd() / "status.txt"
695
+ system_name = platform.system()
696
+
697
+ start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
698
+
699
+ env_data = {}
700
+ try:
701
+ with open(".env", "r") as f:
702
+ for line in f:
703
+ key, value = line.strip().split("=")
704
+ env_data[key] = value
705
+
706
+ nodeID = env_data.get("NODE", "")
707
+
708
+ except FileNotFoundError:
709
+ print("Error: .env with credentials not found")
710
+ return
711
+ except Exception as e:
712
+ print(f"Error reading .env file: {e}")
713
+ return
714
+
715
+ if pid_file.exists():
716
+ try:
717
+ with open(pid_file, "r") as f:
718
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
719
+
720
+ for pid in pids:
721
+ if system_name == "Windows":
722
+ if psutil.pid_exists(pid):
723
+ proc = psutil.Process(pid)
724
+ proc.terminate()
725
+ else:
726
+ try:
727
+ os.kill(pid, 15)
728
+ except OSError:
729
+ continue
730
+
731
+ pid_file.unlink()
732
+
733
+ click.echo(f"Terminated existing {nodeID} processes: {', '.join(map(str, pids))}")
734
+
735
+ except Exception as e:
736
+ click.echo(f"Failed to terminate processes: {e}")
737
+ return
738
+ else:
739
+ click.echo(f"Node {nodeID} is not running")
740
+
741
+ click.echo(f"Starting Node {nodeID}...")
742
+ project_path = Path.cwd()
743
+ script_files = glob.glob("sync_*.py") + glob.glob("stream_*.py") + glob.glob("app.py")
744
+ processes = []
745
+
746
+ python_cmd = "pythonw" if system_name == "Windows" else "python"
747
+
748
+ for script in script_files:
749
+ script_path = project_path / script
750
+ if script_path.exists():
751
+ if d:
752
+ process = subprocess.Popen(
753
+ ["nohup", python_cmd, str(script_path), "&"] if system_name != "Windows"
754
+ else [python_cmd, str(script_path)],
755
+ stdout=subprocess.DEVNULL,
756
+ stderr=subprocess.DEVNULL,
757
+ start_new_session=True
758
+ )
759
+ else:
760
+ process = subprocess.Popen(
761
+ [sys.executable, str(script_path)],
762
+ stdout=None,
763
+ stderr=None
764
+ )
765
+
766
+ processes.append(process.pid)
767
+
768
+ if not processes:
769
+ click.echo("Error: No valid node script found.")
770
+ return
771
+
772
+ with open(pid_file, "w") as f:
773
+ f.write(f"Started at: {start_time}\n")
774
+ f.write("\n".join(map(str, processes)))
775
+
776
+ click.echo(f"Node {nodeID} started with new PIDs: {', '.join(map(str, processes))}")
777
+
778
+
779
+ @click.command()
780
+ def stop_node():
781
+ asyncio.run(async_stop_node())
782
+
783
+ async def async_stop_node():
784
+ click.echo("Stopping Node...")
785
+
786
+ node_pid_path = Path("status.txt")
787
+
788
+ env_data = {}
789
+ try:
790
+ with open(".env", "r") as f:
791
+ for line in f:
792
+ key, value = line.strip().split("=")
793
+ env_data[key] = value
794
+
795
+ nodeID = env_data.get("NODE", "")
796
+
797
+ except FileNotFoundError:
798
+ print("Error: .env with credentials not found")
799
+ return
800
+ except Exception as e:
801
+ print(f"Error reading .env file: {e}")
802
+ return
803
+
804
+ try:
805
+ with open("status.txt", "r") as f:
806
+ pids = [int(line.strip()) for line in f if line.strip().isdigit()]
807
+
808
+ system_name = platform.system()
809
+
810
+ for pid in pids:
811
+ try:
812
+ if system_name == "Windows":
813
+ await asyncio.to_thread(subprocess.run, ["taskkill", "/F", "/PID", str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
814
+ else:
815
+ await asyncio.to_thread(os.kill, pid, 9)
816
+ except ProcessLookupError:
817
+ click.echo(f"Warning: Process {pid} already stopped or does not exist.")
818
+
819
+ await asyncio.to_thread(os.remove, node_pid_path)
820
+ click.echo(f"Node {nodeID} stopped successfully!")
821
+
822
+ except FileNotFoundError:
823
+ click.echo("Error: No active node process found.")
824
+ except subprocess.CalledProcessError:
825
+ click.echo("Error: Unable to stop some node processes.")
826
+
827
+
828
+ @click.command()
829
+ def update_node():
830
+ click.echo("Update your Node")
831
+ env_data = {}
832
+
833
+ try:
834
+ with open(".env", "r") as f:
835
+ for line in f:
836
+ key, value = line.strip().split("=")
837
+ env_data[key] = value
838
+
839
+ host = env_data.get("HOST", "")
840
+
841
+ except FileNotFoundError:
842
+ click.echo("Error: .env with credentials not found")
843
+ return
844
+ except Exception as e:
845
+ click.echo(f"Error reading .env file: {e}")
846
+ return
847
+
848
+ if host.startswith("CMTY_"):
849
+ node_type = questionary.select(
850
+ "Community Cells can only create private Nodes",
851
+ choices=["private"]
852
+ ).ask()
853
+ else:
854
+ node_type = questionary.select(
855
+ "Who can view your Node?:",
856
+ choices=["public", "private", "partners"]
857
+ ).ask()
858
+ partners = "None"
859
+ if node_type == "partners":
860
+ prompt_msg = (
861
+ "Enter the list of partners who can view this Node.\n"
862
+ "Format: partner::cell, partner::cell, partner::cell\n"
863
+ "Press Enter to leave the list unchanged"
864
+ )
865
+ partners = click.prompt(
866
+ prompt_msg,
867
+ default="None",
868
+ show_default=False
869
+ ).strip()
870
+ descr = click.prompt(
871
+ "Update Node description: Type up to 25 characters, or press Enter to leave it unchanged",
872
+ default="None",
873
+ show_default=False
874
+ ).strip()
875
+ if descr and len(descr) > 25:
876
+ click.echo("Description too long. Max 25 characters allowed.")
877
+ return
878
+ asyncio.run(async_update_node(node_type, descr, partners))
879
+
880
+ async def async_update_node(node_type: str, descr: str, partners:str) -> None:
881
+ env_data = {}
882
+
883
+ try:
884
+ with open(".env", "r") as f:
885
+ for line in f:
886
+ key, value = line.strip().split("=")
887
+ env_data[key] = value
888
+
889
+ nodeID = env_data.get("NODE", "")
890
+ host = env_data.get("HOST", "")
891
+ password = env_data.get("PASSWORD", "")
892
+ network = env_data.get("NETWORK", "")
893
+ synapse = env_data.get("SYNAPSE", "")
894
+
895
+ except FileNotFoundError:
896
+ click.echo("Error: .env with credentials not found")
897
+ return
898
+ except Exception as e:
899
+ click.echo(f"Error reading .env file: {e}")
900
+ return
901
+
902
+ try:
903
+ with open("NODE.md", "r") as f:
904
+ nodemd_file = f.read()
905
+
906
+ with open("config.json", "r") as f:
907
+ config_file = f.read()
908
+
909
+ except FileNotFoundError:
910
+ click.echo("Error: NODE.md file not found")
911
+ return
912
+ except Exception as e:
913
+ click.echo(f"Error reading NODE.md file: {e}")
914
+ return
915
+
916
+ if node_type == "partners":
917
+ node_type = partners
918
+
919
+ url = f"https://{network}/api/update_node"
920
+ node = {
921
+ "nodeID": nodeID,
922
+ "host": host,
923
+ "password": password,
924
+ "synapse": synapse,
925
+ "node_type": node_type,
926
+ "nodemd_file": nodemd_file,
927
+ "config_file": config_file,
928
+ "descr": descr,
929
+ }
930
+
931
+ async with aiohttp.ClientSession() as session:
932
+ try:
933
+ async with session.post(url, json=node) as response:
934
+ response.raise_for_status()
935
+ data = await response.json()
936
+ nodeID = data["nodeID"]
937
+ node_url = data["node_url"]
938
+ except aiohttp.ClientError as e:
939
+ click.echo(f"Error sending request: {e}")
940
+ return
941
+
942
+ if node_type == "public":
943
+ click.echo(f"Neuronum Node '{nodeID}' updated! Visit: {node_url}")
944
+ else:
945
+ click.echo(f"Neuronum Node '{nodeID}' updated!")
946
+
947
+
948
+ @click.command()
949
+ def delete_node():
950
+ asyncio.run(async_delete_node())
951
+
952
+ async def async_delete_node():
953
+ env_data = {}
954
+
955
+ try:
956
+ with open(".env", "r") as f:
957
+ for line in f:
958
+ key, value = line.strip().split("=")
959
+ env_data[key] = value
960
+
961
+ nodeID = env_data.get("NODE", "")
962
+ host = env_data.get("HOST", "")
963
+ password = env_data.get("PASSWORD", "")
964
+ network = env_data.get("NETWORK", "")
965
+ synapse = env_data.get("SYNAPSE", "")
966
+
967
+ except FileNotFoundError:
968
+ click.echo("Error: .env with credentials not found")
969
+ return
970
+ except Exception as e:
971
+ click.echo(f"Error reading .env file: {e}")
972
+ return
973
+
974
+ url = f"https://{network}/api/delete_node"
975
+ node_payload = {
976
+ "nodeID": nodeID,
977
+ "host": host,
978
+ "password": password,
979
+ "synapse": synapse
980
+ }
981
+
982
+ async with aiohttp.ClientSession() as session:
983
+ try:
984
+ async with session.post(url, json=node_payload) as response:
985
+ response.raise_for_status()
986
+ data = await response.json()
987
+ nodeID = data["nodeID"]
988
+ except aiohttp.ClientError as e:
989
+ click.echo(f"Error sending request: {e}")
990
+ return
991
+
992
+ click.echo(f"Neuronum Node '{nodeID}' deleted!")
993
+
994
+
995
+ @click.command()
996
+ @click.option('--tx', required=True, help="Transmitter ID")
997
+ @click.argument('kvpairs', nargs=-1)
998
+ def activate(tx, kvpairs):
999
+ try:
1000
+ data = dict(pair.split(':', 1) for pair in kvpairs)
1001
+ except ValueError:
1002
+ click.echo("Invalid input. Use key:value pairs.")
1003
+ return
1004
+
1005
+ asyncio.run(async_activate(tx, data))
1006
+
1007
+ async def async_activate(tx, data):
1008
+ credentials_folder_path = Path.home() / ".neuronum"
1009
+ env_path = credentials_folder_path / ".env"
1010
+ env_data = {}
1011
+
1012
+ try:
1013
+ with open(env_path, "r") as f:
1014
+ for line in f:
1015
+ key, value = line.strip().split("=")
1016
+ env_data[key] = value
1017
+ except FileNotFoundError:
1018
+ click.echo("No cell connected. Try: neuronum connect-cell")
1019
+ return
1020
+ except Exception as e:
1021
+ click.echo(f"Error reading .env: {e}")
1022
+ return
1023
+
1024
+ cell = neuronum.Cell(
1025
+ host=env_data.get("HOST", ""),
1026
+ password=env_data.get("PASSWORD", ""),
1027
+ network=env_data.get("NETWORK", ""),
1028
+ synapse=env_data.get("SYNAPSE", "")
1029
+ )
1030
+
1031
+ tx_response = await cell.activate_tx(tx, data)
1032
+ click.echo(tx_response)
1033
+
1034
+
1035
+ @click.command()
1036
+ @click.option('--ctx', required=True, help="Circuit ID")
1037
+ @click.argument('label', nargs=-1)
1038
+ def load(ctx, label):
1039
+ if len(label) > 1 and all(Path(x).exists() for x in label):
1040
+ label = "*"
1041
+ else:
1042
+ label = " ".join(label)
1043
+
1044
+ asyncio.run(async_load(ctx, label))
1045
+
1046
+
1047
+ async def async_load(ctx, label):
1048
+ credentials_folder_path = Path.home() / ".neuronum"
1049
+ env_path = credentials_folder_path / ".env"
1050
+ env_data = {}
1051
+
1052
+ try:
1053
+ with open(env_path, "r") as f:
1054
+ for line in f:
1055
+ key, value = line.strip().split("=")
1056
+ env_data[key] = value
1057
+ except FileNotFoundError:
1058
+ click.echo("No cell connected. Try: neuronum connect-cell")
1059
+ return
1060
+ except Exception as e:
1061
+ click.echo(f"Error reading .env: {e}")
1062
+ return
1063
+
1064
+ cell = neuronum.Cell(
1065
+ host=env_data.get("HOST", ""),
1066
+ password=env_data.get("PASSWORD", ""),
1067
+ network=env_data.get("NETWORK", ""),
1068
+ synapse=env_data.get("SYNAPSE", "")
1069
+ )
1070
+
1071
+ data = await cell.load(label, ctx)
1072
+ click.echo(data)
1073
+
1074
+
1075
+ @click.command()
1076
+ @click.option('--stx', default=None, help="Stream ID (optional)")
1077
+ def sync(stx):
1078
+ asyncio.run(async_sync(stx))
1079
+
1080
+
1081
+ async def async_sync(stx):
1082
+ credentials_folder_path = Path.home() / ".neuronum"
1083
+ env_path = credentials_folder_path / ".env"
1084
+ env_data = {}
1085
+
1086
+ try:
1087
+ with open(env_path, "r") as f:
1088
+ for line in f:
1089
+ key, value = line.strip().split("=")
1090
+ env_data[key] = value
1091
+ except FileNotFoundError:
1092
+ click.echo("No cell connected. Try: neuronum connect-cell")
1093
+ return
1094
+ except Exception as e:
1095
+ click.echo(f"Error reading .env: {e}")
1096
+ return
1097
+
1098
+ cell = neuronum.Cell(
1099
+ host=env_data.get("HOST", ""),
1100
+ password=env_data.get("PASSWORD", ""),
1101
+ network=env_data.get("NETWORK", ""),
1102
+ synapse=env_data.get("SYNAPSE", "")
1103
+ )
1104
+
1105
+ if stx:
1106
+ print(f"Listening to Stream '{stx}'! Close connection with CTRL+C")
1107
+ else:
1108
+ print(f"Listening to '{cell.host}' private Stream! Close connection with CTRL+C")
1109
+ async for operation in cell.sync() if stx is None else cell.sync(stx):
1110
+ label = operation.get("label")
1111
+ data = operation.get("data")
1112
+ ts = operation.get("time")
1113
+ stxID = operation.get("stxID")
1114
+ operator = operation.get("operator")
1115
+ txID = operation.get("txID")
1116
+ print(label, data, ts, operator, txID, stxID)
1117
+
1118
+
1119
+ cli.add_command(create_cell)
1120
+ cli.add_command(connect_cell)
1121
+ cli.add_command(view_cell)
1122
+ cli.add_command(disconnect_cell)
1123
+ cli.add_command(delete_cell)
1124
+ cli.add_command(init_node)
1125
+ cli.add_command(start_node)
1126
+ cli.add_command(restart_node)
1127
+ cli.add_command(stop_node)
1128
+ cli.add_command(check_node)
1129
+ cli.add_command(update_node)
1130
+ cli.add_command(delete_node)
1131
+ cli.add_command(activate)
1132
+ cli.add_command(load)
1133
+ cli.add_command(sync)
1134
+
1135
+
1136
+ if __name__ == "__main__":
1137
+ cli()