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 +12 -0
- subnx-0.1.0/pyproject.toml +25 -0
- subnx-0.1.0/setup.cfg +4 -0
- subnx-0.1.0/subnx/__init__.py +17 -0
- subnx-0.1.0/subnx/client.py +202 -0
- subnx-0.1.0/subnx/events.py +37 -0
- subnx-0.1.0/subnx/handlers.py +74 -0
- subnx-0.1.0/subnx/models.py +202 -0
- subnx-0.1.0/subnx/protocol.py +95 -0
- subnx-0.1.0/subnx/stats.py +26 -0
- subnx-0.1.0/subnx.egg-info/PKG-INFO +12 -0
- subnx-0.1.0/subnx.egg-info/SOURCES.txt +13 -0
- subnx-0.1.0/subnx.egg-info/dependency_links.txt +1 -0
- subnx-0.1.0/subnx.egg-info/requires.txt +1 -0
- subnx-0.1.0/subnx.egg-info/top_level.txt +1 -0
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,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
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psutil>=5.9.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
subnx
|