rocket-welder-sdk 1.1.31__py3-none-any.whl → 1.1.33__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,197 @@
1
+ """NNG transport using pynng library.
2
+
3
+ NNG (nanomsg next generation) provides high-performance, scalable messaging patterns.
4
+ Supported patterns:
5
+ - Pub/Sub: One publisher to many subscribers
6
+ - Push/Pull: Load-balanced distribution to workers
7
+ """
8
+
9
+ from typing import Any, Optional, cast
10
+
11
+ import pynng
12
+
13
+ from .frame_sink import IFrameSink
14
+ from .frame_source import IFrameSource
15
+
16
+
17
+ class NngFrameSink(IFrameSink):
18
+ """
19
+ Frame sink that publishes to NNG Pub/Sub or Push/Pull pattern.
20
+
21
+ Each frame is sent as a single NNG message (no framing needed - NNG handles message boundaries).
22
+ """
23
+
24
+ def __init__(self, socket: Any, leave_open: bool = False):
25
+ """
26
+ Create an NNG frame sink from a socket.
27
+
28
+ Args:
29
+ socket: pynng socket (Publisher or Pusher)
30
+ leave_open: If True, doesn't close socket on close
31
+ """
32
+ self._socket: Any = socket
33
+ self._leave_open = leave_open
34
+ self._closed = False
35
+
36
+ @classmethod
37
+ def create_publisher(cls, url: str) -> "NngFrameSink":
38
+ """
39
+ Create an NNG Publisher frame sink bound to the specified URL.
40
+
41
+ Args:
42
+ url: NNG URL (e.g., "tcp://127.0.0.1:5555", "ipc:///tmp/mysocket")
43
+
44
+ Returns:
45
+ Frame sink ready to publish messages
46
+ """
47
+ socket = pynng.Pub0()
48
+ socket.listen(url)
49
+ return cls(socket, leave_open=False)
50
+
51
+ @classmethod
52
+ def create_pusher(cls, url: str, bind_mode: bool = True) -> "NngFrameSink":
53
+ """
54
+ Create an NNG Pusher frame sink.
55
+
56
+ Args:
57
+ url: NNG URL (e.g., "tcp://127.0.0.1:5555", "ipc:///tmp/mysocket")
58
+ bind_mode: If True, listens (bind); if False, dials (connect)
59
+
60
+ Returns:
61
+ Frame sink ready to push messages
62
+ """
63
+ socket = pynng.Push0()
64
+ if bind_mode:
65
+ socket.listen(url)
66
+ else:
67
+ socket.dial(url)
68
+ return cls(socket, leave_open=False)
69
+
70
+ def write_frame(self, frame_data: bytes) -> None:
71
+ """Write frame to NNG socket (no length prefix - NNG handles message boundaries)."""
72
+ if self._closed:
73
+ raise ValueError("Cannot write to closed sink")
74
+
75
+ self._socket.send(frame_data)
76
+
77
+ async def write_frame_async(self, frame_data: bytes) -> None:
78
+ """Write frame asynchronously."""
79
+ if self._closed:
80
+ raise ValueError("Cannot write to closed sink")
81
+
82
+ await self._socket.asend(frame_data)
83
+
84
+ def flush(self) -> None:
85
+ """Flush is a no-op for NNG (data sent immediately)."""
86
+ pass
87
+
88
+ async def flush_async(self) -> None:
89
+ """Flush asynchronously is a no-op for NNG."""
90
+ pass
91
+
92
+ def close(self) -> None:
93
+ """Close the NNG sink."""
94
+ if self._closed:
95
+ return
96
+ self._closed = True
97
+ if not self._leave_open:
98
+ self._socket.close()
99
+
100
+ async def close_async(self) -> None:
101
+ """Close the NNG sink asynchronously."""
102
+ self.close()
103
+
104
+
105
+ class NngFrameSource(IFrameSource):
106
+ """
107
+ Frame source that subscribes to NNG Pub/Sub or Pull pattern.
108
+
109
+ Each NNG message is treated as a complete frame (no framing needed - NNG handles message boundaries).
110
+ """
111
+
112
+ def __init__(self, socket: Any, leave_open: bool = False):
113
+ """
114
+ Create an NNG frame source from a socket.
115
+
116
+ Args:
117
+ socket: pynng socket (Subscriber or Puller)
118
+ leave_open: If True, doesn't close socket on close
119
+ """
120
+ self._socket: Any = socket
121
+ self._leave_open = leave_open
122
+ self._closed = False
123
+
124
+ @classmethod
125
+ def create_subscriber(cls, url: str, topic: bytes = b"") -> "NngFrameSource":
126
+ """
127
+ Create an NNG Subscriber frame source connected to the specified URL.
128
+
129
+ Args:
130
+ url: NNG URL (e.g., "tcp://127.0.0.1:5555", "ipc:///tmp/mysocket")
131
+ topic: Optional topic filter (empty for all messages)
132
+
133
+ Returns:
134
+ Frame source ready to receive messages
135
+ """
136
+ socket = pynng.Sub0()
137
+ socket.subscribe(topic)
138
+ socket.dial(url)
139
+ return cls(socket, leave_open=False)
140
+
141
+ @classmethod
142
+ def create_puller(cls, url: str, bind_mode: bool = True) -> "NngFrameSource":
143
+ """
144
+ Create an NNG Puller frame source.
145
+
146
+ Args:
147
+ url: NNG URL (e.g., "tcp://127.0.0.1:5555", "ipc:///tmp/mysocket")
148
+ bind_mode: If True, listens (bind); if False, dials (connect)
149
+
150
+ Returns:
151
+ Frame source ready to pull messages
152
+ """
153
+ socket = pynng.Pull0()
154
+ if bind_mode:
155
+ socket.listen(url)
156
+ else:
157
+ socket.dial(url)
158
+ return cls(socket, leave_open=False)
159
+
160
+ @property
161
+ def has_more_frames(self) -> bool:
162
+ """Check if more frames available (NNG blocks waiting for messages)."""
163
+ return not self._closed
164
+
165
+ def read_frame(self) -> Optional[bytes]:
166
+ """Read frame from NNG socket (blocking)."""
167
+ if self._closed:
168
+ return None
169
+
170
+ try:
171
+ return cast("bytes", self._socket.recv())
172
+ except pynng.Closed:
173
+ self._closed = True
174
+ return None
175
+
176
+ async def read_frame_async(self) -> Optional[bytes]:
177
+ """Read frame asynchronously."""
178
+ if self._closed:
179
+ return None
180
+
181
+ try:
182
+ return cast("bytes", await self._socket.arecv())
183
+ except pynng.Closed:
184
+ self._closed = True
185
+ return None
186
+
187
+ def close(self) -> None:
188
+ """Close the NNG source."""
189
+ if self._closed:
190
+ return
191
+ self._closed = True
192
+ if not self._leave_open:
193
+ self._socket.close()
194
+
195
+ async def close_async(self) -> None:
196
+ """Close the NNG source asynchronously."""
197
+ self.close()
@@ -0,0 +1,193 @@
1
+ """Stream-based transport (file, memory, etc.)."""
2
+
3
+ from typing import BinaryIO, Optional
4
+
5
+ from .frame_sink import IFrameSink
6
+ from .frame_source import IFrameSource
7
+
8
+
9
+ def _write_varint(stream: BinaryIO, value: int) -> None:
10
+ """Write unsigned integer as varint (Protocol Buffers format)."""
11
+ if value < 0:
12
+ raise ValueError(f"Varint requires non-negative value, got {value}")
13
+
14
+ while value >= 0x80:
15
+ stream.write(bytes([value & 0x7F | 0x80]))
16
+ value >>= 7
17
+ stream.write(bytes([value & 0x7F]))
18
+
19
+
20
+ def _read_varint(stream: BinaryIO) -> int:
21
+ """Read varint from stream and decode to unsigned integer."""
22
+ result = 0
23
+ shift = 0
24
+
25
+ while True:
26
+ if shift >= 35: # Max 5 bytes for uint32
27
+ raise ValueError("Varint too long (corrupted stream)")
28
+
29
+ byte_data = stream.read(1)
30
+ if not byte_data:
31
+ raise EOFError("Unexpected end of stream reading varint")
32
+
33
+ byte = byte_data[0]
34
+ result |= (byte & 0x7F) << shift
35
+ shift += 7
36
+
37
+ if not (byte & 0x80):
38
+ break
39
+
40
+ return result
41
+
42
+
43
+ class StreamFrameSink(IFrameSink):
44
+ """
45
+ Frame sink that writes to a BinaryIO stream (file, memory, etc.).
46
+
47
+ Each frame is prefixed with its length (varint encoding) for frame boundary detection.
48
+ Format: [varint length][frame data]
49
+ """
50
+
51
+ def __init__(self, stream: BinaryIO, leave_open: bool = False):
52
+ """
53
+ Create a stream-based frame sink.
54
+
55
+ Args:
56
+ stream: Binary stream to write to
57
+ leave_open: If True, doesn't close stream on close
58
+ """
59
+ self._stream = stream
60
+ self._leave_open = leave_open
61
+ self._closed = False
62
+
63
+ def write_frame(self, frame_data: bytes) -> None:
64
+ """Write frame data to stream with varint length prefix."""
65
+ if self._closed:
66
+ raise ValueError("Cannot write to closed sink")
67
+
68
+ # Write frame length as varint
69
+ _write_varint(self._stream, len(frame_data))
70
+
71
+ # Write frame data
72
+ self._stream.write(frame_data)
73
+
74
+ async def write_frame_async(self, frame_data: bytes) -> None:
75
+ """Write frame data to stream asynchronously."""
76
+ # For regular streams, just use synchronous write
77
+ # If stream supports async, could use aiofiles
78
+ self.write_frame(frame_data)
79
+
80
+ def flush(self) -> None:
81
+ """Flush buffered data to stream."""
82
+ if not self._closed:
83
+ self._stream.flush()
84
+
85
+ async def flush_async(self) -> None:
86
+ """Flush buffered data to stream asynchronously."""
87
+ self.flush()
88
+
89
+ def close(self) -> None:
90
+ """Close the sink."""
91
+ if self._closed:
92
+ return
93
+ self._closed = True
94
+ if not self._leave_open:
95
+ self._stream.close()
96
+
97
+ async def close_async(self) -> None:
98
+ """Close the sink asynchronously."""
99
+ self.close()
100
+
101
+
102
+ class StreamFrameSource(IFrameSource):
103
+ """
104
+ Frame source that reads from a BinaryIO stream (file, memory, etc.).
105
+
106
+ Reads frames prefixed with varint length for frame boundary detection.
107
+ Format: [varint length][frame data]
108
+ """
109
+
110
+ def __init__(self, stream: BinaryIO, leave_open: bool = False):
111
+ """
112
+ Create a stream-based frame source.
113
+
114
+ Args:
115
+ stream: Binary stream to read from
116
+ leave_open: If True, doesn't close stream on close
117
+ """
118
+ self._stream = stream
119
+ self._leave_open = leave_open
120
+ self._closed = False
121
+
122
+ @property
123
+ def has_more_frames(self) -> bool:
124
+ """Check if more data available in stream."""
125
+ if self._closed:
126
+ return False
127
+ current_pos = self._stream.tell()
128
+ # Try seeking to end to check size
129
+ try:
130
+ self._stream.seek(0, 2) # Seek to end
131
+ end_pos = self._stream.tell()
132
+ self._stream.seek(current_pos) # Restore position
133
+ return current_pos < end_pos
134
+ except OSError:
135
+ # Stream not seekable, assume data available
136
+ return True
137
+
138
+ def read_frame(self) -> Optional[bytes]:
139
+ """
140
+ Read frame from stream with varint length-prefix framing.
141
+
142
+ Returns:
143
+ Frame data bytes, or None if end of stream
144
+ """
145
+ if self._closed:
146
+ return None
147
+
148
+ # Check if stream has data (for seekable streams)
149
+ if hasattr(self._stream, "tell") and hasattr(self._stream, "seek"):
150
+ try:
151
+ current_pos = self._stream.tell()
152
+ self._stream.seek(0, 2) # Seek to end
153
+ end_pos = self._stream.tell()
154
+ self._stream.seek(current_pos) # Restore position
155
+ if current_pos >= end_pos:
156
+ return None
157
+ except OSError:
158
+ pass # Stream not seekable, continue
159
+
160
+ # Read frame length (varint)
161
+ try:
162
+ frame_length = _read_varint(self._stream)
163
+ except EOFError:
164
+ return None
165
+
166
+ if frame_length == 0:
167
+ return b""
168
+
169
+ # Read frame data
170
+ frame_data = self._stream.read(frame_length)
171
+ if len(frame_data) != frame_length:
172
+ raise EOFError(
173
+ f"Unexpected end of stream while reading frame. Expected {frame_length} bytes, got {len(frame_data)}"
174
+ )
175
+
176
+ return frame_data
177
+
178
+ async def read_frame_async(self) -> Optional[bytes]:
179
+ """Read frame from stream asynchronously."""
180
+ # For regular streams, just use synchronous read
181
+ return self.read_frame()
182
+
183
+ def close(self) -> None:
184
+ """Close the source."""
185
+ if self._closed:
186
+ return
187
+ self._closed = True
188
+ if not self._leave_open:
189
+ self._stream.close()
190
+
191
+ async def close_async(self) -> None:
192
+ """Close the source asynchronously."""
193
+ self.close()
@@ -0,0 +1,154 @@
1
+ """TCP transport with length-prefix framing."""
2
+
3
+ import contextlib
4
+ import socket
5
+ import struct
6
+ from typing import Optional
7
+
8
+ from .frame_sink import IFrameSink
9
+ from .frame_source import IFrameSource
10
+
11
+
12
+ class TcpFrameSink(IFrameSink):
13
+ """
14
+ Frame sink that writes to a TCP connection with length-prefix framing.
15
+
16
+ Each frame is prefixed with a 4-byte little-endian length header.
17
+
18
+ Frame format: [Length: 4 bytes LE][Frame Data: N bytes]
19
+ """
20
+
21
+ def __init__(self, sock: socket.socket, leave_open: bool = False):
22
+ """
23
+ Create a TCP frame sink.
24
+
25
+ Args:
26
+ sock: TCP socket to write to
27
+ leave_open: If True, doesn't close socket on close
28
+ """
29
+ self._socket = sock
30
+ self._leave_open = leave_open
31
+ self._closed = False
32
+
33
+ def write_frame(self, frame_data: bytes) -> None:
34
+ """Write frame with length prefix to TCP socket."""
35
+ if self._closed:
36
+ raise ValueError("Cannot write to closed sink")
37
+
38
+ # Write 4-byte length prefix (little-endian)
39
+ length_prefix = struct.pack("<I", len(frame_data))
40
+ self._socket.sendall(length_prefix)
41
+
42
+ # Write frame data
43
+ self._socket.sendall(frame_data)
44
+
45
+ async def write_frame_async(self, frame_data: bytes) -> None:
46
+ """Write frame asynchronously (uses sync socket for now)."""
47
+ self.write_frame(frame_data)
48
+
49
+ def flush(self) -> None:
50
+ """Flush is a no-op for TCP (data sent immediately)."""
51
+ pass
52
+
53
+ async def flush_async(self) -> None:
54
+ """Flush asynchronously is a no-op for TCP."""
55
+ pass
56
+
57
+ def close(self) -> None:
58
+ """Close the TCP sink."""
59
+ if self._closed:
60
+ return
61
+ self._closed = True
62
+ if not self._leave_open:
63
+ with contextlib.suppress(OSError):
64
+ self._socket.shutdown(socket.SHUT_WR)
65
+ self._socket.close()
66
+
67
+ async def close_async(self) -> None:
68
+ """Close the TCP sink asynchronously."""
69
+ self.close()
70
+
71
+
72
+ class TcpFrameSource(IFrameSource):
73
+ """
74
+ Frame source that reads from a TCP connection with length-prefix framing.
75
+
76
+ Each frame is prefixed with a 4-byte little-endian length header.
77
+
78
+ Frame format: [Length: 4 bytes LE][Frame Data: N bytes]
79
+ """
80
+
81
+ def __init__(self, sock: socket.socket, leave_open: bool = False):
82
+ """
83
+ Create a TCP frame source.
84
+
85
+ Args:
86
+ sock: TCP socket to read from
87
+ leave_open: If True, doesn't close socket on close
88
+ """
89
+ self._socket = sock
90
+ self._leave_open = leave_open
91
+ self._closed = False
92
+ self._end_of_stream = False
93
+
94
+ @property
95
+ def has_more_frames(self) -> bool:
96
+ """Check if more frames available."""
97
+ return not self._closed and not self._end_of_stream
98
+
99
+ def read_frame(self) -> Optional[bytes]:
100
+ """Read frame with length prefix from TCP socket."""
101
+ if self._closed or self._end_of_stream:
102
+ return None
103
+
104
+ # Read 4-byte length prefix
105
+ length_data = self._recv_exactly(4)
106
+ if length_data is None or len(length_data) < 4:
107
+ self._end_of_stream = True
108
+ return None
109
+
110
+ frame_length = struct.unpack("<I", length_data)[0]
111
+
112
+ if frame_length == 0:
113
+ return b""
114
+
115
+ if frame_length > 100 * 1024 * 1024: # 100 MB sanity check
116
+ raise ValueError(f"Frame length {frame_length} exceeds maximum")
117
+
118
+ # Read frame data
119
+ frame_data = self._recv_exactly(frame_length)
120
+ if frame_data is None or len(frame_data) < frame_length:
121
+ self._end_of_stream = True
122
+ raise ValueError(
123
+ f"Incomplete frame data: expected {frame_length}, got {len(frame_data) if frame_data else 0}"
124
+ )
125
+
126
+ return frame_data
127
+
128
+ async def read_frame_async(self) -> Optional[bytes]:
129
+ """Read frame asynchronously (uses sync socket for now)."""
130
+ return self.read_frame()
131
+
132
+ def _recv_exactly(self, n: int) -> Optional[bytes]:
133
+ """Receive exactly n bytes from socket."""
134
+ data = b""
135
+ while len(data) < n:
136
+ chunk = self._socket.recv(n - len(data))
137
+ if not chunk:
138
+ return data if data else None
139
+ data += chunk
140
+ return data
141
+
142
+ def close(self) -> None:
143
+ """Close the TCP source."""
144
+ if self._closed:
145
+ return
146
+ self._closed = True
147
+ if not self._leave_open:
148
+ with contextlib.suppress(OSError):
149
+ self._socket.shutdown(socket.SHUT_RD)
150
+ self._socket.close()
151
+
152
+ async def close_async(self) -> None:
153
+ """Close the TCP source asynchronously."""
154
+ self.close()