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.
@@ -0,0 +1,155 @@
1
+ import pickle
2
+ from typing import Any, Dict, Tuple
3
+
4
+ try:
5
+ import lzma
6
+
7
+ HAS_LZMA = True
8
+ except ImportError:
9
+ HAS_LZMA = False
10
+
11
+ try:
12
+ import bz2
13
+
14
+ HAS_BZ2 = True
15
+ except ImportError:
16
+ HAS_BZ2 = False
17
+
18
+ try:
19
+ import zlib
20
+
21
+ HAS_ZLIB = True
22
+ except ImportError:
23
+ HAS_ZLIB = False
24
+
25
+ try:
26
+ import gzip
27
+
28
+ HAS_GZIP = True
29
+ except ImportError:
30
+ HAS_GZIP = False
31
+
32
+ try:
33
+ import zstandard as zstd
34
+
35
+ HAS_ZSTD = True
36
+ except ImportError:
37
+ HAS_ZSTD = False
38
+
39
+ try:
40
+ import brotli
41
+
42
+ HAS_BROTLI = True
43
+ except ImportError:
44
+ HAS_BROTLI = False
45
+
46
+
47
+ class MultiCompressor:
48
+ """
49
+ NOTE:
50
+ - pickle methods are unsafe for untrusted data
51
+ - bytes methods are safe
52
+ """
53
+
54
+ _REGISTRY: Dict[str, Tuple[bool, callable, callable, int]] = {
55
+ "zstd": (
56
+ HAS_ZSTD,
57
+ lambda d, level: zstd.compress(d, level=level),
58
+ zstd.decompress if HAS_ZSTD else None,
59
+ 22,
60
+ ),
61
+ "brotli": (
62
+ HAS_BROTLI,
63
+ lambda d, level: brotli.compress(d, quality=level),
64
+ brotli.decompress if HAS_BROTLI else None,
65
+ 11,
66
+ ),
67
+ "lzma": (
68
+ HAS_LZMA,
69
+ lambda d, level: lzma.compress(d, preset=level),
70
+ lzma.decompress if HAS_LZMA else None,
71
+ 9,
72
+ ),
73
+ "bz2": (
74
+ HAS_BZ2,
75
+ lambda d, level: bz2.compress(d, compresslevel=level),
76
+ bz2.decompress if HAS_BZ2 else None,
77
+ 9,
78
+ ),
79
+ "zlib": (
80
+ HAS_ZLIB,
81
+ lambda d, level: zlib.compress(d, level=level),
82
+ zlib.decompress if HAS_ZLIB else None,
83
+ 9,
84
+ ),
85
+ "gzip": (
86
+ HAS_GZIP,
87
+ lambda d, level: gzip.compress(d, compresslevel=level),
88
+ gzip.decompress if HAS_GZIP else None,
89
+ 9,
90
+ ),
91
+ }
92
+
93
+ HEADER_SEP = b":"
94
+
95
+ @classmethod
96
+ def available_methods(cls):
97
+ return [m for m, (ok, *_) in cls._REGISTRY.items() if ok]
98
+
99
+ # ---------- OBJECT (pickle) ----------
100
+
101
+ @classmethod
102
+ def compress(
103
+ cls,
104
+ obj: Any,
105
+ method: str = "zstd",
106
+ level: int | None = None,
107
+ pickle_protocol: int = pickle.HIGHEST_PROTOCOL,
108
+ ) -> bytes:
109
+ data = pickle.dumps(obj, protocol=pickle_protocol)
110
+ return cls.compress_bytes(data, method, level)
111
+
112
+ @classmethod
113
+ def decompress(cls, data: bytes) -> Any:
114
+ raw = cls.decompress_bytes(data)
115
+ return pickle.loads(raw)
116
+
117
+ # ---------- BYTES (in-memory / file) ----------
118
+
119
+ @classmethod
120
+ def compress_bytes(
121
+ cls,
122
+ data: bytes,
123
+ method: str = "zstd",
124
+ level: int | None = None,
125
+ ) -> bytes:
126
+ method = method.lower()
127
+ if method not in cls._REGISTRY:
128
+ raise ValueError(f"Unsupported method '{method}'")
129
+
130
+ available, compress_fn, _, default_level = cls._REGISTRY[method]
131
+ if not available:
132
+ raise ValueError(f"Method '{method}' not available")
133
+
134
+ if level is None:
135
+ level = default_level
136
+
137
+ compressed = compress_fn(data, level)
138
+ return method.encode() + cls.HEADER_SEP + compressed
139
+
140
+ @classmethod
141
+ def decompress_bytes(cls, data: bytes) -> bytes:
142
+ try:
143
+ method_raw, payload = data.split(cls.HEADER_SEP, 1)
144
+ except ValueError:
145
+ raise ValueError("Invalid data header")
146
+
147
+ method = method_raw.decode()
148
+ if method not in cls._REGISTRY:
149
+ raise ValueError(f"Unknown method '{method}'")
150
+
151
+ available, _, decompress_fn, _ = cls._REGISTRY[method]
152
+ if not available or decompress_fn is None:
153
+ raise ValueError(f"Method '{method}' not available")
154
+
155
+ return decompress_fn(payload)
@@ -0,0 +1,104 @@
1
+ from typing import Dict, List, Callable, Any
2
+ import threading
3
+
4
+
5
+ class EventDispatcher:
6
+ def __init__(self):
7
+ self._event_handlers: Dict[str, List[Callable]] = {}
8
+ self._path_handlers: Dict[str, List[Callable]] = {}
9
+ self._path_middleware: Dict[str, List[Callable]] = {}
10
+ self._server = None
11
+ self._client = None
12
+
13
+ def event(self, event_type: str):
14
+ """Decorator for event handlers"""
15
+
16
+ def decorator(func):
17
+ self.register_event(event_type, func)
18
+ return func
19
+
20
+ return decorator
21
+
22
+ def path(self, path: str, middleware=None):
23
+ """Decorator for path handlers"""
24
+
25
+ def decorator(func):
26
+ self.register_path(path, func, middleware)
27
+ return func
28
+
29
+ return decorator
30
+
31
+ def register_event(self, event_type: str, handler: Callable):
32
+ """Register an event handler"""
33
+ if event_type not in self._event_handlers:
34
+ self._event_handlers[event_type] = []
35
+ self._event_handlers[event_type].append(handler)
36
+
37
+ def register_path(self, path: str, handler: Callable, middleware=None):
38
+ """Register a path handler"""
39
+ if path not in self._path_handlers:
40
+ self._path_handlers[path] = []
41
+ self._path_handlers[path].append(handler)
42
+
43
+ # Register middleware separately if provided
44
+ if middleware:
45
+ if isinstance(middleware, list):
46
+ for m in middleware:
47
+ self.register_path_middleware(path, m)
48
+ else:
49
+ self.register_path_middleware(path, middleware)
50
+
51
+ def register_path_middleware(self, path: str, middleware: Callable):
52
+ """Register path middleware"""
53
+ if path not in self._path_middleware:
54
+ self._path_middleware[path] = []
55
+ self._path_middleware[path].append(middleware)
56
+
57
+ def emit(self, event_type: str, data: Any):
58
+ """Emit event"""
59
+
60
+ def _run_handlers():
61
+ if event_type in self._event_handlers:
62
+ for handler in self._event_handlers[event_type]:
63
+ try:
64
+ handler(data)
65
+ except Exception:
66
+ pass
67
+
68
+ threading.Thread(target=_run_handlers, daemon=True).start()
69
+
70
+ def emit_path(self, path: str, data: Any):
71
+ """Emit path event"""
72
+
73
+ def _run_path_handlers():
74
+ # Run middleware first
75
+ current_data = data
76
+ if path in self._path_middleware:
77
+ for middleware_func in self._path_middleware[path]:
78
+ try:
79
+ result = middleware_func(current_data)
80
+ if result is False: # Middleware rejected the request
81
+ return
82
+ elif result is not None: # Middleware modified the data
83
+ current_data = result
84
+ except Exception:
85
+ # Middleware failed, don't proceed to handlers
86
+ return
87
+
88
+ # Run path handlers
89
+ if path in self._path_handlers:
90
+ for handler in self._path_handlers[path]:
91
+ try:
92
+ handler(current_data)
93
+ except Exception:
94
+ pass
95
+
96
+ threading.Thread(target=_run_path_handlers, daemon=True).start()
97
+
98
+ def register_blueprint(self, blueprint):
99
+ """Register a blueprint"""
100
+ blueprint.register_with_dispatcher(self)
101
+
102
+ def set_event_loop(self, loop):
103
+ """Compatibility method - not needed for sync version"""
104
+ pass
@@ -0,0 +1,69 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Optional, Tuple
3
+
4
+
5
+ @dataclass
6
+ class ConnectData:
7
+ server_addr: Tuple[str, int]
8
+ transport: Any
9
+
10
+
11
+ @dataclass
12
+ class DisconnectData:
13
+ server_addr: Tuple[str, int]
14
+ transport: Any
15
+
16
+
17
+ @dataclass
18
+ class ClientConnectData:
19
+ client_addr: Tuple[str, int]
20
+ transport: Any
21
+
22
+
23
+ @dataclass
24
+ class ClientDisconnectData:
25
+ client_addr: Tuple[str, int]
26
+ transport: Any
27
+
28
+
29
+ @dataclass
30
+ class MessageReceivedData:
31
+ data: Any
32
+ client_addr: Optional[Tuple[str, int]] = None
33
+ server_addr: Optional[Tuple[str, int]] = None
34
+ data_id: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class ErrorData:
39
+ error: Exception
40
+ context: str
41
+
42
+
43
+ @dataclass
44
+ class ServerStartData:
45
+ host: str
46
+ port: int
47
+
48
+
49
+ @dataclass
50
+ class ServerStopData:
51
+ host: str
52
+ port: int
53
+
54
+
55
+ class EventType:
56
+ class Client:
57
+ CONNECT = "client.connect"
58
+ DISCONNECT = "client.disconnect"
59
+ MESSAGE = "client.message"
60
+
61
+ class Server:
62
+ CLIENT_CONNECT = "server.client_connect"
63
+ CLIENT_DISCONNECT = "server.client_disconnect"
64
+ MESSAGE = "server.message"
65
+ START = "server.start"
66
+ STOP = "server.stop"
67
+
68
+ class Global:
69
+ ERROR = "global.error"
@@ -0,0 +1,100 @@
1
+ """
2
+ Exception types for socketflow library
3
+ """
4
+
5
+
6
+ class SocketFlowException(Exception):
7
+ """Base exception for all socketflow exceptions"""
8
+
9
+ pass
10
+
11
+
12
+ class NotConnected(SocketFlowException):
13
+ """Raised when trying to perform operations on a disconnected client/server"""
14
+
15
+ pass
16
+
17
+
18
+ class NoResponse(SocketFlowException):
19
+ """Raised when no response is received within timeout"""
20
+
21
+ pass
22
+
23
+
24
+ class ConnectionTimeout(SocketFlowException):
25
+ """Raised when connection times out"""
26
+
27
+ pass
28
+
29
+
30
+ class KeepaliveTimeout(SocketFlowException):
31
+ """Raised when keepalive timeout occurs"""
32
+
33
+ pass
34
+
35
+
36
+ class InvalidData(SocketFlowException):
37
+ """Raised when invalid data is received or sent"""
38
+
39
+ pass
40
+
41
+
42
+ class ProtocolError(SocketFlowException):
43
+ """Raised when protocol error occurs"""
44
+
45
+ pass
46
+
47
+
48
+ class ServerError(SocketFlowException):
49
+ """Raised when server-side error occurs"""
50
+
51
+ pass
52
+
53
+
54
+ class ClientError(SocketFlowException):
55
+ """Raised when client-side error occurs"""
56
+
57
+ pass
58
+
59
+
60
+ class BlueprintError(SocketFlowException):
61
+ """Raised when blueprint-related error occurs"""
62
+
63
+ pass
64
+
65
+
66
+ class CompressionError(SocketFlowException):
67
+ """Raised when compression/decompression fails"""
68
+
69
+ pass
70
+
71
+
72
+ class MessageHandlerError(SocketFlowException):
73
+ """Raised when message handling fails"""
74
+
75
+ pass
76
+
77
+
78
+ class DispatcherError(SocketFlowException):
79
+ """Raised when dispatcher error occurs"""
80
+
81
+ pass
82
+
83
+
84
+ # Namespace for grouped exception access
85
+ class ExceptionType:
86
+ """Namespace for all exception types"""
87
+
88
+ SocketFlow = SocketFlowException
89
+ NotConnected = NotConnected
90
+ NoResponse = NoResponse
91
+ ConnectionTimeout = ConnectionTimeout
92
+ KeepaliveTimeout = KeepaliveTimeout
93
+ InvalidData = InvalidData
94
+ ProtocolError = ProtocolError
95
+ ServerError = ServerError
96
+ ClientError = ClientError
97
+ BlueprintError = BlueprintError
98
+ CompressionError = CompressionError
99
+ MessageHandlerError = MessageHandlerError
100
+ DispatcherError = DispatcherError
@@ -0,0 +1,38 @@
1
+ from .message_manager import message_manager
2
+ from .compression import MultiCompressor
3
+
4
+
5
+ class MessageHandler:
6
+ @staticmethod
7
+ def unpack_data(encoded_message):
8
+ try:
9
+ data = message_manager.decode(encoded_message)
10
+ headers = data[0]
11
+ body = data[1] if len(data) > 1 else None
12
+ if headers.get("compressed") and headers.get("type") == "__user__":
13
+ body = MultiCompressor.decompress(body)
14
+ return headers, body
15
+ except Exception as e:
16
+ print(e)
17
+ return None, None
18
+
19
+ @staticmethod
20
+ def create_ping():
21
+ """Create a ping message"""
22
+ ping_headers = {
23
+ "type": "__ping__",
24
+ }
25
+ length_bytes, encoded_ping = message_manager.encode_with_length(ping_headers)
26
+ return length_bytes + encoded_ping
27
+
28
+ @staticmethod
29
+ def create_pong():
30
+ """Create a pong message in response to a ping"""
31
+ pong_headers = {
32
+ "type": "__pong__",
33
+ }
34
+ length_bytes, encoded_pong = message_manager.encode_with_length(pong_headers)
35
+ return length_bytes + encoded_pong
36
+
37
+
38
+ message_handler = MessageHandler()
@@ -0,0 +1,98 @@
1
+ import struct
2
+ from typing import Any
3
+ import json
4
+
5
+
6
+ class MessageManager:
7
+ def __init__(self):
8
+ pass
9
+
10
+ def encode(self, *messages: Any) -> bytes:
11
+ if not messages:
12
+ raise ValueError("At least one message must be provided")
13
+
14
+ encoded_parts = []
15
+
16
+ for data in messages:
17
+ if isinstance(data, bytes):
18
+ encoded_parts.append(struct.pack("!B", 0))
19
+ encoded_parts.append(struct.pack("!I", len(data)))
20
+ encoded_parts.append(data)
21
+ elif isinstance(data, str):
22
+ msg_bytes = data.encode("utf-8")
23
+ encoded_parts.append(struct.pack("!B", 1))
24
+ encoded_parts.append(struct.pack("!I", len(msg_bytes)))
25
+ encoded_parts.append(msg_bytes)
26
+ else:
27
+ try:
28
+ json_str = json.dumps(data, ensure_ascii=False)
29
+ json_bytes = json_str.encode("utf-8")
30
+ encoded_parts.append(struct.pack("!B", 2))
31
+ encoded_parts.append(struct.pack("!I", len(json_bytes)))
32
+ encoded_parts.append(json_bytes)
33
+ except (TypeError, ValueError) as e:
34
+ raise TypeError(f"Message is not JSON-serializable: {e}")
35
+
36
+ return b"".join(encoded_parts)
37
+
38
+ def encode_with_length(self, *messages: Any) -> tuple[bytes, bytes]:
39
+ encoded_payload = self.encode(*messages)
40
+ length_bytes = struct.pack(">I", len(encoded_payload))
41
+ return length_bytes, encoded_payload
42
+
43
+ def decode(self, payload: bytes, keys_required: int = None):
44
+ if not payload:
45
+ raise ValueError("Payload cannot be empty")
46
+
47
+ messages = []
48
+ offset = 0
49
+ payload_len = len(payload)
50
+
51
+ while offset < payload_len:
52
+ if offset + 5 > payload_len:
53
+ raise ValueError("Malformed payload: incomplete header")
54
+
55
+ msg_type = struct.unpack("!B", payload[offset : offset + 1])[0]
56
+ offset += 1
57
+ msg_len = struct.unpack("!I", payload[offset : offset + 4])[0]
58
+ offset += 4
59
+
60
+ if offset + msg_len > payload_len:
61
+ raise ValueError("Malformed payload: incomplete message data")
62
+
63
+ msg_data = payload[offset : offset + msg_len]
64
+ offset += msg_len
65
+
66
+ if msg_type == 0:
67
+ messages.append(msg_data)
68
+ elif msg_type == 1:
69
+ try:
70
+ messages.append(msg_data.decode("utf-8"))
71
+ except UnicodeDecodeError as e:
72
+ raise ValueError(f"Failed to decode string data: {e}")
73
+ elif msg_type == 2:
74
+ try:
75
+ json_str = msg_data.decode("utf-8")
76
+ messages.append(json.loads(json_str))
77
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
78
+ raise ValueError(f"Failed to decode JSON data: {e}")
79
+ else:
80
+ raise ValueError(f"Unknown message type: {msg_type}")
81
+
82
+ if keys_required is None:
83
+ return messages
84
+ elif keys_required == 1:
85
+ if len(messages) != 1:
86
+ raise ValueError(f"Expected exactly 1 message, but got {len(messages)}")
87
+ return messages[0]
88
+ elif keys_required > 1:
89
+ if len(messages) != keys_required:
90
+ raise ValueError(
91
+ f"Expected exactly {keys_required} messages, but got {len(messages)}"
92
+ )
93
+ return tuple(messages)
94
+ else:
95
+ raise ValueError("keys_required must be None, 1, or greater")
96
+
97
+
98
+ message_manager = MessageManager()
@@ -0,0 +1,3 @@
1
+ from .server import TcpServer
2
+
3
+ __all__ = ["TcpServer"]