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 +45 -0
- socketflow/client_side/__init__.py +3 -0
- socketflow/client_side/client.py +383 -0
- socketflow/global_side/__init__.py +13 -0
- socketflow/global_side/blueprint.py +118 -0
- socketflow/global_side/compression.py +155 -0
- socketflow/global_side/dispatcher.py +104 -0
- socketflow/global_side/event.py +69 -0
- socketflow/global_side/exceptions.py +100 -0
- socketflow/global_side/message_handler.py +38 -0
- socketflow/global_side/message_manager.py +98 -0
- socketflow/server_side/__init__.py +3 -0
- socketflow/server_side/server.py +407 -0
- socketflow-0.1.0.dist-info/METADATA +308 -0
- socketflow-0.1.0.dist-info/RECORD +17 -0
- socketflow-0.1.0.dist-info/WHEEL +5 -0
- socketflow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|