subnx 0.1.0__tar.gz

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.
subnx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: subnx
3
+ Version: 0.1.0
4
+ Summary: High-concurrency event-driven network framework and SDK
5
+ Author-email: Humberto Manuel Cabrera <humbertomanuel_cabrera@outlook.es>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: psutil>=5.9.0
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "subnx"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Humberto Manuel Cabrera", email = "humbertomanuel_cabrera@outlook.es" },
10
+ ]
11
+ description = "High-concurrency event-driven network framework and SDK"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
19
+ ]
20
+ dependencies = ["psutil>=5.9.0"]
21
+
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["subnx*"]
25
+ exclude = ["examples*", "tests*"]
subnx-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ # orvanta/__init__.py
2
+
3
+ from .models import Event, State, MessageState, ClientInformation, ClientHardwareResourcesStatistics
4
+ from .events import GlobalEvents, EventRouter
5
+ from .client import Client
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = [
9
+ "Client",
10
+ "GlobalEvents",
11
+ "Event",
12
+ "State",
13
+ "MessageState",
14
+ "ClientInformation",
15
+ "ClientHardwareResourcesStatistics",
16
+ "EventRouter"
17
+ ]
@@ -0,0 +1,202 @@
1
+ # subnx/client.py
2
+ import socket
3
+ import ssl
4
+ import json
5
+ import struct
6
+ import threading
7
+ import time
8
+ import random
9
+ import queue
10
+ import signal
11
+ from typing import Any, Callable, Optional, Dict, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from .models import Event, State
15
+
16
+ class Client:
17
+ def __init__(self, host: str, port: str, client_name: str, api_key: str, use_tls: bool = False):
18
+ self.host = host
19
+ self.port = int(port)
20
+ self.name = client_name
21
+ self.api_key = api_key
22
+ self.use_tls = use_tls
23
+
24
+ self._mu = threading.RLock()
25
+ self._write_mu = threading.Lock()
26
+ self.conn: Optional[socket.socket] = None
27
+
28
+ self._closed = False
29
+ self._close_lock = threading.Lock()
30
+
31
+ self.is_ready = threading.Event()
32
+ self.done = threading.Event()
33
+
34
+ self.attempt = 1
35
+ self.min_backoff = 1.0
36
+ self.max_backoff = 30.0
37
+
38
+ # 👈 NUEVO: Mapa seguro para procesos concurrentes (Equivalente a chan State)
39
+ self.pending_requests: Dict[str, queue.Queue] = {}
40
+ self.pending_requests_mu = threading.Lock()
41
+
42
+ threading.Thread(target=self._run, daemon=True).start()
43
+
44
+ def send(self, event: 'Event', timeout: float = 65.0, callback: Callable[['State'], None] = None) -> None:
45
+ with self._close_lock:
46
+ if self._closed:
47
+ raise Exception("client cerrado")
48
+
49
+ # 🌟 MAGIA DE CONCURRENCIA
50
+ ready = self.is_ready.wait(timeout=5.0)
51
+
52
+ with self._mu:
53
+ conn_active = self.conn is not None
54
+
55
+ if not ready or not conn_active:
56
+ raise Exception("conexión no está lista (handshake pendiente o servidor caído)")
57
+
58
+ return event.send_data(self, timeout, callback)
59
+
60
+ def set_ready(self):
61
+ self.is_ready.set()
62
+
63
+ def close(self) -> None:
64
+ with self._close_lock:
65
+ if self._closed:
66
+ return
67
+ self._closed = True
68
+
69
+ self.done.set()
70
+
71
+ with self._mu:
72
+ if self.conn:
73
+ try:
74
+ self.conn.close()
75
+ except:
76
+ pass
77
+ self.conn = None
78
+
79
+ def get_conn(self) -> Optional[socket.socket]:
80
+ with self._mu:
81
+ return self.conn
82
+
83
+ def set_conn(self, conn: socket.socket):
84
+ with self._mu:
85
+ self.conn = conn
86
+
87
+ def clear_conn(self):
88
+ with self._mu:
89
+ if self.conn:
90
+ try:
91
+ self.conn.close()
92
+ except:
93
+ pass
94
+ self.conn = None
95
+ self.is_ready.clear()
96
+
97
+ def _backoff(self) -> float:
98
+ base = self.min_backoff * (2 ** (self.attempt - 1))
99
+ if base > self.max_backoff:
100
+ base = self.max_backoff
101
+
102
+ jitter = random.uniform(0, base / 5)
103
+ if random.choice([True, False]):
104
+ return base - jitter
105
+ return base + jitter
106
+
107
+ def _run(self):
108
+ from .protocol import read_loop
109
+
110
+ while not self._closed:
111
+ start_time = time.time()
112
+
113
+ try:
114
+ raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
115
+ if self.use_tls:
116
+ ctx = ssl.create_default_context()
117
+ ctx.check_hostname = False
118
+ ctx.verify_mode = ssl.CERT_NONE
119
+ self.conn = ctx.wrap_socket(raw_sock, server_hostname=self.host)
120
+ else:
121
+ self.conn = raw_sock
122
+
123
+ self.conn.connect((self.host, self.port))
124
+
125
+ except Exception as e:
126
+ delay = self._backoff()
127
+ print(f"[client {self.name}] error al conectar: {e} (reintento en {delay:.2f}s)")
128
+
129
+ if self.done.wait(timeout=delay):
130
+ return
131
+ self.attempt += 1
132
+ continue
133
+
134
+ self.attempt = 1
135
+ handshake_latency = int((time.time() - start_time) * 1000)
136
+ print(f"[client {self.name}] conectado (latencia: {handshake_latency} ms)")
137
+
138
+ server_status = queue.Queue(maxsize=1)
139
+
140
+ threading.Thread(target=read_loop, args=(self, server_status, float(handshake_latency)), daemon=True).start()
141
+
142
+ while not self.done.is_set():
143
+ try:
144
+ status = server_status.get(timeout=0.5)
145
+ if not status:
146
+ print(f"[client {self.name}] desconectado por el servidor, reconectando...")
147
+ self.clear_conn()
148
+ break
149
+ except queue.Empty:
150
+ continue
151
+
152
+ if self.done.is_set():
153
+ self.clear_conn()
154
+ return
155
+
156
+ def write_packet(self, msg_type: int, event_name: str, server_uid: str, obj_data: Any) -> None:
157
+ try:
158
+ data_bytes = json.dumps(obj_data).encode('utf-8')
159
+ except Exception as e:
160
+ raise Exception(f"error al serializar paquete: {e}")
161
+
162
+ event_name_bytes = event_name.encode('utf-8')
163
+ name_len = len(event_name_bytes)
164
+ uid_bytes = server_uid.encode('utf-8').ljust(36, b'\x00')[:36]
165
+ payload_size = len(data_bytes)
166
+
167
+ try:
168
+ header = struct.pack('>BB36sI', msg_type, name_len, uid_bytes, payload_size)
169
+ except Exception as e:
170
+ raise Exception(f"error al empaquetar el header: {e}")
171
+
172
+ packet = header + event_name_bytes + data_bytes
173
+
174
+ with self._write_mu:
175
+ conn = self.get_conn()
176
+ if not conn:
177
+ raise Exception("conexión no disponible")
178
+ try:
179
+ conn.sendall(packet)
180
+ except Exception as e:
181
+ raise Exception(f"error al escribir en socket: {e}")
182
+
183
+ def listen(self):
184
+ stop_event = threading.Event()
185
+
186
+ def handle_signal(signum, frame):
187
+ stop_event.set()
188
+
189
+ try:
190
+ signal.signal(signal.SIGINT, handle_signal)
191
+ signal.signal(signal.SIGTERM, handle_signal)
192
+ except ValueError:
193
+ pass
194
+
195
+ try:
196
+ while not stop_event.is_set():
197
+ stop_event.wait(0.5)
198
+ except KeyboardInterrupt:
199
+ pass
200
+
201
+ print(f"\n[client {self.name}] Apagando el cliente de forma segura...")
202
+ self.close()
@@ -0,0 +1,37 @@
1
+ # subnx/events.py
2
+ import threading
3
+ from typing import Callable, List, Tuple, Optional, Dict, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .client import Client
7
+ from .models import Event
8
+
9
+ # EventHandler define la firma limpia para el desarrollador
10
+ EventHandler = Callable[['Event', 'Client'], None]
11
+
12
+ class EventRouter:
13
+ def __init__(self):
14
+ self._mu = threading.Lock()
15
+ self.handlers: Dict[str, EventHandler] = {}
16
+
17
+ def add_event(self, event_name: str, handler: EventHandler) -> None:
18
+ with self._mu:
19
+ self.handlers[event_name] = handler
20
+
21
+ def remove_event(self, event_name: str) -> None:
22
+ with self._mu:
23
+ if event_name in self.handlers:
24
+ del self.handlers[event_name]
25
+
26
+ def get_registered_event_names(self) -> List[str]:
27
+ with self._mu:
28
+ return list(self.handlers.keys())
29
+
30
+ def get_handler(self, event_name: str) -> Tuple[Optional[EventHandler], bool]:
31
+ with self._mu:
32
+ handler = self.handlers.get(event_name)
33
+ exists = handler is not None
34
+ return handler, exists
35
+
36
+ # GlobalEvents es el enrutador principal del SDK
37
+ GlobalEvents = EventRouter()
@@ -0,0 +1,74 @@
1
+ # subnx/handlers.py
2
+ import json
3
+ import threading
4
+ import time
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .events import GlobalEvents
8
+
9
+ if TYPE_CHECKING:
10
+ from .client import Client
11
+ from .models import Event
12
+
13
+ def handle_events(e: 'Event', conn: 'Client', client_name: str, latency: int):
14
+ if e.event == "connect":
15
+ handle_connect_event(e, conn, client_name, latency)
16
+ return
17
+
18
+ handler, exists = GlobalEvents.get_handler(e.event)
19
+ if not exists:
20
+ print(f"Evento no reconocido: {e.server_uid} {e.event}")
21
+ from .models import State
22
+ e.reply(conn, State(status=False, message="Evento no reconocido", error="NOT_FOUND"))
23
+ return
24
+
25
+ # 🐕 EL PERRO GUARDIÁN DE DOS FASES
26
+ def guard_dog():
27
+ if e.replied.wait(timeout=0.1):
28
+ return
29
+
30
+ e.extended.set() # Activar prórroga
31
+
32
+ from .models import MessageState
33
+ keep_alive = MessageState(status=True, server_uid=e.server_uid, message="El worker está trabajando", error="", process_status=1)
34
+ keep_alive.send(conn) # 🌟 Método directo del objeto
35
+
36
+ if not e.replied.wait(timeout=28.0):
37
+ abort = MessageState(status=False, server_uid=e.server_uid, message="Worker colgado", error="WORKER_HANG", process_status=2)
38
+ abort.send(conn)
39
+
40
+ threading.Thread(target=guard_dog, daemon=True).start()
41
+
42
+ # 🚀 Ejecutamos al desarrollador
43
+ try:
44
+ handler(e, conn)
45
+ except Exception as ex:
46
+ print(f"Error en el handler del desarrollador: {ex}")
47
+
48
+ def handle_connect_event(e: 'Event', conn: 'Client', client_name: str, latency: int):
49
+ from .stats import get_system_stats
50
+ from .models import ClientHardwareResourcesStatistics, ClientInformation, EventsSubscribed, State
51
+
52
+ stats = ClientHardwareResourcesStatistics()
53
+ try:
54
+ get_system_stats(stats)
55
+ except Exception as err:
56
+ print(f"Error obteniendo estadísticas del sistema: {err}")
57
+ return
58
+
59
+ api_key_str = conn.api_key if conn.api_key else ""
60
+
61
+ info = ClientInformation(
62
+ client_name=client_name,
63
+ api_key=api_key_str,
64
+ latency=float(latency),
65
+ resources=stats,
66
+ events=EventsSubscribed(GlobalEvents.get_registered_event_names())
67
+ )
68
+
69
+ try:
70
+ json_data = json.dumps(info.to_dict())
71
+ e.reply(conn, State(status=True, message="Cliente conectado con exito.", data=json_data))
72
+ conn.set_ready()
73
+ except Exception as err:
74
+ e.reply(conn, State(status=False, message="Error interno de JSON", error=str(err)))
@@ -0,0 +1,202 @@
1
+ # subnx/models.py
2
+ import json
3
+ import uuid
4
+ import threading
5
+ import queue
6
+ import time
7
+ from typing import Any, Callable, List, Optional, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .client import Client
11
+
12
+ class ClientHardwareResourcesStatistics:
13
+ def __init__(self):
14
+ self.cpu_usage: float = 0.0
15
+ self.memory_usage: float = 0.0
16
+ self.disk_usage: float = 0.0
17
+ self.disk_busy: float = 0.0
18
+
19
+ def to_dict(self) -> dict:
20
+ return {
21
+ "cpu_usage": self.cpu_usage,
22
+ "memory_usage": self.memory_usage,
23
+ "disk_usage": self.disk_usage,
24
+ "disk_busy": self.disk_busy
25
+ }
26
+
27
+ class EventsSubscribed:
28
+ def __init__(self, events: List[str]):
29
+ self.events = events
30
+
31
+ def to_dict(self) -> dict:
32
+ return {"events": self.events}
33
+
34
+ class ClientInformation:
35
+ def __init__(self, client_name: str, api_key: str, latency: float,
36
+ resources: ClientHardwareResourcesStatistics, events: EventsSubscribed):
37
+ self.client_name = client_name
38
+ self.api_key = api_key
39
+ self.latency = latency
40
+ self.resources = resources
41
+ self.events = events
42
+
43
+ def to_dict(self) -> dict:
44
+ return {
45
+ "client_name": self.client_name,
46
+ "api_key": self.api_key,
47
+ "latency": self.latency,
48
+ "resources": self.resources.to_dict(),
49
+ "events": self.events.to_dict()
50
+ }
51
+
52
+ class State:
53
+ def __init__(self, status: bool, message: str, error: str = "", data: Any = None, uid: str = ""):
54
+ self.status = status
55
+ self.message = message
56
+ self.error = error
57
+ self.uid = uid
58
+ self.server_uid = ""
59
+ self.data = data
60
+ self._raw_data: str = "" # 🌟 MAGIA: Bytes crudos de Go
61
+
62
+ def to_dict(self) -> dict:
63
+ return {
64
+ "status": self.status,
65
+ "message": self.message,
66
+ "error": self.error,
67
+ "uid": self.uid,
68
+ "data": self.data
69
+ }
70
+
71
+ # 🌟 METODO BIND ENCAPSULADO
72
+ def bind(self, target: Any) -> None:
73
+ if not self._raw_data:
74
+ return
75
+ parsed = json.loads(self._raw_data)
76
+ if isinstance(parsed, dict):
77
+ if isinstance(target, dict):
78
+ target.update(parsed)
79
+ elif hasattr(target, '__dict__'):
80
+ for k, v in parsed.items():
81
+ setattr(target, k, v)
82
+ elif isinstance(parsed, list) and isinstance(target, list):
83
+ target.clear()
84
+ target.extend(parsed)
85
+
86
+ def to_string(self) -> str:
87
+ return f"State{{Status: {self.status}, Message: '{self.message}', Error: '{self.error}', Data: {self.data}, UID: '{self.uid}'}}"
88
+
89
+ class Event:
90
+ def __init__(self, event: str, data: Any, uid: str = ""):
91
+ self.event = event
92
+ self.data = data
93
+ self.uid = uid
94
+ self.server_uid = ""
95
+ self._raw_data: str = "" # 🌟 MAGIA: Bytes crudos de Go
96
+
97
+ # Emulación de los canales y sync.Once
98
+ self.replied = threading.Event()
99
+ self.extended = threading.Event()
100
+ self._reply_once = threading.Lock()
101
+ self._has_replied = False
102
+
103
+ def to_dict(self) -> dict:
104
+ return {
105
+ "event": self.event,
106
+ "data": self.data,
107
+ "uid": self.uid
108
+ }
109
+
110
+ # 🌟 METODO BIND ENCAPSULADO
111
+ def bind(self, target: Any) -> None:
112
+ if not self._raw_data:
113
+ return
114
+ parsed = json.loads(self._raw_data)
115
+ if isinstance(parsed, dict):
116
+ if isinstance(target, dict):
117
+ target.update(parsed)
118
+ elif hasattr(target, '__dict__'):
119
+ for k, v in parsed.items():
120
+ setattr(target, k, v)
121
+ elif isinstance(parsed, list) and isinstance(target, list):
122
+ target.clear()
123
+ target.extend(parsed)
124
+
125
+ # 🌟 ENCAPSULACIÓN: SendData le pertenece a Event
126
+ def send_data(self, client: 'Client', timeout_sec: Optional[float] = 65.0, callback: Optional[Callable[[State], None]] = None) -> None:
127
+ self.uid = str(uuid.uuid4())
128
+
129
+ # 1. Enviamos el Tipo 1 y le pasamos e.event
130
+ client.write_packet(1, self.event, "", self.to_dict())
131
+
132
+ # req := &pendingRequest{ Data: make(chan State, 1) }
133
+ req_queue = queue.Queue(maxsize=1)
134
+
135
+ with client.pending_requests_mu:
136
+ client.pending_requests[self.uid] = req_queue
137
+
138
+ if timeout_sec is None:
139
+ timeout_sec = 65.0
140
+
141
+ if callback is not None:
142
+ def wait_sync():
143
+ try:
144
+ state = req_queue.get(timeout=timeout_sec)
145
+ if state.status:
146
+ callback(state)
147
+ else:
148
+ print(f"error del servidor: {state.error}")
149
+ except queue.Empty:
150
+ with client.pending_requests_mu:
151
+ client.pending_requests.pop(self.uid, None)
152
+ print(f"timeout esperando respuesta para UID {self.uid}")
153
+ threading.Thread(target=wait_sync, daemon=True).start()
154
+ else:
155
+ def wait_async():
156
+ try:
157
+ state = req_queue.get(timeout=timeout_sec)
158
+ if not state.status:
159
+ print(f"Respuesta asíncrona fallida: {state.error}")
160
+ except queue.Empty:
161
+ with client.pending_requests_mu:
162
+ client.pending_requests.pop(self.uid, None)
163
+ threading.Thread(target=wait_async, daemon=True).start()
164
+
165
+ # 🌟 ENCAPSULACIÓN: Reply le pertenece a Event
166
+ def reply(self, client: 'Client', state: State):
167
+ # e.replyOnce.Do(...)
168
+ with self._reply_once:
169
+ if not self._has_replied:
170
+ self.replied.set() # close(e.replied)
171
+ self._has_replied = True
172
+
173
+ state.uid = self.uid
174
+ client.write_packet(2, "", self.server_uid, state.to_dict())
175
+
176
+
177
+ class MessageState:
178
+ def __init__(self, status: bool, server_uid: str, message: str, error: str, process_status: int):
179
+ self.status = status
180
+ self.server_uid = server_uid
181
+ self.message = message
182
+ self.error = error
183
+ self.process_status = process_status
184
+
185
+ def to_dict(self) -> dict:
186
+ return {
187
+ "status": self.status,
188
+ "server_uid": self.server_uid,
189
+ "state": self.message,
190
+ "error": self.error,
191
+ "process_status": self.process_status
192
+ }
193
+
194
+ # 🌟 ENCAPSULACIÓN: send le pertenece a MessageState
195
+ def send(self, client: 'Client'):
196
+ client.write_packet(3, "", self.server_uid, self.to_dict())
197
+
198
+ def to_string(self) -> str:
199
+ return f"MessageState{{Status: {self.status}, ServerUID: '{self.server_uid}', Message: '{self.message}', Error: '{self.error}'}}"
200
+
201
+ # Tipo equivalente a: type ResponseCallback func(response State)
202
+ ResponseCallback = Callable[[State], None]
@@ -0,0 +1,95 @@
1
+ # subnx/protocol.py
2
+ import struct
3
+ import json
4
+ import queue
5
+ import socket
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .client import Client
10
+
11
+ def _recv_exactly(conn: socket.socket, n: int) -> bytes:
12
+ data = bytearray()
13
+ while len(data) < n:
14
+ packet = conn.recv(n - len(data))
15
+ if not packet:
16
+ raise Exception("EOF")
17
+ data.extend(packet)
18
+ return bytes(data)
19
+
20
+ def read_loop(client: 'Client', server_status: queue.Queue, latency: float):
21
+ from .models import Event, State, MessageState
22
+ from .handlers import handle_events
23
+
24
+ while not client._closed:
25
+ try:
26
+ conn = client.get_conn()
27
+ if not conn:
28
+ raise Exception("EOF")
29
+
30
+ # 1. LEER PREFIJO FIJO (42 Bytes)
31
+ header = _recv_exactly(conn, 42)
32
+ msg_type, name_len, uid_bytes, payload_size = struct.unpack('>BB36sI', header)
33
+
34
+ message_uid = uid_bytes.decode('utf-8').rstrip('\x00')
35
+
36
+ # 2. LEER CUERPO DINÁMICO
37
+ body = _recv_exactly(conn, name_len + payload_size)
38
+
39
+ # 3. SEPARAMOS LOS DATOS
40
+ event_name = body[:name_len].decode('utf-8')
41
+ data_bytes = body[name_len:]
42
+
43
+ try:
44
+ obj = json.loads(data_bytes.decode('utf-8'))
45
+ except:
46
+ continue
47
+
48
+ if msg_type == 1:
49
+ event_val = event_name if event_name else obj.get('event', '')
50
+
51
+ # 🌟 TRASLADO MÁGICO
52
+ e = Event(event=event_val, data=obj.get('data'), uid=obj.get('uid', ''))
53
+ e.server_uid = message_uid
54
+
55
+ # Interceptamos el bloque data crudo (equivalente a json.RawMessage)
56
+ if 'data' in obj:
57
+ e._raw_data = json.dumps(obj['data'])
58
+
59
+ ack = MessageState(status=True, server_uid=message_uid, message="ACK recibido Evento", error="", process_status=1)
60
+ ack.send(client)
61
+
62
+ import threading
63
+ threading.Thread(target=handle_events, args=(e, client, client.name, int(latency)), daemon=True).start()
64
+
65
+ elif msg_type == 2:
66
+ # 🌟 TRASLADO MÁGICO PARA STATE
67
+ state = State(
68
+ status=obj.get('status', False),
69
+ message=obj.get('message', ''),
70
+ error=obj.get('error', ''),
71
+ data=obj.get('data'),
72
+ uid=obj.get('uid', '')
73
+ )
74
+ state.server_uid = message_uid
75
+
76
+ if 'data' in obj:
77
+ state._raw_data = json.dumps(obj['data'])
78
+
79
+ ack = MessageState(status=True, server_uid=message_uid, message="ACK recibido State", error="", process_status=2)
80
+ ack.send(client)
81
+
82
+ state_uid = state.uid
83
+ with client.pending_requests_mu:
84
+ if state_uid in client.pending_requests:
85
+ req_queue = client.pending_requests.pop(state_uid)
86
+ req_queue.put(state) # Desbloquea a chan State
87
+ else:
88
+ print(f"Proceso no encontrado para State UID: {state_uid}")
89
+
90
+ elif msg_type == 3:
91
+ pass # ACKs ignorados
92
+
93
+ except Exception as e:
94
+ server_status.put(False)
95
+ return
@@ -0,0 +1,26 @@
1
+ # subnx/stats.py
2
+ import time
3
+ import psutil
4
+ from .models import ClientHardwareResourcesStatistics
5
+
6
+ def get_system_stats(c: ClientHardwareResourcesStatistics) -> None:
7
+ try:
8
+ c.cpu_usage = psutil.cpu_percent(interval=0.0)
9
+ c.memory_usage = psutil.virtual_memory().percent
10
+ c.disk_usage = psutil.disk_usage('/').percent
11
+
12
+ io_start = psutil.disk_io_counters()
13
+ time.sleep(1.0)
14
+ io_end = psutil.disk_io_counters()
15
+
16
+ if io_start and io_end:
17
+ total_io = (io_end.read_bytes - io_start.read_bytes) + (io_end.write_bytes - io_start.write_bytes)
18
+ if total_io > 0:
19
+ max_throughput = float(100 * 1024 * 1024)
20
+ disk_busy = (float(total_io) / max_throughput) * 100.0
21
+ if disk_busy > 100.0:
22
+ disk_busy = 100.0
23
+ c.disk_busy = disk_busy
24
+
25
+ except Exception as e:
26
+ raise Exception(f"error obteniendo métricas: {e}")
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: subnx
3
+ Version: 0.1.0
4
+ Summary: High-concurrency event-driven network framework and SDK
5
+ Author-email: Humberto Manuel Cabrera <humbertomanuel_cabrera@outlook.es>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: psutil>=5.9.0
@@ -0,0 +1,13 @@
1
+ pyproject.toml
2
+ subnx/__init__.py
3
+ subnx/client.py
4
+ subnx/events.py
5
+ subnx/handlers.py
6
+ subnx/models.py
7
+ subnx/protocol.py
8
+ subnx/stats.py
9
+ subnx.egg-info/PKG-INFO
10
+ subnx.egg-info/SOURCES.txt
11
+ subnx.egg-info/dependency_links.txt
12
+ subnx.egg-info/requires.txt
13
+ subnx.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ psutil>=5.9.0
@@ -0,0 +1 @@
1
+ subnx