neuronum 8.3.0__py3-none-any.whl → 9.0.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
@@ -359,12 +359,12 @@ async def main():
359
359
  data = transmitter.get("data")
360
360
  transmitter_id = transmitter.get("transmitter_id")
361
361
  client = transmitter.get("operator")
362
- client_public_key = data.get("publicKey")
362
+ client_public_key = data.get("publicKey", {{}})
363
363
  action = data.get("action")
364
364
 
365
365
  response_data = {{}}
366
366
 
367
- if action == "ping_node":
367
+ if action == "ping_node" or action == "start_app":
368
368
 
369
369
  html_content = template.render(client=client, ts=ts, data=action, transmitter_id=transmitter_id)
370
370
 
@@ -373,7 +373,10 @@ async def main():
373
373
  "html": html_content
374
374
  }}
375
375
 
376
- await node.tx_response(transmitter_id, response_data, client_public_key)
376
+ if not client_public_key:
377
+ await node.tx_response(transmitter_id, response_data, encrypted=False)
378
+ else:
379
+ await node.tx_response(transmitter_id, response_data, client_public_key)
377
380
 
378
381
  asyncio.run(main())
379
382
  """)
@@ -516,7 +519,6 @@ asyncio.run(main())
516
519
  data: {{ "action": "ping_node" }},
517
520
  nodePublicKey: '{pem_public_oneline}',
518
521
  }};
519
-
520
522
  if (window.parent) {{
521
523
  window.parent.postMessage(messagePayload, '*');
522
524
  }}
@@ -536,19 +538,9 @@ f"""{{
536
538
  "version": "1.0.0",
537
539
  "author": "{host}",
538
540
  "audience": "private",
539
- "logo": "https://neuronum.net/static/logo.png"
541
+ "logo": "https://neuronum.net/static/logo.png",
542
+ "node_id": "{node_id}"
540
543
  }},
541
- "data_gateways": [
542
- {{
543
- "node_id": "{node_id}",
544
- "actions": [
545
- {{
546
- "action": "ping_node",
547
- "info": "Ping Node"
548
- }}
549
- ]
550
- }}
551
- ],
552
544
  "legals": {{
553
545
  "terms": "https://url_to_your/terms",
554
546
  "privacy_policy": "https://url_to_your/privacy_policy"
@@ -635,7 +627,7 @@ def check_node():
635
627
  with open('config.json', 'r') as f:
636
628
  data = json.load(f)
637
629
 
638
- nodeID = data['data_gateways'][0]['node_id']
630
+ nodeID = data['app_metadata']['node_id']
639
631
 
640
632
  except FileNotFoundError:
641
633
  click.echo("Error: .env with credentials not found")
@@ -699,7 +691,7 @@ def restart_node(d):
699
691
  with open('config.json', 'r') as f:
700
692
  data = json.load(f)
701
693
 
702
- nodeID = data['data_gateways'][0]['node_id']
694
+ nodeID = data['app_metadata']['node_id']
703
695
 
704
696
  except FileNotFoundError:
705
697
  print("Error: .env with credentials not found")
@@ -781,7 +773,7 @@ async def async_stop_node():
781
773
  with open('config.json', 'r') as f:
782
774
  data = json.load(f)
783
775
 
784
- nodeID = data['data_gateways'][0]['node_id']
776
+ nodeID = data['app_metadata']['node_id']
785
777
 
786
778
  except FileNotFoundError:
787
779
  print("Error: .env with credentials not found")
@@ -863,7 +855,7 @@ async def async_update_node(env_data, config_data, audience: str, descr: str):
863
855
  network = env_data.get("NETWORK", "")
864
856
  synapse = env_data.get("SYNAPSE", "")
865
857
 
866
- node_id = config_data.get("data_gateways", [{}])[0].get("node_id", "")
858
+ node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
867
859
 
868
860
  with open("config.json", "r") as f:
869
861
  config_file_content = f.read()
@@ -941,7 +933,7 @@ async def _async_update_node_at_start(env_data, config_data, audience, descr):
941
933
  network = env_data.get("NETWORK", "")
942
934
  synapse = env_data.get("SYNAPSE", "")
943
935
 
944
- node_id = config_data.get("data_gateways", [{}])[0].get("node_id", "")
936
+ node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
945
937
 
946
938
  try:
947
939
  with open("config.json", "r") as f:
@@ -995,7 +987,7 @@ async def async_delete_node():
995
987
  with open('config.json', 'r') as f:
996
988
  data = json.load(f)
997
989
 
998
- nodeID = data['data_gateways'][0]['node_id']
990
+ nodeID = data['app_metadata']['node_id']
999
991
 
1000
992
  except FileNotFoundError:
1001
993
  click.echo("Error: .env with credentials not found")
neuronum/neuronum.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import aiohttp
2
- from typing import AsyncGenerator
2
+ from typing import Optional, AsyncGenerator, Union
3
3
  import websockets
4
4
  import json
5
5
  import asyncio
6
6
  import base64
7
7
  import os
8
+ import ssl
8
9
  from pathlib import Path
9
10
  from websockets.exceptions import ConnectionClosed
10
11
  from cryptography.hazmat.primitives.asymmetric import ec
@@ -13,17 +14,17 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
13
14
  from cryptography.hazmat.primitives.kdf.hkdf import HKDF
14
15
  from cryptography.hazmat.backends import default_backend
15
16
 
16
-
17
17
  class Node:
18
18
  def __init__(self, id: str, private_key: str, public_key: str):
19
19
  self.node_id = id
20
20
  self.private_key_path = private_key
21
21
  self.public_key_path = public_key
22
22
  self.queue = asyncio.Queue()
23
- self.host = self._load_host()
24
- self.network = self._load_network()
25
- self.synapse = self._load_synapse()
26
- self.password = self._load_password()
23
+ self.env = self._load_env()
24
+ self.host = self.env.get("HOST", "")
25
+ self.network = self.env.get("NETWORK", "")
26
+ self.synapse = self.env.get("SYNAPSE", "")
27
+ self.password = self.env.get("PASSWORD", "")
27
28
  self._private_key = self._load_private_key()
28
29
  self._public_key = self._load_public_key()
29
30
 
@@ -34,110 +35,39 @@ class Node:
34
35
  "password": self.password,
35
36
  "synapse": self.synapse
36
37
  }
38
+
37
39
 
38
-
39
- def _load_private_key(self):
40
- try:
41
- with open(self.private_key_path, "rb") as f:
42
- private_key = serialization.load_pem_private_key(
43
- f.read(),
44
- password=None,
45
- backend=default_backend()
46
- )
47
- return private_key
48
- except FileNotFoundError:
49
- print(f"Private key file not found at {self.private_key_path}.")
50
- return None
51
-
52
-
53
- def _load_host(self):
54
- credentials_folder_path = Path.home() / ".neuronum"
55
- env_path = credentials_folder_path / ".env"
56
-
57
- env_data = {}
58
-
59
- try:
60
- with open(env_path, "r") as f:
61
- for line in f:
62
- key, value = line.strip().split("=")
63
- env_data[key] = value
64
-
65
- host = env_data.get("HOST", "")
66
- return host
67
- except FileNotFoundError:
68
- print(f"Cell Host not found")
69
- return None
70
-
71
-
72
- def _load_password(self):
73
- credentials_folder_path = Path.home() / ".neuronum"
74
- env_path = credentials_folder_path / ".env"
75
-
76
- env_data = {}
77
-
78
- try:
79
- with open(env_path, "r") as f:
80
- for line in f:
81
- key, value = line.strip().split("=")
82
- env_data[key] = value
83
-
84
- host = env_data.get("PASSWORD", "")
85
- return host
86
- except FileNotFoundError:
87
- print(f"Cell Password not found")
88
- return None
89
-
90
- def _load_synapse(self):
40
+ def _load_env(self) -> dict:
91
41
  credentials_folder_path = Path.home() / ".neuronum"
92
42
  env_path = credentials_folder_path / ".env"
93
-
94
- env_data = {}
95
-
43
+ env_data = {}
96
44
  try:
97
45
  with open(env_path, "r") as f:
98
46
  for line in f:
99
47
  key, value = line.strip().split("=")
100
48
  env_data[key] = value
101
-
102
- host = env_data.get("SYNAPSE", "")
103
- return host
49
+ return env_data
104
50
  except FileNotFoundError:
105
- print(f"Cell Synapse not found")
106
- return None
107
-
108
- def _load_network(self):
109
- credentials_folder_path = Path.home() / ".neuronum"
110
- env_path = credentials_folder_path / ".env"
51
+ print(f"Cell credentials (.env) not found at {env_path}")
52
+ return {}
111
53
 
112
- env_data = {}
113
54
 
55
+ def _load_private_key(self):
114
56
  try:
115
- with open(env_path, "r") as f:
116
- for line in f:
117
- key, value = line.strip().split("=")
118
- env_data[key] = value
119
-
120
- host = env_data.get("NETWORK", "")
121
- return host
57
+ with open(self.private_key_path, "rb") as f:
58
+ return serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
122
59
  except FileNotFoundError:
123
- print(f"Cell Network not found")
60
+ print(f"Private key file not found at {self.private_key_path}.")
124
61
  return None
125
62
 
126
63
 
127
64
  def _load_public_key(self):
128
65
  try:
129
66
  with open(self.public_key_path, "rb") as f:
130
- public_key = serialization.load_pem_public_key(
131
- f.read(),
132
- backend=default_backend()
133
- )
134
- return public_key
67
+ return serialization.load_pem_public_key(f.read(), backend=default_backend())
135
68
  except FileNotFoundError:
136
- print(f"Public key file not found at {self.public_key_path}. Deriving from private key.")
137
- if self._private_key:
138
- return self._private_key.public_key()
139
- else:
140
- return None
69
+ print(f"Public key file not found. Deriving from private key.")
70
+ return self._private_key.public_key() if self._private_key else None
141
71
 
142
72
 
143
73
  def get_public_key_jwk(self):
@@ -145,44 +75,167 @@ class Node:
145
75
  if not public_key:
146
76
  print("Public key not loaded. Cannot generate JWK.")
147
77
  return None
148
-
149
78
  public_numbers = public_key.public_numbers()
150
-
151
79
  x_bytes = public_numbers.x.to_bytes((public_numbers.x.bit_length() + 7) // 8, 'big')
152
80
  y_bytes = public_numbers.y.to_bytes((public_numbers.y.bit_length() + 7) // 8, 'big')
153
-
154
81
  return {
155
82
  "kty": "EC",
156
83
  "crv": "P-256",
157
84
  "x": base64.urlsafe_b64encode(x_bytes).rstrip(b'=').decode('utf-8'),
158
85
  "y": base64.urlsafe_b64encode(y_bytes).rstrip(b'=').decode('utf-8')
159
86
  }
160
-
161
-
87
+
88
+
162
89
  def _decrypt_with_ecdh_aesgcm(self, ephemeral_public_key_bytes, nonce, ciphertext):
163
90
  try:
164
91
  ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
165
92
  ec.SECP256R1(), ephemeral_public_key_bytes
166
93
  )
167
-
168
94
  shared_secret = self._private_key.exchange(ec.ECDH(), ephemeral_public_key)
169
-
170
- derived_key = HKDF(
171
- algorithm=hashes.SHA256(),
172
- length=32,
173
- salt=None,
174
- info=b'handshake data'
175
- ).derive(shared_secret)
176
-
95
+ derived_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b'handshake data').derive(shared_secret)
177
96
  aesgcm = AESGCM(derived_key)
178
97
  plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
179
98
  return json.loads(plaintext_bytes.decode())
180
-
181
99
  except Exception as e:
182
100
  print(f"Decryption failed: {e}")
183
101
  return None
184
102
 
185
103
 
104
+ def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
105
+ ephemeral_private = ec.generate_private_key(ec.SECP256R1())
106
+ shared_secret = ephemeral_private.exchange(ec.ECDH(), public_key)
107
+ derived_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b'handshake data').derive(shared_secret)
108
+ aesgcm = AESGCM(derived_key)
109
+ nonce = os.urandom(12)
110
+ plaintext_bytes = json.dumps(plaintext_dict).encode()
111
+ ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
112
+ ephemeral_public_bytes = ephemeral_private.public_key().public_bytes(
113
+ serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
114
+ )
115
+ return {
116
+ 'ciphertext': base64.urlsafe_b64encode(ciphertext).rstrip(b'=').decode(),
117
+ 'nonce': base64.urlsafe_b64encode(nonce).rstrip(b'=').decode(),
118
+ 'ephemeralPublicKey': base64.urlsafe_b64encode(ephemeral_public_bytes).rstrip(b'=').decode()
119
+ }
120
+
121
+
122
+ def _load_public_key_from_jwk(self, jwk):
123
+ try:
124
+ x_bytes = base64.urlsafe_b64decode(jwk['x'] + '==')
125
+ y_bytes = base64.urlsafe_b64decode(jwk['y'] + '==')
126
+ public_numbers = ec.EllipticCurvePublicNumbers(
127
+ int.from_bytes(x_bytes, 'big'),
128
+ int.from_bytes(y_bytes, 'big'),
129
+ ec.SECP256R1()
130
+ )
131
+ return public_numbers.public_key(default_backend())
132
+ except Exception as e:
133
+ print(f"Error loading public key from JWK: {e}")
134
+ return None
135
+
136
+
137
+ def _load_public_key_from_pem(self, pem_string: str):
138
+ try:
139
+ corrected_pem = pem_string.replace("-----BEGINPUBLICKEY-----", "-----BEGIN PUBLIC KEY-----") \
140
+ .replace("-----ENDPUBLICKEY-----", "-----END PUBLIC KEY-----")
141
+ public_key = serialization.load_pem_public_key(corrected_pem.encode(), backend=default_backend())
142
+ return public_key
143
+ except Exception as e:
144
+ print(f"Error loading public key from PEM: {e}")
145
+ return None
146
+
147
+
148
+ async def _get_target_node_public_key(self, node_id: str):
149
+ nodes = await self.list_nodes()
150
+ for node in nodes:
151
+ app_metadata = node.get('config', {}).get('app_metadata', {})
152
+ if app_metadata.get('node_id') == node_id:
153
+ pem = node.get('config', {}).get('public_key')
154
+ if not pem:
155
+ print(f"Public key missing for node: {node_id}")
156
+ return None
157
+ public_key = self._load_public_key_from_pem(pem)
158
+ if not public_key:
159
+ return None
160
+ return public_key
161
+ print(f"Target node not found: {node_id}")
162
+ return None
163
+
164
+
165
+ async def _post_request(self, url, payload):
166
+ async with aiohttp.ClientSession() as session:
167
+ try:
168
+ async with session.post(url, json=payload) as response:
169
+ response.raise_for_status()
170
+ return await response.json()
171
+ except aiohttp.ClientError as e:
172
+ print(f"HTTP Error: {e.status}, URL: {url}")
173
+ except Exception as e:
174
+ print(f"Unexpected error: {e}")
175
+ return None
176
+
177
+
178
+ async def list_nodes(self):
179
+ full_url = f"https://{self.network}/api/list_nodes"
180
+ payload = {"cell": self.to_dict()}
181
+ data = await self._post_request(full_url, payload)
182
+ return data.get("Nodes", []) if data else []
183
+
184
+
185
+ async def tx_response(self, transmitter_id: str, data: dict, client_public_key_str: Optional[Union[str, dict]] = None, encrypted: Optional[bool] = True):
186
+ url = f"https://{self.network}/api/tx_response/{transmitter_id}"
187
+
188
+ if encrypted:
189
+ if not client_public_key_str:
190
+ print("Error: client_public_key_str is required for encrypted responses.")
191
+ return
192
+
193
+ public_key_jwk = json.loads(client_public_key_str) if isinstance(client_public_key_str, str) else client_public_key_str
194
+ public_key = self._load_public_key_from_jwk(public_key_jwk)
195
+ if not public_key:
196
+ return
197
+
198
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data)
199
+ payload = {"data": encrypted_payload, "cell": self.to_dict()}
200
+ else:
201
+ payload = {"data": data, "cell": self.to_dict()}
202
+
203
+ await self._post_request(url, payload)
204
+
205
+
206
+ async def activate_tx(self, node_id: str, data: dict, encrypted: Optional[bool] = True):
207
+ url = f"https://{self.network}/api/activate_tx/{node_id}"
208
+ payload = {"cell": self.to_dict()}
209
+
210
+ if encrypted:
211
+ public_key = await self._get_target_node_public_key(node_id)
212
+ if not public_key: return None
213
+ data_to_encrypt = data.copy()
214
+ data_to_encrypt["publicKey"] = self.get_public_key_jwk()
215
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data_to_encrypt)
216
+ payload["data"] = {"encrypted": encrypted_payload}
217
+ else:
218
+ payload["data"] = data
219
+
220
+ response_data = await self._post_request(url, payload)
221
+
222
+ if encrypted:
223
+ if not response_data or "response" not in response_data:
224
+ print("Unexpected or missing response.")
225
+ return response_data
226
+ inner_response = response_data["response"]
227
+ if "ciphertext" in inner_response:
228
+ ephemeral_public_key_bytes = base64.urlsafe_b64decode(inner_response["ephemeralPublicKey"] + '==')
229
+ nonce = base64.urlsafe_b64decode(inner_response["nonce"] + '==')
230
+ ciphertext = base64.urlsafe_b64decode(inner_response["ciphertext"] + '==')
231
+ return self._decrypt_with_ecdh_aesgcm(ephemeral_public_key_bytes, nonce, ciphertext)
232
+ else:
233
+ print("Server response was not encrypted as expected.")
234
+ return inner_response
235
+ else:
236
+ return response_data
237
+
238
+
186
239
  async def sync(self) -> AsyncGenerator[str, None]:
187
240
  full_url = f"wss://{self.network}/sync/{self.node_id}"
188
241
  auth_payload = {
@@ -245,94 +298,67 @@ class Node:
245
298
  await asyncio.sleep(3)
246
299
 
247
300
 
248
- async def list_nodes(self):
249
- full_url = f"https://{self.network}/api/list_nodes"
250
- list_nodes_payload = {
251
- "cell": self.to_dict()
252
- }
253
- async with aiohttp.ClientSession() as session:
254
- try:
255
- async with session.post(full_url, json=list_nodes_payload) as response:
256
- response.raise_for_status()
257
- data = await response.json()
258
- return data.get("Nodes", [])
259
- except aiohttp.ClientError as e:
260
- print(f"Error sending request: {e}")
261
- except Exception as e:
262
- print(f"Unexpected error: {e}")
263
-
264
-
265
- def _load_public_key_from_jwk(self, jwk):
266
- try:
267
- print(jwk)
268
- x = base64.urlsafe_b64decode(jwk['x'] + '==')
269
- y = base64.urlsafe_b64decode(jwk['y'] + '==')
270
- public_numbers = ec.EllipticCurvePublicNumbers(
271
- int.from_bytes(x, 'big'),
272
- int.from_bytes(y, 'big'),
273
- ec.SECP256R1()
274
- )
275
- return public_numbers.public_key(default_backend())
276
- except (KeyError, ValueError, TypeError) as e:
277
- print(f"Error loading public key from JWK string: {e}")
278
- return None
279
-
280
-
281
- def _encrypt_with_ecdh_aesgcm(self, public_key, plaintext_dict):
282
- ephemeral_private = ec.generate_private_key(ec.SECP256R1())
283
- ephemeral_public = ephemeral_private.public_key()
284
- shared_secret = ephemeral_private.exchange(ec.ECDH(), public_key)
285
- derived_key = HKDF(
286
- algorithm=hashes.SHA256(),
287
- length=32,
288
- salt=None,
289
- info=b'handshake data'
290
- ).derive(shared_secret)
291
- aesgcm = AESGCM(derived_key)
292
- nonce = os.urandom(12)
293
- plaintext_bytes = json.dumps(plaintext_dict).encode()
294
- ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, None)
295
- ephemeral_public_bytes = ephemeral_public.public_bytes(
296
- serialization.Encoding.X962,
297
- serialization.PublicFormat.UncompressedPoint
298
- )
299
- return {
300
- 'ciphertext': base64.b64encode(ciphertext).decode(),
301
- 'nonce': base64.b64encode(nonce).decode(),
302
- 'ephemeralPublicKey': base64.b64encode(ephemeral_public_bytes).decode()
303
- }
304
-
305
-
306
- async def tx_response(self, transmitter_id: str, data: dict, client_public_key_str):
307
- if isinstance(client_public_key_str, str):
308
- try:
309
- client_public_key_jwk = json.loads(client_public_key_str)
310
- except json.JSONDecodeError:
311
- print("Failed to decode client public key from string. Aborting response.")
301
+ async def stream(self, label: str, data: dict, node_id: str = None, encrypted: Optional[bool] = True, retry_delay: int = 3):
302
+ context = ssl.create_default_context()
303
+ context.check_hostname = True
304
+ context.verify_mode = ssl.CERT_REQUIRED
305
+
306
+ target_node_public_key = None
307
+ if encrypted:
308
+ target_node_public_key = await self._get_target_node_public_key(node_id)
309
+ if not target_node_public_key:
310
+ print("Failed to get target node's public key. Cannot stream data.")
312
311
  return
313
- elif isinstance(client_public_key_str, dict):
314
- client_public_key_jwk = client_public_key_str
315
- else:
316
- print("Invalid type for client public key. Expected str or dict. Aborting response.")
317
- return
318
- public_key = self._load_public_key_from_jwk(client_public_key_jwk)
319
- if not public_key:
320
- print("Failed to load public key. Aborting response.")
321
- return
322
- encrypted_payload = self._encrypt_with_ecdh_aesgcm(public_key, data)
323
- url = f"https://{self.network}/api/tx_response/{transmitter_id}"
324
- tx_response = {
325
- "data": encrypted_payload,
326
- "cell": self.to_dict()
327
- }
328
- async with aiohttp.ClientSession() as session:
312
+
313
+ while True:
329
314
  try:
330
- for _ in range(2):
331
- async with session.post(url, json=tx_response) as response:
332
- response.raise_for_status()
333
- data = await response.json()
334
- print(data["message"])
335
- except aiohttp.ClientError as e:
336
- print(f"Error sending request: {e}")
315
+ reader, writer = await asyncio.open_connection(self.network, 55555, ssl=context, server_hostname=self.network)
316
+
317
+ credentials = f"{self.host}\n{self.password}\n{self.synapse}\n{node_id}\n"
318
+ writer.write(credentials.encode("utf-8"))
319
+ await writer.drain()
320
+
321
+ response = await reader.read(1024)
322
+ response_text = response.decode("utf-8").strip()
323
+
324
+ if "Authentication successful" not in response_text:
325
+ print("Authentication failed, retrying...")
326
+ writer.close()
327
+ await writer.wait_closed()
328
+ await asyncio.sleep(retry_delay)
329
+ continue
330
+
331
+ stream_payload = {
332
+ "label": label,
333
+ "data": data.copy()
334
+ }
335
+
336
+ if encrypted:
337
+ data_to_encrypt = data.copy()
338
+ data_to_encrypt["publicKey"] = self.get_public_key_jwk()
339
+
340
+ encrypted_payload = self._encrypt_with_ecdh_aesgcm(target_node_public_key, data_to_encrypt)
341
+ stream_payload["data"] = {"encrypted": encrypted_payload}
342
+
343
+ writer.write(json.dumps(stream_payload).encode("utf-8"))
344
+ await writer.drain()
345
+
346
+ response = await reader.read(1024)
347
+ response_text = response.decode("utf-8").strip()
348
+
349
+ if response_text == "Sent":
350
+ print(f"Success: {response_text} - {stream_payload}")
351
+ break
352
+ else:
353
+ print(f"Error sending: {stream_payload}")
354
+
355
+ except (ssl.SSLError, ConnectionError) as e:
356
+ print(f"Connection error: {e}, retrying...")
357
+ await asyncio.sleep(retry_delay)
337
358
  except Exception as e:
338
- print(f"Unexpected error: {e}")
359
+ print(f"Unexpected error: {e}, retrying...")
360
+ await asyncio.sleep(retry_delay)
361
+ finally:
362
+ if 'writer' in locals():
363
+ writer.close()
364
+ await writer.wait_closed()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuronum
3
- Version: 8.3.0
3
+ Version: 9.0.0
4
4
  Summary: The E2E Web Engine
5
5
  Home-page: https://neuronum.net
6
6
  Author: Neuronum Cybernetics
@@ -0,0 +1,10 @@
1
+ cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli/main.py,sha256=Ol3kyxh7odt03buze27PGCCaDS0iOax8nbTWYHvK1pA,31798
3
+ neuronum/__init__.py,sha256=qkAz6fpiS2KKnaKwPbS15y7lRCQV-XaWNTkVUarluPk,26
4
+ neuronum/neuronum.py,sha256=3Sway-04-Yb3PHcnbIVumL0kQMB__fiO5w9vDXbkWGk,16186
5
+ neuronum-9.0.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
6
+ neuronum-9.0.0.dist-info/METADATA,sha256=UIovNeYOTcpaZz0FUhJvWQ5EqIu7dDvVpxgrxu8DTWM,3448
7
+ neuronum-9.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ neuronum-9.0.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
+ neuronum-9.0.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
+ neuronum-9.0.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cli/main.py,sha256=rsr2vt0qQjb0IX6V4-s5RkApZz5FPnoPuxuZXMtUlUM,31799
3
- neuronum/__init__.py,sha256=qkAz6fpiS2KKnaKwPbS15y7lRCQV-XaWNTkVUarluPk,26
4
- neuronum/neuronum.py,sha256=4iZjeQ1XTKPhUpojmpkYQiOaNZPZE6t7JUyhLPMOyuA,13183
5
- neuronum-8.3.0.dist-info/licenses/LICENSE.md,sha256=m7pw_FktMNCs4tcy2UXP3QQP2S_je28P1SepdYoo0Xo,1961
6
- neuronum-8.3.0.dist-info/METADATA,sha256=FXfumz1JVjicIaNHN4boBYy85m3FwuDkrrZCYbCyYhc,3448
7
- neuronum-8.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- neuronum-8.3.0.dist-info/entry_points.txt,sha256=XKYBcRNxGeJpZZkDPsa8HA_RaJ7Km_R_JaUq5T9Nk2U,42
9
- neuronum-8.3.0.dist-info/top_level.txt,sha256=ru8Fr84cHm6oHr_DcJ8-uaq3RTiuCRFIr6AC8V0zPu4,13
10
- neuronum-8.3.0.dist-info/RECORD,,