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.
Files changed (40) hide show
  1. rocket_welder_sdk/__init__.py +95 -0
  2. rocket_welder_sdk/bytes_size.py +234 -0
  3. rocket_welder_sdk/connection_string.py +291 -0
  4. rocket_welder_sdk/controllers.py +831 -0
  5. rocket_welder_sdk/external_controls/__init__.py +30 -0
  6. rocket_welder_sdk/external_controls/contracts.py +100 -0
  7. rocket_welder_sdk/external_controls/contracts_old.py +105 -0
  8. rocket_welder_sdk/frame_metadata.py +138 -0
  9. rocket_welder_sdk/gst_metadata.py +411 -0
  10. rocket_welder_sdk/high_level/__init__.py +54 -0
  11. rocket_welder_sdk/high_level/client.py +235 -0
  12. rocket_welder_sdk/high_level/connection_strings.py +331 -0
  13. rocket_welder_sdk/high_level/data_context.py +169 -0
  14. rocket_welder_sdk/high_level/frame_sink_factory.py +118 -0
  15. rocket_welder_sdk/high_level/schema.py +195 -0
  16. rocket_welder_sdk/high_level/transport_protocol.py +238 -0
  17. rocket_welder_sdk/keypoints_protocol.py +642 -0
  18. rocket_welder_sdk/opencv_controller.py +278 -0
  19. rocket_welder_sdk/periodic_timer.py +303 -0
  20. rocket_welder_sdk/py.typed +2 -0
  21. rocket_welder_sdk/rocket_welder_client.py +497 -0
  22. rocket_welder_sdk/segmentation_result.py +420 -0
  23. rocket_welder_sdk/session_id.py +238 -0
  24. rocket_welder_sdk/transport/__init__.py +31 -0
  25. rocket_welder_sdk/transport/frame_sink.py +122 -0
  26. rocket_welder_sdk/transport/frame_source.py +74 -0
  27. rocket_welder_sdk/transport/nng_transport.py +197 -0
  28. rocket_welder_sdk/transport/stream_transport.py +193 -0
  29. rocket_welder_sdk/transport/tcp_transport.py +154 -0
  30. rocket_welder_sdk/transport/unix_socket_transport.py +339 -0
  31. rocket_welder_sdk/ui/__init__.py +48 -0
  32. rocket_welder_sdk/ui/controls.py +362 -0
  33. rocket_welder_sdk/ui/icons.py +21628 -0
  34. rocket_welder_sdk/ui/ui_events_projection.py +226 -0
  35. rocket_welder_sdk/ui/ui_service.py +358 -0
  36. rocket_welder_sdk/ui/value_types.py +72 -0
  37. rocket_welder_sdk-1.1.36.dev14.dist-info/METADATA +845 -0
  38. rocket_welder_sdk-1.1.36.dev14.dist-info/RECORD +40 -0
  39. rocket_welder_sdk-1.1.36.dev14.dist-info/WHEEL +5 -0
  40. 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()