rocket-welder-sdk 1.1.36.dev14__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.
- rocket_welder_sdk/__init__.py +95 -0
- rocket_welder_sdk/bytes_size.py +234 -0
- rocket_welder_sdk/connection_string.py +291 -0
- rocket_welder_sdk/controllers.py +831 -0
- rocket_welder_sdk/external_controls/__init__.py +30 -0
- rocket_welder_sdk/external_controls/contracts.py +100 -0
- rocket_welder_sdk/external_controls/contracts_old.py +105 -0
- rocket_welder_sdk/frame_metadata.py +138 -0
- rocket_welder_sdk/gst_metadata.py +411 -0
- rocket_welder_sdk/high_level/__init__.py +54 -0
- rocket_welder_sdk/high_level/client.py +235 -0
- rocket_welder_sdk/high_level/connection_strings.py +331 -0
- rocket_welder_sdk/high_level/data_context.py +169 -0
- rocket_welder_sdk/high_level/frame_sink_factory.py +118 -0
- rocket_welder_sdk/high_level/schema.py +195 -0
- rocket_welder_sdk/high_level/transport_protocol.py +238 -0
- rocket_welder_sdk/keypoints_protocol.py +642 -0
- rocket_welder_sdk/opencv_controller.py +278 -0
- rocket_welder_sdk/periodic_timer.py +303 -0
- rocket_welder_sdk/py.typed +2 -0
- rocket_welder_sdk/rocket_welder_client.py +497 -0
- rocket_welder_sdk/segmentation_result.py +420 -0
- rocket_welder_sdk/session_id.py +238 -0
- rocket_welder_sdk/transport/__init__.py +31 -0
- rocket_welder_sdk/transport/frame_sink.py +122 -0
- rocket_welder_sdk/transport/frame_source.py +74 -0
- rocket_welder_sdk/transport/nng_transport.py +197 -0
- rocket_welder_sdk/transport/stream_transport.py +193 -0
- rocket_welder_sdk/transport/tcp_transport.py +154 -0
- rocket_welder_sdk/transport/unix_socket_transport.py +339 -0
- rocket_welder_sdk/ui/__init__.py +48 -0
- rocket_welder_sdk/ui/controls.py +362 -0
- rocket_welder_sdk/ui/icons.py +21628 -0
- rocket_welder_sdk/ui/ui_events_projection.py +226 -0
- rocket_welder_sdk/ui/ui_service.py +358 -0
- rocket_welder_sdk/ui/value_types.py +72 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/METADATA +845 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/RECORD +40 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/WHEEL +5 -0
- rocket_welder_sdk-1.1.36.dev14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Frame sink abstraction for writing frames to any transport."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IFrameSink(ABC):
|
|
7
|
+
"""
|
|
8
|
+
Low-level abstraction for writing discrete frames to any transport.
|
|
9
|
+
|
|
10
|
+
Transport-agnostic interface that handles the question: "where do frames go?"
|
|
11
|
+
This abstraction decouples protocol logic (KeyPoints, SegmentationResults) from
|
|
12
|
+
transport mechanisms (File, TCP, WebSocket, NNG). Each frame is written atomically.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def write_frame(self, frame_data: bytes) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Write a complete frame to the underlying transport synchronously.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
frame_data: Complete frame data to write
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def write_frame_async(self, frame_data: bytes) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Write a complete frame to the underlying transport asynchronously.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
frame_data: Complete frame data to write
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def flush(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Flush any buffered data to the transport synchronously.
|
|
39
|
+
|
|
40
|
+
For message-based transports (NNG, WebSocket), this may be a no-op.
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def flush_async(self) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Flush any buffered data to the transport asynchronously.
|
|
48
|
+
|
|
49
|
+
For message-based transports (NNG, WebSocket), this may be a no-op.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> "IFrameSink":
|
|
54
|
+
"""Context manager entry."""
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, *args: object) -> None:
|
|
58
|
+
"""Context manager exit."""
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
async def __aenter__(self) -> "IFrameSink":
|
|
62
|
+
"""Async context manager entry."""
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def __aexit__(self, *args: object) -> None:
|
|
66
|
+
"""Async context manager exit."""
|
|
67
|
+
await self.close_async()
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
"""Close the sink and release resources."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
async def close_async(self) -> None:
|
|
76
|
+
"""Close the sink and release resources asynchronously."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NullFrameSink(IFrameSink):
|
|
81
|
+
"""
|
|
82
|
+
A frame sink that discards all data.
|
|
83
|
+
|
|
84
|
+
Use when no output URL is configured or for testing.
|
|
85
|
+
Singleton pattern - use NullFrameSink.instance() to get the shared instance.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_instance: "NullFrameSink | None" = None
|
|
89
|
+
|
|
90
|
+
def __new__(cls) -> "NullFrameSink":
|
|
91
|
+
if cls._instance is None:
|
|
92
|
+
cls._instance = super().__new__(cls)
|
|
93
|
+
return cls._instance
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def instance(cls) -> "NullFrameSink":
|
|
97
|
+
"""Get the singleton instance."""
|
|
98
|
+
return cls()
|
|
99
|
+
|
|
100
|
+
def write_frame(self, frame_data: bytes) -> None:
|
|
101
|
+
"""Discards the frame data (no-op)."""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
async def write_frame_async(self, frame_data: bytes) -> None:
|
|
105
|
+
"""Discards the frame data (no-op)."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def flush(self) -> None:
|
|
109
|
+
"""No-op flush."""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
async def flush_async(self) -> None:
|
|
113
|
+
"""No-op flush."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def close(self) -> None:
|
|
117
|
+
"""No-op close (singleton, never actually closed)."""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
async def close_async(self) -> None:
|
|
121
|
+
"""No-op close (singleton, never actually closed)."""
|
|
122
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Frame source abstraction for reading frames from any transport."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IFrameSource(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Low-level abstraction for reading discrete frames from any transport.
|
|
10
|
+
|
|
11
|
+
Transport-agnostic interface that handles the question: "where do frames come from?"
|
|
12
|
+
This abstraction decouples protocol logic (KeyPoints, SegmentationResults) from
|
|
13
|
+
transport mechanisms (File, TCP, WebSocket, NNG). Each frame is read atomically.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def read_frame(self) -> Optional[bytes]:
|
|
18
|
+
"""
|
|
19
|
+
Read a complete frame from the underlying transport synchronously.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Complete frame data, or None if end of stream/no more messages
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def read_frame_async(self) -> Optional[bytes]:
|
|
28
|
+
"""
|
|
29
|
+
Read a complete frame from the underlying transport asynchronously.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Complete frame data, or None if end of stream/no more messages
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def has_more_frames(self) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Check if more frames are available.
|
|
41
|
+
|
|
42
|
+
For streaming transports (file), this checks for EOF.
|
|
43
|
+
For message-based transports (NNG), this may always return True until disconnection.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if more frames are available, False otherwise
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> "IFrameSource":
|
|
51
|
+
"""Context manager entry."""
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, *args: object) -> None:
|
|
55
|
+
"""Context manager exit."""
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
async def __aenter__(self) -> "IFrameSource":
|
|
59
|
+
"""Async context manager entry."""
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(self, *args: object) -> None:
|
|
63
|
+
"""Async context manager exit."""
|
|
64
|
+
await self.close_async()
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
"""Close the source and release resources."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def close_async(self) -> None:
|
|
73
|
+
"""Close the source and release resources asynchronously."""
|
|
74
|
+
pass
|
|
@@ -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()
|