rocket-welder-sdk 1.1.42__py3-none-any.whl → 1.1.44__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.
@@ -1,4 +1,4 @@
1
- """SessionId parsing utilities for NNG URL generation.
1
+ """SessionId parsing utilities.
2
2
 
3
3
  SessionId format: ps-{guid} (e.g., ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890)
4
4
  Prefix "ps" = PipelineSession.
@@ -6,33 +6,20 @@ Prefix "ps" = PipelineSession.
6
6
  This module provides utilities to:
7
7
  1. Parse SessionId from environment variable
8
8
  2. Extract the Guid portion
9
- 3. Generate NNG IPC URLs for streaming results
10
- 4. Read explicit NNG URLs from environment variables (preferred)
11
9
 
12
- ## URL Configuration Priority
10
+ ## URL Configuration
13
11
 
14
- The SDK supports two ways to configure NNG URLs:
15
-
16
- 1. **Explicit URLs (PREFERRED)** - Set by rocket-welder2:
17
- - SEGMENTATION_SINK_URL
18
- - KEYPOINTS_SINK_URL
19
- - ACTIONS_SINK_URL
20
-
21
- 2. **Derived from SessionId (FALLBACK)** - For backwards compatibility:
22
- - SessionId env var → parse GUID → generate URLs
23
-
24
- Use `get_nng_urls_from_env()` for explicit URLs (preferred).
25
- Use `get_nng_urls(session_id)` for SessionId-derived URLs (fallback).
12
+ Output URLs are configured via environment variables set by rocket-welder2:
13
+ - SEGMENTATION_SINK_URL: URL for segmentation output (e.g., socket:///tmp/seg.sock)
14
+ - KEYPOINTS_SINK_URL: URL for keypoints output (e.g., socket:///tmp/kp.sock)
15
+ - ACTIONS_SINK_URL: URL for actions output
26
16
  """
27
17
 
28
18
  from __future__ import annotations
29
19
 
30
- import logging
31
20
  import os
32
21
  import uuid
33
22
 
34
- logger = logging.getLogger(__name__)
35
-
36
23
  SESSION_ID_PREFIX = "ps-"
37
24
  SESSION_ID_ENV_VAR = "SessionId"
38
25
 
@@ -73,166 +60,3 @@ def get_session_id_from_env() -> str | None:
73
60
  SessionId string or None if not set
74
61
  """
75
62
  return os.environ.get(SESSION_ID_ENV_VAR)
76
-
77
-
78
- def get_nng_urls(session_id: str) -> dict[str, str]:
79
- """Generate NNG IPC URLs from SessionId.
80
-
81
- Args:
82
- session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
83
-
84
- Returns:
85
- Dictionary with 'segmentation', 'keypoints', 'actions' URLs
86
-
87
- Examples:
88
- >>> urls = get_nng_urls("ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890")
89
- >>> urls["segmentation"]
90
- 'ipc:///tmp/rw-a1b2c3d4-e5f6-7890-abcd-ef1234567890-seg.sock'
91
- """
92
- guid = parse_session_id(session_id)
93
- return {
94
- "segmentation": f"ipc:///tmp/rw-{guid}-seg.sock",
95
- "keypoints": f"ipc:///tmp/rw-{guid}-kp.sock",
96
- "actions": f"ipc:///tmp/rw-{guid}-actions.sock",
97
- }
98
-
99
-
100
- def get_segmentation_url(session_id: str) -> str:
101
- """Get NNG URL for segmentation stream.
102
-
103
- Args:
104
- session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
105
-
106
- Returns:
107
- IPC URL for segmentation stream
108
- """
109
- guid = parse_session_id(session_id)
110
- return f"ipc:///tmp/rw-{guid}-seg.sock"
111
-
112
-
113
- def get_keypoints_url(session_id: str) -> str:
114
- """Get NNG URL for keypoints stream.
115
-
116
- Args:
117
- session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
118
-
119
- Returns:
120
- IPC URL for keypoints stream
121
- """
122
- guid = parse_session_id(session_id)
123
- return f"ipc:///tmp/rw-{guid}-kp.sock"
124
-
125
-
126
- def get_actions_url(session_id: str) -> str:
127
- """Get NNG URL for actions stream.
128
-
129
- Args:
130
- session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
131
-
132
- Returns:
133
- IPC URL for actions stream
134
- """
135
- guid = parse_session_id(session_id)
136
- return f"ipc:///tmp/rw-{guid}-actions.sock"
137
-
138
-
139
- # ============================================================================
140
- # Explicit URL functions (PREFERRED - URLs set by rocket-welder2)
141
- # ============================================================================
142
-
143
-
144
- def get_nng_urls_from_env() -> dict[str, str | None]:
145
- """Get NNG URLs from explicit environment variables.
146
-
147
- This is the PREFERRED method for getting NNG URLs. rocket-welder2
148
- sets these environment variables when starting containers.
149
-
150
- Returns:
151
- Dictionary with 'segmentation', 'keypoints', 'actions' URLs.
152
- Values are None if not configured.
153
-
154
- Examples:
155
- >>> os.environ["SEGMENTATION_SINK_URL"] = "ipc:///tmp/rw-abc-seg.sock"
156
- >>> urls = get_nng_urls_from_env()
157
- >>> urls["segmentation"]
158
- 'ipc:///tmp/rw-abc-seg.sock'
159
- """
160
- return {
161
- "segmentation": os.environ.get(SEGMENTATION_SINK_URL_ENV),
162
- "keypoints": os.environ.get(KEYPOINTS_SINK_URL_ENV),
163
- "actions": os.environ.get(ACTIONS_SINK_URL_ENV),
164
- }
165
-
166
-
167
- def get_segmentation_url_from_env() -> str | None:
168
- """Get segmentation NNG URL from environment variable.
169
-
170
- Returns:
171
- IPC URL for segmentation stream, or None if not configured.
172
- """
173
- return os.environ.get(SEGMENTATION_SINK_URL_ENV)
174
-
175
-
176
- def get_keypoints_url_from_env() -> str | None:
177
- """Get keypoints NNG URL from environment variable.
178
-
179
- Returns:
180
- IPC URL for keypoints stream, or None if not configured.
181
- """
182
- return os.environ.get(KEYPOINTS_SINK_URL_ENV)
183
-
184
-
185
- def get_actions_url_from_env() -> str | None:
186
- """Get actions NNG URL from environment variable.
187
-
188
- Returns:
189
- IPC URL for actions stream, or None if not configured.
190
- """
191
- return os.environ.get(ACTIONS_SINK_URL_ENV)
192
-
193
-
194
- def has_explicit_nng_urls() -> bool:
195
- """Check if explicit NNG URLs are configured.
196
-
197
- Returns:
198
- True if at least segmentation OR keypoints URL is configured.
199
- """
200
- urls = get_nng_urls_from_env()
201
- return bool(urls["segmentation"] or urls["keypoints"])
202
-
203
-
204
- def get_configured_nng_urls() -> dict[str, str]:
205
- """Get all configured NNG URLs (explicit or derived from SessionId).
206
-
207
- Priority:
208
- 1. Explicit URLs from environment (SEGMENTATION_SINK_URL, etc.)
209
- 2. Derived from SessionId environment variable (fallback)
210
-
211
- Returns:
212
- Dictionary with 'segmentation', 'keypoints', 'actions' URLs.
213
- Only includes URLs that are actually configured.
214
-
215
- Raises:
216
- ValueError: If no NNG URLs are configured (neither explicit nor SessionId).
217
- """
218
- # Try explicit URLs first (preferred)
219
- explicit_urls = get_nng_urls_from_env()
220
- result: dict[str, str] = {}
221
-
222
- for name, url in explicit_urls.items():
223
- if url:
224
- result[name] = url
225
-
226
- # If we have at least one explicit URL, return what we have
227
- if result:
228
- return result
229
-
230
- # Fallback: derive from SessionId
231
- session_id = get_session_id_from_env()
232
- if session_id:
233
- return get_nng_urls(session_id)
234
-
235
- raise ValueError(
236
- "No NNG URLs configured. Set SEGMENTATION_SINK_URL/KEYPOINTS_SINK_URL "
237
- "environment variables, or set SessionId for URL derivation."
238
- )
@@ -6,7 +6,6 @@ Provides transport-agnostic frame sink/source abstractions for protocols.
6
6
 
7
7
  from .frame_sink import IFrameSink, NullFrameSink
8
8
  from .frame_source import IFrameSource
9
- from .nng_transport import NngFrameSink, NngFrameSource
10
9
  from .stream_transport import StreamFrameSink, StreamFrameSource
11
10
  from .tcp_transport import TcpFrameSink, TcpFrameSource
12
11
  from .unix_socket_transport import (
@@ -14,12 +13,16 @@ from .unix_socket_transport import (
14
13
  UnixSocketFrameSource,
15
14
  UnixSocketServer,
16
15
  )
16
+ from .websocket_transport import (
17
+ WebSocketFrameSink,
18
+ WebSocketFrameSource,
19
+ connect_websocket_sink,
20
+ connect_websocket_source,
21
+ )
17
22
 
18
23
  __all__ = [
19
24
  "IFrameSink",
20
25
  "IFrameSource",
21
- "NngFrameSink",
22
- "NngFrameSource",
23
26
  "NullFrameSink",
24
27
  "StreamFrameSink",
25
28
  "StreamFrameSource",
@@ -28,4 +31,8 @@ __all__ = [
28
31
  "UnixSocketFrameSink",
29
32
  "UnixSocketFrameSource",
30
33
  "UnixSocketServer",
34
+ "WebSocketFrameSink",
35
+ "WebSocketFrameSource",
36
+ "connect_websocket_sink",
37
+ "connect_websocket_source",
31
38
  ]
@@ -9,7 +9,7 @@ class IFrameSink(ABC):
9
9
 
10
10
  Transport-agnostic interface that handles the question: "where do frames go?"
11
11
  This abstraction decouples protocol logic (KeyPoints, SegmentationResults) from
12
- transport mechanisms (File, TCP, WebSocket, NNG). Each frame is written atomically.
12
+ transport mechanisms (File, TCP, WebSocket, Unix Socket). Each frame is written atomically.
13
13
  """
14
14
 
15
15
  @abstractmethod
@@ -37,7 +37,7 @@ class IFrameSink(ABC):
37
37
  """
38
38
  Flush any buffered data to the transport synchronously.
39
39
 
40
- For message-based transports (NNG, WebSocket), this may be a no-op.
40
+ For message-based transports (WebSocket), this may be a no-op.
41
41
  """
42
42
  pass
43
43
 
@@ -46,7 +46,7 @@ class IFrameSink(ABC):
46
46
  """
47
47
  Flush any buffered data to the transport asynchronously.
48
48
 
49
- For message-based transports (NNG, WebSocket), this may be a no-op.
49
+ For message-based transports (WebSocket), this may be a no-op.
50
50
  """
51
51
  pass
52
52
 
@@ -10,7 +10,7 @@ class IFrameSource(ABC):
10
10
 
11
11
  Transport-agnostic interface that handles the question: "where do frames come from?"
12
12
  This abstraction decouples protocol logic (KeyPoints, SegmentationResults) from
13
- transport mechanisms (File, TCP, WebSocket, NNG). Each frame is read atomically.
13
+ transport mechanisms (File, TCP, WebSocket, Unix Socket). Each frame is read atomically.
14
14
  """
15
15
 
16
16
  @abstractmethod
@@ -40,7 +40,7 @@ class IFrameSource(ABC):
40
40
  Check if more frames are available.
41
41
 
42
42
  For streaming transports (file), this checks for EOF.
43
- For message-based transports (NNG), this may always return True until disconnection.
43
+ For message-based transports, this may always return True until disconnection.
44
44
 
45
45
  Returns:
46
46
  True if more frames are available, False otherwise
@@ -0,0 +1,316 @@
1
+ """
2
+ WebSocket transport for reading/writing frames.
3
+ Matches C# WebSocketFrameSink and WebSocketFrameSource from RocketWelder.SDK.Transport.
4
+
5
+ Uses the websockets library for async WebSocket support.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from typing import Any, Optional, Protocol
12
+
13
+ from .frame_sink import IFrameSink
14
+ from .frame_source import IFrameSource
15
+
16
+
17
+ class WebSocketProtocol(Protocol):
18
+ """Protocol for WebSocket-like objects (for type checking)."""
19
+
20
+ @property
21
+ def closed(self) -> bool: ...
22
+
23
+ @property
24
+ def state(self) -> Any: ...
25
+
26
+ async def send(self, message: bytes) -> None: ...
27
+
28
+ async def recv(self) -> bytes: ...
29
+
30
+ async def close(self) -> None: ...
31
+
32
+
33
+ class WebSocketFrameSink(IFrameSink):
34
+ """
35
+ Frame sink that writes to a WebSocket connection.
36
+
37
+ Each frame is sent as a single binary WebSocket message.
38
+
39
+ Attributes:
40
+ leave_open: If True, doesn't close WebSocket on disposal
41
+ """
42
+
43
+ __slots__ = ("_closed", "_leave_open", "_websocket")
44
+
45
+ def __init__(self, websocket: WebSocketProtocol, leave_open: bool = False) -> None:
46
+ """
47
+ Create a WebSocket frame sink.
48
+
49
+ Args:
50
+ websocket: WebSocket connection to write to
51
+ leave_open: If True, doesn't close WebSocket on disposal
52
+ """
53
+ if websocket is None:
54
+ raise ValueError("websocket cannot be None")
55
+ self._websocket = websocket
56
+ self._leave_open = leave_open
57
+ self._closed = False
58
+
59
+ def write_frame(self, frame_data: bytes) -> None:
60
+ """
61
+ Write a complete frame to the WebSocket synchronously.
62
+
63
+ Note: WebSocket is inherently async, so this runs the async
64
+ version in a new event loop. Prefer write_frame_async for better performance.
65
+
66
+ Args:
67
+ frame_data: Complete frame data to write
68
+
69
+ Raises:
70
+ RuntimeError: If sink is closed or WebSocket is not open
71
+ """
72
+ if self._closed:
73
+ raise RuntimeError("WebSocketFrameSink is closed")
74
+
75
+ # Use asyncio.run for simplicity (creates new event loop)
76
+ try:
77
+ loop = asyncio.get_running_loop()
78
+ # If we're already in an async context, use run_coroutine_threadsafe
79
+ future = asyncio.run_coroutine_threadsafe(self.write_frame_async(frame_data), loop)
80
+ future.result()
81
+ except RuntimeError:
82
+ # No running event loop, create a new one
83
+ asyncio.run(self.write_frame_async(frame_data))
84
+
85
+ async def write_frame_async(self, frame_data: bytes) -> None:
86
+ """
87
+ Write a complete frame to the WebSocket asynchronously.
88
+
89
+ Args:
90
+ frame_data: Complete frame data to write
91
+
92
+ Raises:
93
+ RuntimeError: If sink is closed or WebSocket is not open
94
+ """
95
+ if self._closed:
96
+ raise RuntimeError("WebSocketFrameSink is closed")
97
+
98
+ if self._websocket.closed:
99
+ raise RuntimeError(f"WebSocket is not open: {self._websocket.state.name}")
100
+
101
+ # Send as single binary message
102
+ await self._websocket.send(frame_data)
103
+
104
+ def flush(self) -> None:
105
+ """
106
+ Flush any buffered data.
107
+
108
+ WebSocket sends immediately, so this is a no-op.
109
+ """
110
+ pass
111
+
112
+ async def flush_async(self) -> None:
113
+ """
114
+ Flush any buffered data asynchronously.
115
+
116
+ WebSocket sends immediately, so this is a no-op.
117
+ """
118
+ pass
119
+
120
+ def close(self) -> None:
121
+ """Close the sink and release resources."""
122
+ if self._closed:
123
+ return
124
+ self._closed = True
125
+
126
+ if not self._leave_open and not self._websocket.closed:
127
+ try:
128
+ loop = asyncio.get_running_loop()
129
+ future = asyncio.run_coroutine_threadsafe(self.close_async(), loop)
130
+ future.result()
131
+ except RuntimeError:
132
+ asyncio.run(self._close_websocket())
133
+
134
+ async def close_async(self) -> None:
135
+ """Close the sink and release resources asynchronously."""
136
+ if self._closed:
137
+ return
138
+ self._closed = True
139
+
140
+ await self._close_websocket()
141
+
142
+ async def _close_websocket(self) -> None:
143
+ """Internal async close helper."""
144
+ if not self._leave_open and not self._websocket.closed:
145
+ try: # noqa: SIM105
146
+ await self._websocket.close()
147
+ except Exception:
148
+ # Best effort close
149
+ pass
150
+
151
+
152
+ class WebSocketFrameSource(IFrameSource):
153
+ """
154
+ Frame source that reads from a WebSocket connection.
155
+
156
+ Each WebSocket binary message is treated as a complete frame.
157
+
158
+ Attributes:
159
+ leave_open: If True, doesn't close WebSocket on disposal
160
+ """
161
+
162
+ __slots__ = ("_closed", "_leave_open", "_websocket")
163
+
164
+ def __init__(self, websocket: WebSocketProtocol, leave_open: bool = False) -> None:
165
+ """
166
+ Create a WebSocket frame source.
167
+
168
+ Args:
169
+ websocket: WebSocket connection to read from
170
+ leave_open: If True, doesn't close WebSocket on disposal
171
+ """
172
+ if websocket is None:
173
+ raise ValueError("websocket cannot be None")
174
+ self._websocket = websocket
175
+ self._leave_open = leave_open
176
+ self._closed = False
177
+
178
+ @property
179
+ def has_more_frames(self) -> bool:
180
+ """
181
+ Check if more frames are available.
182
+
183
+ Returns True if the WebSocket is open or in CloseSent state.
184
+ """
185
+ return not self._websocket.closed
186
+
187
+ def read_frame(self) -> Optional[bytes]:
188
+ """
189
+ Read a complete frame from the WebSocket synchronously.
190
+
191
+ Note: WebSocket is inherently async, so this runs the async
192
+ version. Prefer read_frame_async for better performance.
193
+
194
+ Returns:
195
+ Complete frame data, or None if connection closed
196
+
197
+ Raises:
198
+ RuntimeError: If source is closed
199
+ """
200
+ if self._closed:
201
+ raise RuntimeError("WebSocketFrameSource is closed")
202
+
203
+ try:
204
+ loop = asyncio.get_running_loop()
205
+ future = asyncio.run_coroutine_threadsafe(self.read_frame_async(), loop)
206
+ return future.result()
207
+ except RuntimeError:
208
+ return asyncio.run(self.read_frame_async())
209
+
210
+ async def read_frame_async(self) -> Optional[bytes]:
211
+ """
212
+ Read a complete frame from the WebSocket asynchronously.
213
+
214
+ Returns:
215
+ Complete frame data, or None if connection closed
216
+
217
+ Raises:
218
+ RuntimeError: If source is closed
219
+ ValueError: If received non-binary message
220
+ """
221
+ if self._closed:
222
+ raise RuntimeError("WebSocketFrameSource is closed")
223
+
224
+ if not self.has_more_frames:
225
+ return None
226
+
227
+ try:
228
+ # websockets library handles message framing automatically
229
+ message = await self._websocket.recv()
230
+
231
+ # Ensure we got binary data
232
+ if isinstance(message, str):
233
+ raise ValueError("Expected binary message, got text")
234
+
235
+ return message
236
+
237
+ except Exception:
238
+ # Connection closed or error
239
+ return None
240
+
241
+ def close(self) -> None:
242
+ """Close the source and release resources."""
243
+ if self._closed:
244
+ return
245
+ self._closed = True
246
+
247
+ if not self._leave_open and not self._websocket.closed:
248
+ try:
249
+ loop = asyncio.get_running_loop()
250
+ future = asyncio.run_coroutine_threadsafe(self.close_async(), loop)
251
+ future.result()
252
+ except RuntimeError:
253
+ asyncio.run(self._close_websocket())
254
+
255
+ async def close_async(self) -> None:
256
+ """Close the source and release resources asynchronously."""
257
+ if self._closed:
258
+ return
259
+ self._closed = True
260
+
261
+ await self._close_websocket()
262
+
263
+ async def _close_websocket(self) -> None:
264
+ """Internal async close helper."""
265
+ if not self._leave_open and not self._websocket.closed:
266
+ try: # noqa: SIM105
267
+ await self._websocket.close()
268
+ except Exception:
269
+ # Best effort close
270
+ pass
271
+
272
+
273
+ async def connect_websocket_sink(url: str, leave_open: bool = False) -> WebSocketFrameSink:
274
+ """
275
+ Connect to a WebSocket server and return a frame sink.
276
+
277
+ Args:
278
+ url: WebSocket URL (ws:// or wss://)
279
+ leave_open: If True, doesn't close WebSocket on disposal
280
+
281
+ Returns:
282
+ Connected WebSocketFrameSink
283
+ """
284
+ try:
285
+ import websockets
286
+ except ImportError as e:
287
+ raise ImportError(
288
+ "websockets package is required for WebSocket transport. "
289
+ "Install with: pip install websockets"
290
+ ) from e
291
+
292
+ websocket = await websockets.connect(url)
293
+ return WebSocketFrameSink(websocket, leave_open)
294
+
295
+
296
+ async def connect_websocket_source(url: str, leave_open: bool = False) -> WebSocketFrameSource:
297
+ """
298
+ Connect to a WebSocket server and return a frame source.
299
+
300
+ Args:
301
+ url: WebSocket URL (ws:// or wss://)
302
+ leave_open: If True, doesn't close WebSocket on disposal
303
+
304
+ Returns:
305
+ Connected WebSocketFrameSource
306
+ """
307
+ try:
308
+ import websockets
309
+ except ImportError as e:
310
+ raise ImportError(
311
+ "websockets package is required for WebSocket transport. "
312
+ "Install with: pip install websockets"
313
+ ) from e
314
+
315
+ websocket = await websockets.connect(url)
316
+ return WebSocketFrameSource(websocket, leave_open)