socketflow 0.1.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.
socketflow/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ from .global_side.event import EventType
2
+ from .server_side.server import TcpServer
3
+ from .client_side.client import TcpClient
4
+ from .global_side.blueprint import Blueprint
5
+ from .global_side.message_manager import message_manager, MessageManager
6
+ from .global_side.exceptions import (
7
+ SocketFlowException,
8
+ NotConnected,
9
+ NoResponse,
10
+ ConnectionTimeout,
11
+ KeepaliveTimeout,
12
+ InvalidData,
13
+ ProtocolError,
14
+ ServerError,
15
+ ClientError,
16
+ BlueprintError,
17
+ CompressionError,
18
+ MessageHandlerError,
19
+ DispatcherError,
20
+ ExceptionType,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = [
25
+ "TcpServer",
26
+ "TcpClient",
27
+ "EventType",
28
+ "Blueprint",
29
+ "MessageManager",
30
+ "message_manager",
31
+ "SocketFlowException",
32
+ "NotConnected",
33
+ "NoResponse",
34
+ "ConnectionTimeout",
35
+ "KeepaliveTimeout",
36
+ "InvalidData",
37
+ "ProtocolError",
38
+ "ServerError",
39
+ "ClientError",
40
+ "BlueprintError",
41
+ "CompressionError",
42
+ "MessageHandlerError",
43
+ "DispatcherError",
44
+ "ExceptionType",
45
+ ]
@@ -0,0 +1,3 @@
1
+ from .client import TcpClient
2
+
3
+ __all__ = ["TcpClient"]
@@ -0,0 +1,383 @@
1
+ import threading
2
+ import socket as socket_module
3
+ from ..global_side.event import (
4
+ EventType,
5
+ ConnectData,
6
+ DisconnectData,
7
+ MessageReceivedData,
8
+ ErrorData,
9
+ )
10
+ from ..global_side.dispatcher import EventDispatcher
11
+ from ..global_side.compression import MultiCompressor
12
+ from ..global_side.message_manager import message_manager
13
+ from ..global_side.message_handler import message_handler
14
+ from ..global_side.exceptions import ExceptionType
15
+ from typing import Optional, Union
16
+ import uuid
17
+ import time
18
+ import concurrent.futures
19
+
20
+
21
+ class TcpClientProtocol:
22
+ def __init__(self, client, socket):
23
+ self.client = client
24
+ self.socket = socket
25
+ self._buffer = bytearray()
26
+ self._last_ping_time = None
27
+ self._missed_pings = 0
28
+ self._ping_task = None
29
+
30
+ def handle_data(self, data):
31
+ """Handle incoming data from server"""
32
+ self._buffer.extend(data)
33
+ self._missed_pings = 0
34
+
35
+ offset = 0
36
+ while len(self._buffer) - offset >= 4:
37
+ msg_len = int.from_bytes(self._buffer[offset : offset + 4], byteorder="big")
38
+
39
+ if len(self._buffer) - offset < 4 + msg_len:
40
+ break
41
+
42
+ start = offset + 4
43
+ end = start + msg_len
44
+ message_data = self._buffer[start:end]
45
+
46
+ offset = end
47
+
48
+ headers, body = message_handler.unpack_data(bytes(message_data))
49
+ if not headers or not isinstance(headers, dict):
50
+ error_msg = (
51
+ "Invalid message format"
52
+ if headers is None
53
+ else "Received message with invalid headers format"
54
+ )
55
+ self.client.dispatcher.emit(
56
+ EventType.Global.ERROR,
57
+ ErrorData(
58
+ error=ExceptionType.InvalidData(error_msg),
59
+ context="client.handle_data",
60
+ ),
61
+ )
62
+ continue
63
+
64
+ msg_type = headers.get("type")
65
+ if not msg_type:
66
+ self.client.dispatcher.emit(
67
+ EventType.Global.ERROR,
68
+ ErrorData(
69
+ error=ExceptionType.InvalidData(
70
+ "Received message without type"
71
+ ),
72
+ context="client.handle_data",
73
+ ),
74
+ )
75
+ continue
76
+
77
+ if msg_type == "__ping__":
78
+ pong_message = message_handler.create_pong()
79
+ try:
80
+ self.send_data(pong_message)
81
+ except Exception:
82
+ pass
83
+ elif msg_type == "__user__":
84
+ path = headers.get("path")
85
+ data_id = headers.get("id")
86
+ server_addr = (
87
+ self.socket.getpeername() if self.socket else ("unknown", 0)
88
+ )
89
+ event_data = MessageReceivedData(
90
+ data=body, server_addr=server_addr, data_id=data_id
91
+ )
92
+
93
+ if data_id and data_id in self.client.pending_responses:
94
+ future = self.client.pending_responses.pop(data_id)
95
+ if hasattr(future, "set_result") and not future.done():
96
+ future.set_result(body)
97
+ continue
98
+
99
+ if path:
100
+ self.client.dispatcher.emit_path(path, event_data)
101
+ else:
102
+ self.client.dispatcher.emit(EventType.Client.MESSAGE, event_data)
103
+
104
+ if offset > 0:
105
+ del self._buffer[:offset]
106
+
107
+ def send_data(self, data):
108
+ """Send data to server"""
109
+ if self.socket:
110
+ try:
111
+ self.socket.sendall(data)
112
+ except Exception as e:
113
+ raise ExceptionType.MessageHandlerError(e)
114
+ else:
115
+ raise ExceptionType.NotConnected("Not connected to server")
116
+
117
+ def handle_connection_lost(self):
118
+ """Handle server disconnection"""
119
+ self._ping_task = None
120
+
121
+ self.client._connected = False
122
+ server_addr = self._server_addr if self._server_addr else ("unknown", 0)
123
+
124
+ for data_id, future in list(self.client.pending_responses.items()):
125
+ if hasattr(future, "set_exception") and not future.done():
126
+ future.set_exception(
127
+ ExceptionType.NotConnected(f"Server {server_addr} disconnected")
128
+ )
129
+ self.client.pending_responses.clear()
130
+
131
+ self.client.dispatcher.emit(
132
+ EventType.Client.DISCONNECT,
133
+ DisconnectData(server_addr=server_addr, transport=self.socket),
134
+ )
135
+
136
+ self.socket.close()
137
+
138
+ self._connected = False
139
+
140
+ def keepalive_check(self):
141
+ """Send periodic pings to server"""
142
+ while self.client._connected:
143
+ try:
144
+ ping_message = message_handler.create_ping()
145
+ self.send_data(ping_message)
146
+ except Exception:
147
+ pass
148
+
149
+ if not self.client._connected:
150
+ break
151
+
152
+ time.sleep(self.client.keepalive_interval)
153
+
154
+ self._missed_pings += 1
155
+
156
+ if self._missed_pings >= self.client.keepalive_max_missed:
157
+ self.client.dispatcher.emit(
158
+ EventType.Global.ERROR,
159
+ ErrorData(
160
+ error=ExceptionType.KeepaliveTimeout("Keepalive timeout"),
161
+ context="client.keepalive",
162
+ ),
163
+ )
164
+ self.handle_connection_lost()
165
+ break
166
+
167
+
168
+ class TcpClient:
169
+ def __init__(
170
+ self,
171
+ host: str = "127.0.0.1",
172
+ port: int = 8080,
173
+ compression_type: str = "zlib",
174
+ compression_level: int = 6,
175
+ compress: bool = True,
176
+ keepalive_interval: float = 30.0,
177
+ keepalive_max_missed: int = 3,
178
+ connection_timeout: float = 10.0,
179
+ flow_control: bool = True,
180
+ recv_buffer_size: int = 65536,
181
+ send_buffer_size: int = 65536,
182
+ ):
183
+ self.host = host
184
+ self.port = port
185
+ self.dispatcher = EventDispatcher()
186
+ self._socket = None
187
+ self._protocol = None
188
+ self._connected = False
189
+ self.compression_type = compression_type
190
+ self.compression_level = compression_level
191
+ self.compress = compress
192
+ self.keepalive_interval = keepalive_interval
193
+ self.keepalive_max_missed = keepalive_max_missed
194
+ self.connection_timeout = connection_timeout
195
+ self.flow_control_enabled = flow_control
196
+ self.pending_responses = {}
197
+ self.seperator = b"\r\nSOCKETFLOW\r\n"
198
+ self.recv_buffer_size = recv_buffer_size
199
+ self.send_buffer_size = send_buffer_size
200
+
201
+ def connect(self):
202
+ """Connect to server"""
203
+ try:
204
+ self._socket = socket_module.socket(
205
+ socket_module.AF_INET, socket_module.SOCK_STREAM
206
+ )
207
+ self._socket.settimeout(self.connection_timeout)
208
+
209
+ # Set socket buffer sizes
210
+ self._socket.setsockopt(
211
+ socket_module.SOL_SOCKET, socket_module.SO_RCVBUF, self.recv_buffer_size
212
+ )
213
+ self._socket.setsockopt(
214
+ socket_module.SOL_SOCKET, socket_module.SO_SNDBUF, self.send_buffer_size
215
+ )
216
+
217
+ # Enable TCP Keep-Alive
218
+ self._socket.setsockopt(
219
+ socket_module.SOL_SOCKET, socket_module.SO_KEEPALIVE, 1
220
+ )
221
+
222
+ self._socket.connect((self.host, self.port))
223
+ self._socket.settimeout(None)
224
+
225
+ self._protocol = TcpClientProtocol(self, self._socket)
226
+ self._connected = True
227
+
228
+ threading.Thread(target=self._receive_loop, daemon=True).start()
229
+
230
+ threading.Thread(target=self._protocol.keepalive_check, daemon=True).start()
231
+
232
+ server_addr = self._socket.getpeername()
233
+ self.dispatcher.emit(
234
+ EventType.Client.CONNECT,
235
+ ConnectData(server_addr=server_addr, transport=self._socket),
236
+ )
237
+ # Store server address for later use
238
+ self._protocol._server_addr = server_addr
239
+
240
+ except socket_module.timeout:
241
+ self.dispatcher.emit(
242
+ EventType.Global.ERROR,
243
+ ErrorData(
244
+ error=ExceptionType.ConnectionTimeout(
245
+ f"Connection timeout after {self.connection_timeout}s"
246
+ ),
247
+ context="client.connect",
248
+ ),
249
+ )
250
+ raise ExceptionType.ConnectionTimeout(
251
+ f"Connection timeout after {self.connection_timeout}s"
252
+ )
253
+ except Exception as e:
254
+ self.dispatcher.emit(
255
+ EventType.Global.ERROR, ErrorData(error=e, context="client.connect")
256
+ )
257
+ raise
258
+
259
+ def _receive_loop(self):
260
+ """Main receive loop"""
261
+ while self._connected:
262
+ try:
263
+ data = self._socket.recv(65536)
264
+ if not data:
265
+ break
266
+ self._protocol.handle_data(data)
267
+ except socket_module.timeout:
268
+ pass
269
+ except Exception:
270
+ break
271
+ self._protocol.handle_connection_lost()
272
+
273
+ def disconnect(self):
274
+ """Disconnect from server"""
275
+ if self._connected:
276
+ self._socket.close()
277
+ self._connected = False
278
+
279
+ def send(
280
+ self,
281
+ data: Union[bytes, str],
282
+ data_id: Optional[str] = None,
283
+ path: Optional[str] = None,
284
+ wait_response: bool = False,
285
+ wait_response_timeout: Optional[float] = 30.0,
286
+ ):
287
+ if not self._connected:
288
+ raise ExceptionType.NotConnected("Client is not connected")
289
+
290
+ if data_id is None:
291
+ data_id = str(uuid.uuid4())
292
+
293
+ headers = {
294
+ "type": "__user__",
295
+ "id": data_id,
296
+ "path": path,
297
+ }
298
+
299
+ if self.compress:
300
+ try:
301
+ compressed_msg = MultiCompressor.compress(
302
+ data, method=self.compression_type, level=self.compression_level
303
+ )
304
+ headers["compressed"] = True
305
+ length_bytes, encoded_message = message_manager.encode_with_length(
306
+ headers, compressed_msg
307
+ )
308
+ except Exception as e:
309
+ self.dispatcher.emit(
310
+ EventType.Global.ERROR,
311
+ ErrorData(
312
+ error=ExceptionType.CompressionError(
313
+ f"Compression failed: {e}"
314
+ ),
315
+ context="client.send",
316
+ ),
317
+ )
318
+ raise ExceptionType.CompressionError(f"Compression failed: {e}")
319
+ else:
320
+ length_bytes, encoded_message = message_manager.encode_with_length(
321
+ headers, data
322
+ )
323
+
324
+ self._protocol.send_data(length_bytes + encoded_message)
325
+
326
+ if wait_response:
327
+ future = concurrent.futures.Future()
328
+ self.pending_responses[data_id] = future
329
+
330
+ def timeout_handler():
331
+ if data_id in self.pending_responses:
332
+ self.pending_responses.pop(data_id, None)
333
+ if not future.done():
334
+ future.set_exception(
335
+ ExceptionType.NoResponse(
336
+ f"No response received within {wait_response_timeout} timeout"
337
+ )
338
+ )
339
+
340
+ threading.Timer(wait_response_timeout, timeout_handler).start()
341
+
342
+ try:
343
+ return future.result()
344
+ except ExceptionType.NotConnected:
345
+ raise ExceptionType.NoResponse(
346
+ f"No response received within {wait_response_timeout} timeout - not connected"
347
+ )
348
+ except ExceptionType.NoResponse:
349
+ raise ExceptionType.NoResponse(
350
+ f"No response received within {wait_response_timeout} timeout"
351
+ )
352
+ except Exception as e:
353
+ raise ExceptionType.ClientError(
354
+ f"Error while waiting for response: {e}"
355
+ )
356
+ finally:
357
+ self.pending_responses.pop(data_id, None)
358
+
359
+ def wait(self):
360
+ """Wait for client to stay connected"""
361
+ try:
362
+ while self._connected:
363
+ time.sleep(0.1)
364
+ except KeyboardInterrupt:
365
+ self.disconnect()
366
+
367
+ def connect_and_wait(self):
368
+ """Connect and wait"""
369
+ self.connect()
370
+ self.wait()
371
+
372
+ def is_connected(self):
373
+ return self._connected
374
+
375
+ def event(self, event_type: str):
376
+ return self.dispatcher.event(event_type)
377
+
378
+ def path(self, path: str, middleware=None):
379
+ return self.dispatcher.path(path, middleware)
380
+
381
+ def register_blueprint(self, blueprint):
382
+ blueprint._client = self # Associate blueprint with this client
383
+ self.dispatcher.register_blueprint(blueprint)
@@ -0,0 +1,13 @@
1
+ from .event import EventType
2
+ from .dispatcher import EventDispatcher
3
+ from .compression import MultiCompressor
4
+ from .message_handler import MessageHandler
5
+ from .blueprint import Blueprint
6
+
7
+ __all__ = [
8
+ "EventType",
9
+ "EventDispatcher",
10
+ "MultiCompressor",
11
+ "MessageHandler",
12
+ "Blueprint",
13
+ ]
@@ -0,0 +1,118 @@
1
+ from typing import Dict, List, Callable, Optional
2
+ from .exceptions import ExceptionType
3
+
4
+
5
+ class Blueprint:
6
+ def __init__(self, name: str):
7
+ self.name = name
8
+ self._event_handlers: Dict[str, List[Callable]] = {}
9
+ self._path_handlers: Dict[str, List[Callable]] = {}
10
+ self._path_middleware: Dict[str, List[Callable]] = {}
11
+
12
+ def event(self, event_type: str):
13
+ """Decorator for event handlers"""
14
+
15
+ def decorator(func):
16
+ self.register_event(event_type, func)
17
+ return func
18
+
19
+ return decorator
20
+
21
+ def path(self, path: str, middleware=None):
22
+ """Decorator for path handlers"""
23
+
24
+ def decorator(func):
25
+ self.register_path(path, func, middleware)
26
+ return func
27
+
28
+ return decorator
29
+
30
+ def register_event(self, event_type: str, handler: Callable):
31
+ """Register an event handler"""
32
+ if event_type not in self._event_handlers:
33
+ self._event_handlers[event_type] = []
34
+ self._event_handlers[event_type].append(handler)
35
+
36
+ def register_path(self, path: str, handler: Callable, middleware=None):
37
+ """Register a path handler"""
38
+ if path not in self._path_handlers:
39
+ self._path_handlers[path] = []
40
+ self._path_handlers[path].append(handler)
41
+
42
+ # Store middleware separately if provided
43
+ if middleware:
44
+ if path not in self._path_middleware:
45
+ self._path_middleware[path] = []
46
+ if isinstance(middleware, list):
47
+ self._path_middleware[path].extend(middleware)
48
+ else:
49
+ self._path_middleware[path].append(middleware)
50
+
51
+ def register_with_dispatcher(self, dispatcher):
52
+ """Register all handlers with a dispatcher"""
53
+ for event_type, handlers in self._event_handlers.items():
54
+ for handler in handlers:
55
+ dispatcher.register_event(event_type, handler)
56
+
57
+ for path, handlers in self._path_handlers.items():
58
+ for handler in handlers:
59
+ # Get middleware for this path
60
+ middleware = self._path_middleware.get(path, [])
61
+ dispatcher.register_path(path, handler, middleware)
62
+
63
+ def is_connected(self, client_addr: tuple = None):
64
+ if hasattr(self, "_client") and self._client:
65
+ return self._client.is_connected()
66
+ elif hasattr(self, "_server") and self._server:
67
+ if not client_addr:
68
+ raise ExceptionType.BlueprintError("client_addr parameter is required")
69
+ return self._server.is_connected(client_addr)
70
+ else:
71
+ raise ExceptionType.BlueprintError(
72
+ "Blueprint not registered with client or server"
73
+ )
74
+
75
+ def send(
76
+ self,
77
+ data: bytes,
78
+ data_id: Optional[str] = None,
79
+ path: Optional[str] = None,
80
+ wait_response: bool = False,
81
+ wait_response_timeout: Optional[float] = 30.0,
82
+ ):
83
+ """Send message from client (for blueprints)"""
84
+ if hasattr(self, "_client") and self._client:
85
+ return self._client.send(
86
+ data, data_id, path, wait_response, wait_response_timeout
87
+ )
88
+ raise ExceptionType.BlueprintError("Blueprint not registered with client")
89
+
90
+ def send_client(
91
+ self,
92
+ client_addr: tuple,
93
+ data: bytes,
94
+ data_id: Optional[str] = None,
95
+ path: Optional[str] = None,
96
+ wait_response: bool = False,
97
+ wait_response_timeout: Optional[float] = 30.0,
98
+ ):
99
+ """Send message to client (for blueprints)"""
100
+ if hasattr(self, "_server") and self._server:
101
+ return self._server.send_client(
102
+ client_addr, data, data_id, path, wait_response, wait_response_timeout
103
+ )
104
+ raise ExceptionType.BlueprintError("Blueprint not registered with server")
105
+
106
+ def disconnect(self):
107
+ """Disconnect client (for blueprints)"""
108
+ if hasattr(self, "_client") and self._client:
109
+ self._client.disconnect()
110
+ else:
111
+ raise ExceptionType.BlueprintError("Blueprint not registered with client")
112
+
113
+ def disconnect_client(self, client_addr: tuple):
114
+ """Disconnect client (for blueprints)"""
115
+ if hasattr(self, "_server") and self._server:
116
+ self._server.disconnect_client(client_addr)
117
+ else:
118
+ raise ExceptionType.BlueprintError("Blueprint not registered with server")