neuronum 8.0.0__py3-none-any.whl → 8.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

cli/main.py CHANGED
@@ -340,21 +340,21 @@ async def async_init_node(descr):
340
340
  app_path = project_path / "app.py"
341
341
  app_path.write_text(f"""\
342
342
  import asyncio
343
- import neuronum
343
+ from neuronum import Node
344
344
  from jinja2 import Environment, FileSystemLoader
345
345
 
346
346
  env = Environment(loader=FileSystemLoader('.'))
347
347
  template = env.get_template('ping.html')
348
348
 
349
- cell = neuronum.Cell(
350
- private_key_path="{private_key_file}",
351
- public_key_path="{public_key_file}"
349
+ node = Node(
350
+ id="{node_id}",
351
+ private_key="{private_key_file}",
352
+ public_key="{public_key_file}"
352
353
  )
353
354
 
354
355
  async def main():
355
-
356
- node_id = "{node_id}"
357
- async for transmitter in cell.sync(node_id):
356
+
357
+ async for transmitter in node.sync():
358
358
  ts = transmitter.get("time")
359
359
  data = transmitter.get("data")
360
360
  transmitter_id = transmitter.get("transmitter_id")
@@ -373,7 +373,7 @@ async def main():
373
373
  "html": html_content
374
374
  }}
375
375
 
376
- await cell.tx_response(transmitter_id, response_data, client_public_key)
376
+ await node.tx_response(transmitter_id, response_data, client_public_key)
377
377
 
378
378
  asyncio.run(main())
379
379
  """)
@@ -534,7 +534,8 @@ f"""{{
534
534
  "app_metadata": {{
535
535
  "name": "{descr}",
536
536
  "version": "1.0.0",
537
- "author": "{host}"
537
+ "author": "{host}",
538
+ "audience": "private"
538
539
  }},
539
540
  "data_gateways": [
540
541
  {{
@@ -815,107 +816,72 @@ async def async_stop_node():
815
816
  @click.command()
816
817
  def update_node():
817
818
  click.echo("Update your Node")
818
- credentials_folder_path = Path.home() / ".neuronum"
819
- env_path = credentials_folder_path / ".env"
820
- env_data = {}
821
-
822
819
  try:
820
+ env_path = Path.home() / ".neuronum" / ".env"
821
+ env_data = {}
823
822
  with open(env_path, "r") as f:
824
823
  for line in f:
825
- key, value = line.strip().split("=")
826
- env_data[key] = value
824
+ if "=" in line:
825
+ key, value = line.strip().split("=", 1)
826
+ env_data[key] = value
827
827
 
828
828
  host = env_data.get("HOST", "")
829
829
 
830
- except FileNotFoundError:
831
- click.echo("Error: .env with credentials not found")
830
+ with open("config.json", "r") as f:
831
+ config_data = json.load(f)
832
+
833
+ audience = config_data.get("app_metadata", {}).get("audience", "")
834
+ descr = config_data.get("app_metadata", {}).get("name", "")
835
+
836
+ if host.startswith("CMTY_") and audience != "private":
837
+ raise click.ClickException(
838
+ 'Community Cells can only create private Nodes. Set audience to "private".'
839
+ )
840
+ if descr and len(descr) > 25:
841
+ raise click.ClickException(
842
+ 'Description too long. Max 25 characters allowed.'
843
+ )
844
+
845
+ except FileNotFoundError as e:
846
+ click.echo(f"Error: File not found - {e.filename}")
832
847
  return
833
- except Exception as e:
834
- click.echo(f"Error reading .env file: {e}")
848
+ except click.ClickException as e:
849
+ click.echo(e.format_message())
835
850
  return
836
-
837
- if host.startswith("CMTY_"):
838
- node_type = questionary.select(
839
- "Community Cells can only create private Nodes",
840
- choices=["private"]
841
- ).ask()
842
- else:
843
- node_type = questionary.select(
844
- "Who can view your Node?:",
845
- choices=["public", "private", "partners"]
846
- ).ask()
847
- partners = "None"
848
- if node_type == "partners":
849
- prompt_msg = (
850
- "Enter the list of partners who can view this Node.\n"
851
- "Format: partner::cell, partner::cell, partner::cell\n"
852
- "Press Enter to leave the list unchanged"
853
- )
854
- partners = click.prompt(
855
- prompt_msg,
856
- default="None",
857
- show_default=False
858
- ).strip()
859
- descr = click.prompt(
860
- "Update Node description: Type up to 25 characters, or press Enter to leave it unchanged",
861
- default="None",
862
- show_default=False
863
- ).strip()
864
- if descr and len(descr) > 25:
865
- click.echo("Description too long. Max 25 characters allowed.")
851
+ except Exception as e:
852
+ click.echo(f"Error reading files: {e}")
866
853
  return
867
- asyncio.run(async_update_node(node_type, descr, partners))
868
854
 
869
- async def async_update_node(node_type: str, descr: str, partners:str) -> None:
870
- credentials_folder_path = Path.home() / ".neuronum"
871
- env_path = credentials_folder_path / ".env"
872
- env_data = {}
855
+ asyncio.run(async_update_node(env_data, config_data, audience, descr))
873
856
 
874
- try:
875
- with open(env_path, "r") as f:
876
- for line in f:
877
- key, value = line.strip().split("=")
878
- env_data[key] = value
879
857
 
858
+ async def async_update_node(env_data, config_data, audience: str, descr: str):
859
+ try:
880
860
  host = env_data.get("HOST", "")
881
861
  password = env_data.get("PASSWORD", "")
882
862
  network = env_data.get("NETWORK", "")
883
863
  synapse = env_data.get("SYNAPSE", "")
884
864
 
885
- with open('config.json', 'r') as f:
886
- data = json.load(f)
887
-
888
- nodeID = data['data_gateways'][0]['node_id']
889
-
890
- except FileNotFoundError:
891
- click.echo("Error: .env with credentials not found")
892
- return
893
- except Exception as e:
894
- click.echo(f"Error reading .env file: {e}")
895
- return
865
+ node_id = config_data.get("data_gateways", [{}])[0].get("node_id", "")
896
866
 
897
- try:
898
867
  with open("config.json", "r") as f:
899
- config_file = f.read()
868
+ config_file_content = f.read()
900
869
 
901
870
  except FileNotFoundError:
902
- click.echo("Error: Config File not found")
871
+ click.echo("Error: config.json or .env not found")
903
872
  return
904
873
  except Exception as e:
905
- click.echo(f"Error reading Config file: {e}")
874
+ click.echo(f"Error reading files: {e}")
906
875
  return
907
-
908
- if node_type == "partners":
909
- node_type = partners
910
876
 
911
877
  url = f"https://{network}/api/update_node"
912
878
  node = {
913
- "nodeID": nodeID,
879
+ "nodeID": node_id,
914
880
  "host": host,
915
881
  "password": password,
916
882
  "synapse": synapse,
917
- "node_type": node_type,
918
- "config_file": config_file,
883
+ "node_type": audience,
884
+ "config_file": config_file_content,
919
885
  "descr": descr,
920
886
  }
921
887
 
@@ -924,120 +890,73 @@ async def async_update_node(node_type: str, descr: str, partners:str) -> None:
924
890
  async with session.post(url, json=node) as response:
925
891
  response.raise_for_status()
926
892
  data = await response.json()
927
- nodeID = data["nodeID"]
893
+ updated_node_id = data.get("nodeID", node_id)
894
+ click.echo(f"Neuronum Node '{updated_node_id}' updated!")
928
895
  except aiohttp.ClientError as e:
929
896
  click.echo(f"Error sending request: {e}")
930
- return
931
897
 
932
- if node_type == "public":
933
- click.echo(f"Neuronum Node '{nodeID}' updated!")
934
- else:
935
- click.echo(f"Neuronum Node '{nodeID}' updated!")
936
898
 
937
899
 
938
900
  def update_node_at_start():
939
901
  click.echo("Update your Node")
940
- credentials_folder_path = Path.home() / ".neuronum"
941
- env_path = credentials_folder_path / ".env"
942
- env_data = {}
943
-
944
902
  try:
903
+ env_path = Path.home() / ".neuronum" / ".env"
904
+ env_data = {}
945
905
  with open(env_path, "r") as f:
946
906
  for line in f:
947
- key, value = line.strip().split("=")
948
- env_data[key] = value
949
-
950
- host = env_data.get("HOST", "")
951
-
952
- except FileNotFoundError:
953
- click.echo("Error: .env with credentials not found")
954
- return
955
- except Exception as e:
956
- click.echo(f"Error reading .env file: {e}")
957
- return
958
-
959
- if host.startswith("CMTY_"):
960
- node_type = questionary.select(
961
- "Community Cells can only create private Nodes",
962
- choices=["private"]
963
- ).ask()
964
- else:
965
- node_type = questionary.select(
966
- "Who can view your Node?:",
967
- choices=["public", "private", "partners"]
968
- ).ask()
969
- partners = "None"
970
- if node_type == "partners":
971
- prompt_msg = (
972
- "Enter the list of partners who can view this Node.\n"
973
- "Format: partner::cell, partner::cell, partner::cell\n"
974
- "Press Enter to leave the list unchanged"
975
- )
976
- partners = click.prompt(
977
- prompt_msg,
978
- default="None",
979
- show_default=False
980
- ).strip()
981
- descr = click.prompt(
982
- "Update Node description: Type up to 25 characters, or press Enter to leave it unchanged",
983
- default="None",
984
- show_default=False
985
- ).strip()
986
- if descr and len(descr) > 25:
987
- click.echo("Description too long. Max 25 characters allowed.")
988
- return
989
- asyncio.run(async_update_node_at_start(node_type, descr, partners))
990
-
991
- async def async_update_node_at_start(node_type: str, descr: str, partners:str) -> None:
992
- credentials_folder_path = Path.home() / ".neuronum"
993
- env_path = credentials_folder_path / ".env"
994
- env_data = {}
907
+ if "=" in line:
908
+ key, value = line.strip().split("=", 1)
909
+ env_data[key] = value
995
910
 
996
- try:
997
- with open(env_path, "r") as f:
998
- for line in f:
999
- key, value = line.strip().split("=")
1000
- env_data[key] = value
911
+ with open("config.json", "r") as f:
912
+ config_data = json.load(f)
1001
913
 
1002
914
  host = env_data.get("HOST", "")
1003
- password = env_data.get("PASSWORD", "")
1004
- network = env_data.get("NETWORK", "")
1005
- synapse = env_data.get("SYNAPSE", "")
915
+ audience = config_data.get("app_metadata", {}).get("audience", "")
916
+ descr = config_data.get("app_metadata", {}).get("name", "")
917
+
918
+ if host.startswith("CMTY_") and audience != "private":
919
+ raise click.ClickException(
920
+ 'Community Cells can only start private Nodes. Node starting "privately".'
921
+ )
922
+ if descr and len(descr) > 25:
923
+ raise click.ClickException(
924
+ 'Description too long. Max 25 characters allowed.'
925
+ )
926
+
927
+ asyncio.run(_async_update_node_at_start(env_data, config_data, audience, descr))
928
+
929
+ except FileNotFoundError as e:
930
+ click.echo(f"Error: File not found - {e.filename}")
931
+ except click.ClickException as e:
932
+ click.echo(e.format_message())
933
+ except Exception as e:
934
+ click.echo(f"Unexpected error: {e}")
1006
935
 
1007
- with open('config.json', 'r') as f:
1008
- data = json.load(f)
1009
936
 
1010
- nodeID = data['data_gateways'][0]['node_id']
937
+ async def _async_update_node_at_start(env_data, config_data, audience, descr):
938
+ host = env_data.get("HOST", "")
939
+ password = env_data.get("PASSWORD", "")
940
+ network = env_data.get("NETWORK", "")
941
+ synapse = env_data.get("SYNAPSE", "")
1011
942
 
1012
- except FileNotFoundError:
1013
- click.echo("Error: .env with credentials not found")
1014
- return
1015
- except Exception as e:
1016
- click.echo(f"Error reading .env file: {e}")
1017
- return
943
+ node_id = config_data.get("data_gateways", [{}])[0].get("node_id", "")
1018
944
 
1019
945
  try:
1020
946
  with open("config.json", "r") as f:
1021
- config_file = f.read()
1022
-
1023
- except FileNotFoundError:
1024
- click.echo("Error: File not found")
1025
- return
947
+ config_file_content = f.read()
1026
948
  except Exception as e:
1027
- click.echo(f"Error reading file: {e}")
949
+ click.echo(f"Error reading config.json content: {e}")
1028
950
  return
1029
-
1030
- if node_type == "partners":
1031
- node_type = partners
1032
951
 
1033
952
  url = f"https://{network}/api/update_node"
1034
953
  node = {
1035
- "nodeID": nodeID,
954
+ "nodeID": node_id,
1036
955
  "host": host,
1037
956
  "password": password,
1038
957
  "synapse": synapse,
1039
- "node_type": node_type,
1040
- "config_file": config_file,
958
+ "node_type": audience,
959
+ "config_file": config_file_content,
1041
960
  "descr": descr,
1042
961
  }
1043
962
 
@@ -1046,15 +965,10 @@ async def async_update_node_at_start(node_type: str, descr: str, partners:str) -
1046
965
  async with session.post(url, json=node) as response:
1047
966
  response.raise_for_status()
1048
967
  data = await response.json()
1049
- nodeID = data["nodeID"]
968
+ updated_node_id = data.get("nodeID", node_id)
969
+ click.echo(f"Neuronum Node '{updated_node_id}' updated!")
1050
970
  except aiohttp.ClientError as e:
1051
971
  click.echo(f"Error sending request: {e}")
1052
- return
1053
-
1054
- if node_type == "public":
1055
- click.echo(f"Neuronum Node '{nodeID}' updated!")
1056
- else:
1057
- click.echo(f"Neuronum Node '{nodeID}' updated!")
1058
972
 
1059
973
 
1060
974
  @click.command()
neuronum/__init__.py CHANGED
@@ -1,2 +1 @@
1
- from .neuronum import Cell
2
- __all__ = ['Cell']
1
+ from .neuronum import Node
neuronum/neuronum.py CHANGED
@@ -14,10 +14,11 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
14
14
  from cryptography.hazmat.backends import default_backend
15
15
 
16
16
 
17
- class Cell:
18
- def __init__(self, private_key_path: str, public_key_path: str):
19
- self.private_key_path = private_key_path
20
- self.public_key_path = public_key_path
17
+ class Node:
18
+ def __init__(self, id: str, private_key: str, public_key: str):
19
+ self.node_id = id
20
+ self.private_key_path = private_key
21
+ self.public_key_path = public_key
21
22
  self.queue = asyncio.Queue()
22
23
  self.host = self._load_host()
23
24
  self.network = self._load_network()
@@ -34,6 +35,7 @@ class Cell:
34
35
  "synapse": self.synapse
35
36
  }
36
37
 
38
+
37
39
  def _load_private_key(self):
38
40
  try:
39
41
  with open(self.private_key_path, "rb") as f:
@@ -42,12 +44,12 @@ class Cell:
42
44
  password=None,
43
45
  backend=default_backend()
44
46
  )
45
- print("Private key loaded successfully.")
46
47
  return private_key
47
48
  except FileNotFoundError:
48
49
  print(f"Private key file not found at {self.private_key_path}.")
49
50
  return None
50
51
 
52
+
51
53
  def _load_host(self):
52
54
  credentials_folder_path = Path.home() / ".neuronum"
53
55
  env_path = credentials_folder_path / ".env"
@@ -129,8 +131,6 @@ class Cell:
129
131
  f.read(),
130
132
  backend=default_backend()
131
133
  )
132
- print("Public key loaded successfully.")
133
- print(public_key)
134
134
  return public_key
135
135
  except FileNotFoundError:
136
136
  print(f"Public key file not found at {self.public_key_path}. Deriving from private key.")
@@ -146,7 +146,6 @@ class Cell:
146
146
  print("Public key not loaded. Cannot generate JWK.")
147
147
  return None
148
148
 
149
- print("Public key loaded successfully.")
150
149
  public_numbers = public_key.public_numbers()
151
150
 
152
151
  x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
@@ -184,8 +183,8 @@ class Cell:
184
183
  return None
185
184
 
186
185
 
187
- async def sync(self, node_id: str) -> AsyncGenerator[str, None]:
188
- full_url = f"wss://{self.network}/sync/{node_id}"
186
+ async def sync(self) -> AsyncGenerator[str, None]:
187
+ full_url = f"wss://{self.network}/sync/{self.node_id}"
189
188
  auth_payload = {
190
189
  "host": self.host,
191
190
  "password": self.password,
@@ -195,7 +194,7 @@ class Cell:
195
194
  try:
196
195
  async with websockets.connect(full_url) as ws:
197
196
  await ws.send(json.dumps(auth_payload))
198
- print("Connected to WebSocket.")
197
+ print("Node syncing...")
199
198
  while True:
200
199
  try:
201
200
  raw_operation = await ws.recv()
@@ -261,108 +260,7 @@ class Cell:
261
260
  print(f"Error sending request: {e}")
262
261
  except Exception as e:
263
262
  print(f"Unexpected error: {e}")
264
-
265
-
266
- async def activate_tx(self, node_id: str, data: dict):
267
- url = f"https://{self.network}/api/activate_tx/{node_id}"
268
-
269
- nodes = await self.list_nodes()
270
-
271
- target_public_key_pem = None
272
- target_node = node_id
273
-
274
- for node in nodes:
275
- node_ids = node.get('config', {}).get('data_gateways', [])
276
- for node_id_entry in node_ids:
277
- if node_id_entry.get('node_id') == target_node:
278
- target_public_key_pem = node.get('config', {}).get('public_key')
279
- break
280
- if target_public_key_pem:
281
- break
282
-
283
- if not target_public_key_pem:
284
- print(f"Target node not found or public key is missing.")
285
- return None
286
-
287
- try:
288
- print(target_public_key_pem)
289
- partner_public_key_jwk = self._load_public_key_from_pem(target_public_key_pem)
290
- if not partner_public_key_jwk:
291
- print("Failed to convert public key to JWK format. Aborting.")
292
- return None
293
-
294
- partner_public_key = self._load_public_key_from_jwk(partner_public_key_jwk)
295
- if not partner_public_key:
296
- print("Failed to load partner's public key. Aborting.")
297
- return None
298
-
299
- data_to_encrypt = data.copy()
300
- data_to_encrypt["publicKey"] = self.get_public_key_jwk()
301
-
302
- encrypted_payload = self._encrypt_with_ecdh_aesgcm(partner_public_key, data_to_encrypt)
303
-
304
- TX = {
305
- "data": {
306
- "encrypted": encrypted_payload
307
- },
308
- "cell": self.to_dict()
309
- }
310
-
311
- async with aiohttp.ClientSession() as session:
312
- async with session.post(url, json=TX) as response:
313
- if response.status == 504:
314
- print(f"Gateway Timeout: No response from transmitter for txID: {node_id}")
315
- return None
316
-
317
- response.raise_for_status()
318
-
319
- response_data = await response.json()
320
-
321
- if "response" in response_data:
322
- inner_response = response_data["response"]
323
-
324
- if "ciphertext" in inner_response:
325
- ephemeral_public_key_b64 = inner_response["ephemeralPublicKey"]
326
- ephemeral_public_key_b64 += '=' * ((4 - len(ephemeral_public_key_b64) % 4) % 4)
327
- ephemeral_public_key_bytes = base64.urlsafe_b64decode(ephemeral_public_key_b64)
328
-
329
- nonce_b64 = inner_response["nonce"]
330
- nonce_b64 += '=' * ((4 - len(nonce_b64) % 4) % 4)
331
- nonce = base64.urlsafe_b64decode(nonce_b64)
332
-
333
- ciphertext_b64 = inner_response["ciphertext"]
334
- ciphertext_b64 += '=' * ((4 - len(ciphertext_b64) % 4) % 4)
335
- ciphertext = base64.urlsafe_b64decode(ciphertext_b64)
336
-
337
- decrypted_response = self._decrypt_with_ecdh_aesgcm(
338
- ephemeral_public_key_bytes, nonce, ciphertext
339
- )
340
-
341
- if decrypted_response:
342
- return decrypted_response
343
- else:
344
- print("Failed to decrypt server response.")
345
- return None
346
- else:
347
- print("Server response was not encrypted as expected.")
348
- return inner_response
349
-
350
- else:
351
- print("Unexpected response format.")
352
- return response_data
353
-
354
- except aiohttp.ClientResponseError as e:
355
- print(f"HTTP Error: {e.status}, Message: {e.message}, URL: {e.request_info.url}")
356
- return None
357
-
358
- except aiohttp.ClientError as e:
359
- print(f"Connection Error: {e}")
360
- return None
361
-
362
- except Exception as e:
363
- print(f"Unexpected error: {e}")
364
- return None
365
-
263
+
366
264
 
367
265
  def _load_public_key_from_jwk(self, jwk):
368
266
  try:
@@ -378,31 +276,6 @@ class Cell:
378
276
  except (KeyError, ValueError, TypeError) as e:
379
277
  print(f"Error loading public key from JWK string: {e}")
380
278
  return None
381
-
382
-
383
- def _load_public_key_from_pem(self, pem_string: str):
384
- try:
385
- print(pem_string)
386
- corrected_pem_string = pem_string.replace("-----BEGINPUBLICKEY-----", "-----BEGIN PUBLIC KEY-----")
387
- corrected_pem_string = corrected_pem_string.replace("-----ENDPUBLICKEY-----", "-----END PUBLIC KEY-----")
388
-
389
- public_key = serialization.load_pem_public_key(
390
- corrected_pem_string.encode(),
391
- backend=default_backend()
392
- )
393
-
394
- public_numbers = public_key.public_numbers()
395
- x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
396
- y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
397
- return {
398
- "kty": "EC",
399
- "crv": "P-256",
400
- "x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
401
- "y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
402
- }
403
- except Exception as e:
404
- print(f"Error loading public key from PEM string: {e}")
405
- return None
406
279
 
407
280
 
408
281
  def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
@@ -462,6 +335,4 @@ class Cell:
462
335
  except aiohttp.ClientError as e:
463
336
  print(f"Error sending request: {e}")
464
337
  except Exception as e:
465
- print(f"Unexpected error: {e}")
466
-
467
- __all__ = ['Cell']
338
+ print(f"Unexpected error: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuronum
3
- Version: 8.0.0
3
+ Version: 8.2.0
4
4
  Summary: The E2E Web Engine
5
5
  Home-page: https://neuronum.net
6
6
  Author: Neuronum Cybernetics
@@ -71,7 +71,7 @@ Neuronum is a real-time web engine designed for developers to build E2E-native a
71
71
  ### **Tools & Features**
72
72
  - Cell: Account to interact with Neuronum
73
73
  - Nodes: Apps & Services built on Neuronum
74
- - Browser: Open Source Web Engine -> [build from source](https://github.com/neuronumcybernetics/neuronum_browser)
74
+ - Browser: Web Browser built on Neuronum -> [build from source](https://github.com/neuronumcybernetics/neuronum_browser)
75
75
 
76
76
  ### Requirements
77
77
  - Python >= 3.8
@@ -100,8 +100,6 @@ neuronum connect-cell
100
100
 
101
101
 
102
102
  ### **Build On Neuronum**
103
- Visit & build with [Node Examples](https://github.com/neuronumcybernetics/neuronum/tree/main/features/nodes/examples) to gain deeper knowledge on how to build on Neuronum.
104
-
105
103
  To get started, initialize a new Node with the command below.
106
104
  ```sh
107
105
  neuronum init-node
@@ -123,4 +121,4 @@ neuronum start-node
123
121
 
124
122
  ### **Interact with your Node**
125
123
 
126
- The **Neuronum Browser** is an open source web engine that allows you to interact with your nodes -> [build from source](https://github.com/neuronumcybernetics/neuronum_browser)
124
+ The **Neuronum Browser** is an open source E2E web browser that allows you to interact with your nodes -> [build from source](https://github.com/neuronumcybernetics/neuronum_browser)
@@ -0,0 +1,10 @@
1
+ cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli/main.py,sha256=lNzUeW_cJKS9sTbaWO1wXZvLcvZ_PbUC3vYYTNGDEfs,31747
3
+ neuronum/__init__.py,sha256=qkAz6fpiS2KKnaKwPbS15y7lRCQV-XaWNTkVUarluPk,26
4
+ neuronum/neuronum.py,sha256=4iZjeQ1XTKPhUpojmpkYQiOaNZPZE6t7JUyhLPMOyuA,13183
5
+ neuronum-8.2.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
6
+ neuronum-8.2.0.dist-info/METADATA,sha256=WK0q97Q2HOJstFkcaPiotF2LeczYr1afQ__zGYqDkaw,3448
7
+ neuronum-8.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ neuronum-8.2.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
+ neuronum-8.2.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
+ neuronum-8.2.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cli/main.py,sha256=77Gmm2w4TB33i7PnqHYmb2BeunLLoVF-hqGLTPIYzQU,33969
3
- neuronum/__init__.py,sha256=tAdqNC9rByY_CwG63yAyEe34phBdQ_Vv3FyFpTXQ2wo,45
4
- neuronum/neuronum.py,sha256=JQetKNsdf-p2IrFyCPCI6EyPIsbQi_FUnXkNXr-H5Zc,18898
5
- neuronum-8.0.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
6
- neuronum-8.0.0.dist-info/METADATA,sha256=yaaAHaOqZCaC249KYx998KuFjvLHVaP50HjQ9kse4M0,3609
7
- neuronum-8.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- neuronum-8.0.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
- neuronum-8.0.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
- neuronum-8.0.0.dist-info/RECORD,,