rocket-welder-sdk 1.1.32__py3-none-any.whl → 1.1.34__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 +39 -6
- rocket_welder_sdk/controllers.py +138 -101
- rocket_welder_sdk/frame_metadata.py +138 -0
- rocket_welder_sdk/high_level/__init__.py +52 -0
- rocket_welder_sdk/high_level/client.py +262 -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/schema.py +197 -0
- rocket_welder_sdk/high_level/transport_protocol.py +238 -0
- rocket_welder_sdk/keypoints_protocol.py +642 -0
- rocket_welder_sdk/rocket_welder_client.py +94 -3
- rocket_welder_sdk/segmentation_result.py +420 -0
- rocket_welder_sdk/session_id.py +238 -0
- rocket_welder_sdk/transport/__init__.py +30 -0
- rocket_welder_sdk/transport/frame_sink.py +77 -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-1.1.32.dist-info → rocket_welder_sdk-1.1.34.dist-info}/METADATA +15 -2
- rocket_welder_sdk-1.1.34.dist-info/RECORD +39 -0
- rocket_welder_sdk-1.1.32.dist-info/RECORD +0 -22
- {rocket_welder_sdk-1.1.32.dist-info → rocket_welder_sdk-1.1.34.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.1.32.dist-info → rocket_welder_sdk-1.1.34.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""SessionId parsing utilities for NNG URL generation.
|
|
2
|
+
|
|
3
|
+
SessionId format: ps-{guid} (e.g., ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890)
|
|
4
|
+
Prefix "ps" = PipelineSession.
|
|
5
|
+
|
|
6
|
+
This module provides utilities to:
|
|
7
|
+
1. Parse SessionId from environment variable
|
|
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
|
+
|
|
12
|
+
## URL Configuration Priority
|
|
13
|
+
|
|
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).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import uuid
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
SESSION_ID_PREFIX = "ps-"
|
|
37
|
+
SESSION_ID_ENV_VAR = "SessionId"
|
|
38
|
+
|
|
39
|
+
# Explicit URL environment variables (set by rocket-welder2)
|
|
40
|
+
SEGMENTATION_SINK_URL_ENV = "SEGMENTATION_SINK_URL"
|
|
41
|
+
KEYPOINTS_SINK_URL_ENV = "KEYPOINTS_SINK_URL"
|
|
42
|
+
ACTIONS_SINK_URL_ENV = "ACTIONS_SINK_URL"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_session_id(session_id: str) -> uuid.UUID:
|
|
46
|
+
"""Parse SessionId (ps-{guid}) to extract Guid.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
session_id: SessionId string (e.g., "ps-a1b2c3d4-...")
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
UUID extracted from SessionId
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If session_id format is invalid
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
>>> parse_session_id("ps-a1b2c3d4-e5f6-7890-abcd-ef1234567890")
|
|
59
|
+
UUID('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
|
60
|
+
>>> parse_session_id("a1b2c3d4-e5f6-7890-abcd-ef1234567890") # backwards compat
|
|
61
|
+
UUID('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
|
|
62
|
+
"""
|
|
63
|
+
if session_id.startswith(SESSION_ID_PREFIX):
|
|
64
|
+
return uuid.UUID(session_id[len(SESSION_ID_PREFIX) :])
|
|
65
|
+
# Fallback: try parsing as raw guid for backwards compatibility
|
|
66
|
+
return uuid.UUID(session_id)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_session_id_from_env() -> str | None:
|
|
70
|
+
"""Get SessionId from environment variable.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
SessionId string or None if not set
|
|
74
|
+
"""
|
|
75
|
+
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
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport layer for RocketWelder SDK.
|
|
3
|
+
|
|
4
|
+
Provides transport-agnostic frame sink/source abstractions for protocols.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .frame_sink import IFrameSink
|
|
8
|
+
from .frame_source import IFrameSource
|
|
9
|
+
from .nng_transport import NngFrameSink, NngFrameSource
|
|
10
|
+
from .stream_transport import StreamFrameSink, StreamFrameSource
|
|
11
|
+
from .tcp_transport import TcpFrameSink, TcpFrameSource
|
|
12
|
+
from .unix_socket_transport import (
|
|
13
|
+
UnixSocketFrameSink,
|
|
14
|
+
UnixSocketFrameSource,
|
|
15
|
+
UnixSocketServer,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"IFrameSink",
|
|
20
|
+
"IFrameSource",
|
|
21
|
+
"NngFrameSink",
|
|
22
|
+
"NngFrameSource",
|
|
23
|
+
"StreamFrameSink",
|
|
24
|
+
"StreamFrameSource",
|
|
25
|
+
"TcpFrameSink",
|
|
26
|
+
"TcpFrameSource",
|
|
27
|
+
"UnixSocketFrameSink",
|
|
28
|
+
"UnixSocketFrameSource",
|
|
29
|
+
"UnixSocketServer",
|
|
30
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
|
@@ -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()
|